diff --git a/apps/simplex-badge-service/src/BadgeService/Catalog.hs b/apps/simplex-badge-service/src/BadgeService/Catalog.hs index b15185da68..2ebb06facf 100644 --- a/apps/simplex-badge-service/src/BadgeService/Catalog.hs +++ b/apps/simplex-badge-service/src/BadgeService/Catalog.hs @@ -19,7 +19,7 @@ import Data.Text (Text) import Data.Time.Clock (UTCTime) import Data.Word (Word32, Word64, Word8) import Simplex.Chat.Badges (BadgeType (..)) -import Simplex.Chat.Badges.Service (BadgeOffer (..), BadgePrice (..)) +import Simplex.Chat.StoreService (BadgeOffer (..), BadgePrice (..)) import Simplex.Chat.Badges.Types (BadgeItemStatus (..), BadgeOfferId (..), BadgePriceId (..), OfferDiscount (..)) import Simplex.Chat.PaymentService.Types (CurrencyAmount (..)) diff --git a/apps/simplex-badge-service/src/BadgeService/Service.hs b/apps/simplex-badge-service/src/BadgeService/Service.hs index 0e189a3489..c1636bccf5 100644 --- a/apps/simplex-badge-service/src/BadgeService/Service.hs +++ b/apps/simplex-badge-service/src/BadgeService/Service.hs @@ -51,7 +51,7 @@ import Data.Word (Word32) import Simplex.Chat.Badges import Simplex.Chat.Badges.Code import Simplex.Chat.Badges.Ledger -import Simplex.Chat.Badges.Service +import Simplex.Chat.StoreService import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..)) import Simplex.Chat.Bot (initializeBotAddress') import Simplex.Chat.Bot.Store (withDB, withDB') @@ -272,44 +272,44 @@ handleServiceRequest key cc trackerQ_ User {userId} reqId sigKey reqData = do Right _ -> pure () Left e -> logError $ "badge service response failed for " <> reqIdT <> ": " <> tshow e -responseObject :: BadgeServiceResponse -> J.Object +responseObject :: StoreServiceResponse -> J.Object responseObject r = case J.toJSON r of J.Object o -> o - _ -> KM.fromList [("type", J.String "error"), ("code", J.toJSON BSEInternal)] + _ -> KM.fromList [("type", J.String "error"), ("code", J.toJSON STEInternal)] -errorResponse :: BadgeServiceErrorCode -> BadgeServiceResponse -errorResponse code = BSPError {code, message = Nothing, retryAfter = badgeErrorRetryAfter code} +errorResponse :: StoreServiceErrorCode -> StoreServiceResponse +errorResponse code = STPError {code, message = Nothing, retryAfter = badgeErrorRetryAfter code} -badgeErrorRetryAfter :: BadgeServiceErrorCode -> Maybe Word32 +badgeErrorRetryAfter :: StoreServiceErrorCode -> Maybe Word32 badgeErrorRetryAfter = \case - BSEPaymentPending -> Just 300 - BSEProviderUnavailable -> Just 300 - BSERateLimited -> Just 60 + STEPaymentPending -> Just 300 + STEProviderUnavailable -> Just 300 + STERateLimited -> Just 60 _ -> Nothing -- | The agent verified the signature, so sigKey is a key the sender holds; a differing purchaseKey would let a client claim a purchase it cannot sign for. -badgeServiceResponse :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> Maybe C.PublicKeyEd25519 -> J.Object -> IO BadgeServiceResponse +badgeServiceResponse :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> Maybe C.PublicKeyEd25519 -> J.Object -> IO StoreServiceResponse badgeServiceResponse key cc trackerQ_ sigKey reqData = case J.fromJSON (J.Object reqData) of - J.Error _ -> pure $ errorResponse BSEBadRequest - J.Success BadgeServiceRequest {version, purchaseKey, request} - | not (version `isCompatible` supportedBadgeServiceVRange) -> pure $ errorResponse BSEUnsupportedVersion - | purchaseKey /= sigKey -> pure $ errorResponse BSEBadRequest + J.Error _ -> pure $ errorResponse STEBadRequest + J.Success StoreServiceRequest {version, purchaseKey, request} + | not (version `isCompatible` supportedStoreServiceVRange) -> pure $ errorResponse STEUnsupportedVersion + | purchaseKey /= sigKey -> pure $ errorResponse STEBadRequest | otherwise -> case request of - BSCRedeemBadgeCode {masterKey, code} -> case purchaseKey of + STCRedeemBadgeCode {masterKey, code} -> case purchaseKey of Just k -> redeemCode key cc trackerQ_ k masterKey code - Nothing -> pure $ errorResponse BSEBadRequest - BSCIssueBadge {balance} -> case purchaseKey of + Nothing -> pure $ errorResponse STEBadRequest + STCIssueBadge {balance} -> case purchaseKey of Just k -> issueBadgeCmd key cc k balance - Nothing -> pure $ errorResponse BSEBadRequest + Nothing -> pure $ errorResponse STEBadRequest -- Every command but redeemBadgeCode needs a key the service already knows; that one creates the purchase, so its key is unknown on a first redemption. _ -> case purchaseKey of - Nothing -> pure $ errorResponse BSEUnsupportedVersion + Nothing -> pure $ errorResponse STEUnsupportedVersion Just k -> withDB' "purchaseKeyExists" cc (`purchaseKeyExists` k) >>= \case - Right True -> pure $ errorResponse BSEUnsupportedVersion - Right False -> pure $ errorResponse BSEUnknownPurchaseKey - Left _ -> pure $ errorResponse BSEInternal + Right True -> pure $ errorResponse STEUnsupportedVersion + Right False -> pure $ errorResponse STEUnknownPurchaseKey + Left _ -> pure $ errorResponse STEInternal badgeNow :: ChatController -> IO UTCTime badgeNow ChatController {config = ChatConfig {badgeCurrentTime}} = badgeCurrentTime @@ -326,18 +326,18 @@ credentialForEntry BadgeIssuerKey {keyIdx, secretKey} masterKey e@StatementEntry issuanceAfter :: StatementEntry -> (StatementEntry, BadgeCredential) -> (StatementEntry, StatementEntry, BadgeCredential) issuanceAfter previous (issued, credential) = (previous, issued, credential) -credentialResponse :: Maybe BadgeCredential -> Maybe T.Text -> [StatementEntry] -> BadgeServiceResponse +credentialResponse :: Maybe BadgeCredential -> Maybe T.Text -> [StatementEntry] -> StoreServiceResponse credentialResponse credential previousEntryId entries = - BSPBadgeCredential {credential, receipt = Nothing, statement = BadgeStatement {entries, previousEntryId}} + STPBadgeCredential {credential, receipt = Nothing, statement = BadgeStatement {entries, previousEntryId}} -- | Nothing is written until the credential is signed, so a signing failure leaves the code unspent. -redeemCode :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> C.PublicKeyEd25519 -> BadgeMasterKey -> T.Text -> IO BadgeServiceResponse +redeemCode :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> C.PublicKeyEd25519 -> BadgeMasterKey -> T.Text -> IO StoreServiceResponse redeemCode key cc trackerQ_ purchaseKey masterKey codeText = case parseBadgeCode codeText of - Nothing -> pure $ errorResponse BSECodeInvalid + Nothing -> pure $ errorResponse STECodeInvalid Just code -> do now <- badgeNow cc withDB "getBadgeCode" cc (readCode now code) >>= \case - Left _ -> pure $ errorResponse BSEInternal + Left _ -> pure $ errorResponse STEInternal Right (Left resp) -> pure resp Right (Right IssuedCode {badgeCodeId, badgeType, months, redeemLimit}) -> do (grantUuid, issueUuid) <- (,) <$> randomId cc <*> randomId cc @@ -346,9 +346,9 @@ redeemCode key cc trackerQ_ purchaseKey masterKey codeText = case parseBadgeCode let granted = grantEntry now grantUuid months SCCode $ emptyEntry now badgeType -- A grant of at least one month starting now always has a month to issue. case issueEntry now issueUuid granted of - Nothing -> pure $ errorResponse BSEInternal + Nothing -> pure $ errorResponse STEInternal Just issued -> credentialForEntry key masterKey issued >>= \case - Left e -> logError ("badge service signing failed: " <> T.pack e) $> errorResponse BSEInternal + Left e -> logError ("badge service signing failed: " <> T.pack e) $> errorResponse STEInternal Right signed -> do -- If the code was revoked or used up while signing, the claim fails. Read the code again to tell the client why. r <- withDB "writeCodeRedemption" cc $ \db -> @@ -356,47 +356,47 @@ redeemCode key cc trackerQ_ purchaseKey masterKey codeText = case parseBadgeCode Nothing -> readCode now code db >>= \case Left resp -> pure (resp, False) - Right _ -> logError "badge service: redeeming a code failed, but the code has uses left and is not revoked" $> (errorResponse BSEInternal, False) + Right _ -> logError "badge service: redeeming a code failed, but the code has uses left and is not revoked" $> (errorResponse STEInternal, False) Just purchaseId -> liftIO $ do appendLedgerPlan db purchaseId [granted] $ Just $ issuanceAfter granted signed entries_ <- getLedgerEntries db purchaseId 0 - pure (maybe (errorResponse BSEInternal) (credentialResponse (Just $ snd signed) Nothing) entries_, True) - let (resp, claimed) = fromRight (errorResponse BSEInternal, False) r + pure (maybe (errorResponse STEInternal) (credentialResponse (Just $ snd signed) Nothing) entries_, True) + let (resp, claimed) = fromRight (errorResponse STEInternal, False) r when (claimed && hasTracker redeemLimit) $ refreshTracker cc trackerQ_ badgeCodeId code pure resp where readCode now code db = liftIO $ getBadgeCode db (badgeCodeHash code) >>= \case - Nothing -> pure $ Left $ errorResponse BSECodeInvalid + Nothing -> pure $ Left $ errorResponse STECodeInvalid Just c@IssuedCode {badgeCodeId, revokedAt, paymentStatus, expiresAt, redeemLimit, redeemCount} -> getCodePurchaseForKey db badgeCodeId purchaseKey >>= \case -- A key that already redeemed gets its credential back without a use, even if the code has since -- expired or been revoked: a client whose reply was lost retries, and would otherwise lose the badge. KeyRedeemed KeyPurchase {badgePurchaseId, credential} -> - maybe (Left $ errorResponse BSEInternal) (Left . credentialResponse (Just credential) Nothing) + maybe (Left $ errorResponse STEInternal) (Left . credentialResponse (Just credential) Nothing) <$> getLedgerEntries db badgePurchaseId 0 -- code_used would make the client drop its keys, so the holder could never get the badge back. KeyRedeemedUnreadable -> - logError "badge service: a redeemed code's credential is missing or unreadable" $> Left (errorResponse BSEInternal) + logError "badge service: a redeemed code's credential is missing or unreadable" $> Left (errorResponse STEInternal) KeyUnredeemed -- Revoked is checked first, so it answers as if the code never existed. - | Just _ <- revokedAt -> pure $ Left $ errorResponse BSECodeInvalid + | Just _ <- revokedAt -> pure $ Left $ errorResponse STECodeInvalid -- Redeeming an unpaid code would issue a free badge, so unpaid is refused. - | CPSUnpaid <- paymentStatus -> pure $ Left $ errorResponse BSEPaymentPending - | redeemCount >= redeemLimit -> pure $ Left $ errorResponse BSECodeUsed - | maybe False (now >=) expiresAt -> pure $ Left $ errorResponse BSECodeExpired + | CPSUnpaid <- paymentStatus -> pure $ Left $ errorResponse STEPaymentPending + | redeemCount >= redeemLimit -> pure $ Left $ errorResponse STECodeUsed + | maybe False (now >=) expiresAt -> pure $ Left $ errorResponse STECodeExpired | otherwise -> pure $ Right c -- | The purchase is reached through the verified signer key and no other way. -issueBadgeCmd :: BadgeIssuerKey -> ChatController -> C.PublicKeyEd25519 -> BadgeBalance -> IO BadgeServiceResponse +issueBadgeCmd :: BadgeIssuerKey -> ChatController -> C.PublicKeyEd25519 -> BadgeBalance -> IO StoreServiceResponse issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do now <- badgeNow cc purchase_ <- withDB' "getBadgePurchase" cc $ \db -> do p_ <- getPurchaseByKey db purchaseKey forM p_ $ \p@ServicePurchase {badgePurchaseId} -> (p,) <$> getLedgerTip db badgePurchaseId case purchase_ of - Left _ -> pure $ errorResponse BSEInternal - Right Nothing -> pure $ errorResponse BSEUnknownPurchaseKey + Left _ -> pure $ errorResponse STEInternal + Right Nothing -> pure $ errorResponse STEUnknownPurchaseKey Right (Just (ServicePurchase {badgePurchaseId, masterKey, badgeType}, tip)) -> do (lapseUuid, issueUuid) <- (,) <$> randomId cc <*> randomId cc let tipEntry = fromMaybe (emptyEntry now badgeType) tip @@ -406,7 +406,7 @@ issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do Nothing -> writeIssued badgePurchaseId tip (maybeToList lapsed) now Nothing Just e -> credentialForEntry key masterKey e >>= \case - Left err -> logError ("badge service signing failed: " <> T.pack err) $> errorResponse BSEInternal + Left err -> logError ("badge service signing failed: " <> T.pack err) $> errorResponse STEInternal Right signed -> writeIssued badgePurchaseId tip (maybeToList lapsed) now $ Just $ issuanceAfter current signed where @@ -416,7 +416,7 @@ issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do tip' <- getLedgerTip db purchaseId when (fmap entryId tip' == fmap entryId tip) $ appendLedgerPlan db purchaseId rows issuance_ issueResponse db purchaseId t - pure $ fromRight (errorResponse BSEInternal) r + pure $ fromRight (errorResponse STEInternal) r -- Only the asserted entry's identity is read, never the months it claims. issueResponse db purchaseId t = do let StatementEntry {entryId = assertedUuid} = lastEntry @@ -425,4 +425,4 @@ issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do -- single opening credit, rather than resending the whole history entries_ <- getLedgerEntries db purchaseId (fromMaybe 0 assertedId) credential_ <- getCurrentIssuance db purchaseId t - pure $ maybe (errorResponse BSEInternal) (credentialResponse credential_ (assertedUuid <$ assertedId)) entries_ + pure $ maybe (errorResponse STEInternal) (credentialResponse credential_ (assertedUuid <$ assertedId)) entries_ diff --git a/apps/simplex-badge-service/src/BadgeService/Store.hs b/apps/simplex-badge-service/src/BadgeService/Store.hs index b51e47bdad..f455b332bd 100644 --- a/apps/simplex-badge-service/src/BadgeService/Store.hs +++ b/apps/simplex-badge-service/src/BadgeService/Store.hs @@ -46,7 +46,7 @@ import Data.Text (Text) import Data.Time.Clock (UTCTime) import Simplex.Chat.Badges (BadgeCredential, BadgeMasterKey (..), BadgeType) import Simplex.Chat.Badges.Ledger -import Simplex.Chat.Badges.Service (StatementEntry (..)) +import Simplex.Chat.StoreService (StatementEntry (..)) import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus, BadgePurchaseStatus (..)) import Simplex.Chat.Store.Shared (insertedRowId) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) diff --git a/apps/simplex-badge-service/src/BadgeService/Store/Invoices.hs b/apps/simplex-badge-service/src/BadgeService/Store/Invoices.hs index a3e177400e..7b31b58b3d 100644 --- a/apps/simplex-badge-service/src/BadgeService/Store/Invoices.hs +++ b/apps/simplex-badge-service/src/BadgeService/Store/Invoices.hs @@ -53,7 +53,7 @@ import Data.Time.Clock (UTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) import Data.Word (Word8, Word32) import Simplex.Chat.Badges (BadgeType (..)) -import Simplex.Chat.Badges.Service (BadgePrice (..), BadgeOffer (..)) +import Simplex.Chat.StoreService (BadgePrice (..), BadgeOffer (..)) import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..), BadgeItemStatus (..), BadgeOfferId (..), BadgePriceId (..), OfferDiscount (..)) import Simplex.Chat.Store.Shared (insertedRowId) import Simplex.Chat.PaymentService.Types (CardProvider (..), CryptoCurrency (..), CurrencyAmount (..), InvoiceId (..), InvoiceStatus (..), PaymentProvider (..), PaymentStatus (..), ServicePaymentDestination (..)) diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 6974cb2339..870bc86dc0 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -15,7 +15,6 @@ This file is generated automatically. - [BadgeInfo](#badgeinfo) - [BadgeProof](#badgeproof) - [BadgeRedeemError](#badgeredeemerror) -- [BadgeServiceErrorCode](#badgeserviceerrorcode) - [BadgeStatus](#badgestatus) - [BadgeType](#badgetype) - [BlockingInfo](#blockinginfo) @@ -202,6 +201,7 @@ This file is generated automatically. - [SndGroupEvent](#sndgroupevent) - [SrvError](#srverror) - [StoreError](#storeerror) +- [StoreServiceErrorCode](#storeserviceerrorcode) - [SubscriptionStatus](#subscriptionstatus) - [SupportGroupPreference](#supportgrouppreference) - [SwitchPhase](#switchphase) @@ -453,7 +453,7 @@ BadgeActive: ServiceError: - type: "serviceError" -- serviceError: [BadgeServiceErrorCode](#badgeserviceerrorcode) +- serviceError: [StoreServiceErrorCode](#storeserviceerrorcode) InvalidResponse: - type: "invalidResponse" @@ -466,30 +466,6 @@ CredentialNotVerified: - type: "credentialNotVerified" ---- - -## BadgeServiceErrorCode - -**Enum type**: -- "bad_request" -- "unsupported_version" -- "unknown_purchase_key" -- "unknown_offer_id" -- "offer_disabled" -- "offer_mismatch" -- "product_unavailable" -- "payment_not_entitled" -- "payment_pending" -- "provider_unavailable" -- "rate_limited" -- "code_invalid" -- "code_used" -- "code_expired" -- "receipt_invalid" -- "receipt_used" -- "internal" - - --- ## BadgeStatus @@ -4435,6 +4411,30 @@ WorkItemError: - errContext: string +--- + +## StoreServiceErrorCode + +**Enum type**: +- "bad_request" +- "unsupported_version" +- "unknown_purchase_key" +- "unknown_offer_id" +- "offer_disabled" +- "offer_mismatch" +- "product_unavailable" +- "payment_not_entitled" +- "payment_pending" +- "provider_unavailable" +- "rate_limited" +- "code_invalid" +- "code_used" +- "code_expired" +- "receipt_invalid" +- "receipt_used" +- "internal" + + --- ## SubscriptionStatus diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 174bba1cba..6ea907fae9 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -35,7 +35,7 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Operators import Simplex.Messaging.Agent.Store.Entity (DBStored (..)) import Simplex.Chat.Badges -import Simplex.Chat.Badges.Service +import Simplex.Chat.StoreService import Simplex.Chat.Names import Simplex.Chat.Types import Simplex.Chat.Types.Preferences @@ -221,7 +221,6 @@ chatTypesDocsData = (sti @AutoAccept, STRecord, "", [], "", ""), (sti @BadgeProof, STRecord, "", [], "", ""), (sti @BadgeRedeemError, STUnion, "BRE", [], "", ""), - (sti @BadgeServiceErrorCode, STEnum' (consSep "BSE" '_'), "", ["BSEUnknown"], "", ""), (sti @BlockingInfo, STRecord, "", [], "", ""), (sti @BlockingReason, STEnum, "BR", [], "", ""), (sti @BrokerErrorType, STUnion, "", [], "", ""), @@ -392,6 +391,7 @@ chatTypesDocsData = (sti @SndGroupEvent, STUnion, "SGE", [], "", ""), (sti @SrvError, STUnion, "SrvErr", [], "", ""), (sti @StoreError, STUnion, "SE", [], "", ""), + (sti @StoreServiceErrorCode, STEnum' (consSep "STE" '_'), "", ["STEUnknown"], "", ""), (sti @SubscriptionStatus, STUnion, "SS", [], "", ""), (sti @SupportGroupPreference, STRecord, "", [], "", ""), (sti @SwitchPhase, STEnum, "SP", [], "", ""), @@ -458,7 +458,6 @@ deriving instance Generic AgentServiceError deriving instance Generic AutoAccept deriving instance Generic BadgeProof deriving instance Generic BadgeRedeemError -deriving instance Generic BadgeServiceErrorCode deriving instance Generic BlockingInfo deriving instance Generic BlockingReason deriving instance Generic BrokerErrorType @@ -634,6 +633,7 @@ deriving instance Generic SndFileTransfer deriving instance Generic SndGroupEvent deriving instance Generic SrvError deriving instance Generic StoreError +deriving instance Generic StoreServiceErrorCode deriving instance Generic SubscriptionStatus deriving instance Generic SupportGroupPreference deriving instance Generic SwitchPhase diff --git a/docs/protocol/badges-rpc.md b/docs/protocol/badges-rpc.md index 8c43ca6b28..85e9e47bc1 100644 --- a/docs/protocol/badges-rpc.md +++ b/docs/protocol/badges-rpc.md @@ -1,6 +1,6 @@ # Badge service RPC protocol -Schema: `badges-rpc.schema.json`, definitions `request` and `response`. Types: `Simplex.Chat.Badges.Service`. Model: `plans/2026-07-30-supporter-badges-v3-ux.md` §3 — cited below as "model". +Schema: `badges-rpc.schema.json`, definitions `request` and `response`. Types: `Simplex.Chat.StoreService`. Model: `plans/2026-07-30-supporter-badges-v3-ux.md` §3 — cited below as "model". ## Transport diff --git a/docs/protocol/badges-rpc.schema.json b/docs/protocol/badges-rpc.schema.json index 01581a5bac..2d302c40a7 100644 --- a/docs/protocol/badges-rpc.schema.json +++ b/docs/protocol/badges-rpc.schema.json @@ -379,7 +379,7 @@ "properties": { "code": { "type": "string", - "metadata": {"comment": "BadgeServiceErrorCode"} + "metadata": {"comment": "StoreServiceErrorCode"} } }, "optionalProperties": { diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index c5b0bab36a..de6e30c0ca 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -284,7 +284,7 @@ export namespace BadgeRedeemError { export interface ServiceError extends Interface { type: "serviceError" - serviceError: BadgeServiceErrorCode + serviceError: StoreServiceErrorCode } export interface InvalidResponse extends Interface { @@ -301,26 +301,6 @@ export namespace BadgeRedeemError { } } -export enum BadgeServiceErrorCode { - Bad_request = "bad_request", - Unsupported_version = "unsupported_version", - Unknown_purchase_key = "unknown_purchase_key", - Unknown_offer_id = "unknown_offer_id", - Offer_disabled = "offer_disabled", - Offer_mismatch = "offer_mismatch", - Product_unavailable = "product_unavailable", - Payment_not_entitled = "payment_not_entitled", - Payment_pending = "payment_pending", - Provider_unavailable = "provider_unavailable", - Rate_limited = "rate_limited", - Code_invalid = "code_invalid", - Code_used = "code_used", - Code_expired = "code_expired", - Receipt_invalid = "receipt_invalid", - Receipt_used = "receipt_used", - Internal = "internal", -} - export enum BadgeStatus { Active = "active", Expired = "expired", @@ -5166,6 +5146,26 @@ export namespace StoreError { } } +export enum StoreServiceErrorCode { + Bad_request = "bad_request", + Unsupported_version = "unsupported_version", + Unknown_purchase_key = "unknown_purchase_key", + Unknown_offer_id = "unknown_offer_id", + Offer_disabled = "offer_disabled", + Offer_mismatch = "offer_mismatch", + Product_unavailable = "product_unavailable", + Payment_not_entitled = "payment_not_entitled", + Payment_pending = "payment_pending", + Provider_unavailable = "provider_unavailable", + Rate_limited = "rate_limited", + Code_invalid = "code_invalid", + Code_used = "code_used", + Code_expired = "code_expired", + Receipt_invalid = "receipt_invalid", + Receipt_used = "receipt_used", + Internal = "internal", +} + export type SubscriptionStatus = | SubscriptionStatus.Active | SubscriptionStatus.Pending diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 6b83e95398..f32ec20db0 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -196,7 +196,7 @@ class BadgeRedeemError_badgeActive(TypedDict): class BadgeRedeemError_serviceError(TypedDict): type: Literal["serviceError"] - serviceError: "BadgeServiceErrorCode" + serviceError: "StoreServiceErrorCode" class BadgeRedeemError_invalidResponse(TypedDict): type: Literal["invalidResponse"] @@ -220,8 +220,6 @@ BadgeRedeemError = ( BadgeRedeemError_Tag = Literal["invalidCode", "serviceNotConfigured", "badgeActive", "serviceError", "invalidResponse", "unknownKeyIndex", "credentialNotVerified"] -BadgeServiceErrorCode = Literal["bad_request", "unsupported_version", "unknown_purchase_key", "unknown_offer_id", "offer_disabled", "offer_mismatch", "product_unavailable", "payment_not_entitled", "payment_pending", "provider_unavailable", "rate_limited", "code_invalid", "code_used", "code_expired", "receipt_invalid", "receipt_used", "internal"] - BadgeStatus = Literal["active", "expired", "expiredOld", "failed", "unknownKey"] BadgeType = Literal["supporter", "legend", "investor"] @@ -3602,6 +3600,8 @@ StoreError = ( StoreError_Tag = Literal["duplicateName", "userNotFound", "relayUserNotFound", "userNotFoundByName", "userNotFoundByContactId", "userNotFoundByGroupId", "userNotFoundByFileId", "userNotFoundByContactRequestId", "contactNotFound", "contactNotFoundByName", "contactNotFoundByMemberId", "contactNotReady", "duplicateContactLink", "userContactLinkNotFound", "contactRequestNotFound", "contactRequestNotFoundByName", "invalidContactRequestEntity", "invalidBusinessChatContactRequest", "groupNotFound", "groupNotFoundByName", "groupMemberNameNotFound", "groupMemberNotFound", "groupMemberNotFoundByIndex", "memberRelationsVectorNotFound", "groupHostMemberNotFound", "groupMemberNotFoundByMemberId", "memberContactGroupMemberNotFound", "invalidMemberRelationUpdate", "groupWithoutUser", "duplicateGroupMember", "duplicateMemberId", "groupAlreadyJoined", "groupInvitationNotFound", "noteFolderAlreadyExists", "noteFolderNotFound", "userNoteFolderNotFound", "sndFileNotFound", "sndFileInvalid", "rcvFileNotFound", "rcvFileDescrNotFound", "fileNotFound", "rcvFileInvalid", "rcvFileInvalidDescrPart", "localFileNoTransfer", "sharedMsgIdNotFoundByFileId", "fileIdNotFoundBySharedMsgId", "sndFileNotFoundXFTP", "rcvFileNotFoundXFTP", "connectionNotFound", "connectionNotFoundById", "connectionNotFoundByMemberId", "pendingConnectionNotFound", "uniqueID", "largeMsg", "internalError", "dBException", "dBBusyError", "badChatItem", "chatItemNotFound", "chatItemNotFoundByText", "chatItemSharedMsgIdNotFound", "chatItemNotFoundByFileId", "chatItemNotFoundByContactId", "chatItemNotFoundByGroupId", "profileNotFound", "duplicateGroupLink", "groupLinkNotFound", "hostMemberIdNotFound", "contactNotFoundByFileId", "noGroupSndStatus", "duplicateGroupMessage", "remoteHostNotFound", "remoteHostUnknown", "remoteHostDuplicateCA", "remoteCtrlNotFound", "remoteCtrlDuplicateCA", "prohibitedDeleteUser", "operatorNotFound", "usageConditionsNotFound", "userChatRelayNotFound", "groupRelayNotFound", "groupRelayNotFoundByMemberId", "invalidQuote", "invalidMention", "invalidDeliveryTask", "deliveryTaskNotFound", "invalidDeliveryJob", "deliveryJobNotFound", "walletNotFound", "duplicateWallet", "workItemError"] +StoreServiceErrorCode = Literal["bad_request", "unsupported_version", "unknown_purchase_key", "unknown_offer_id", "offer_disabled", "offer_mismatch", "product_unavailable", "payment_not_entitled", "payment_pending", "provider_unavailable", "rate_limited", "code_invalid", "code_used", "code_expired", "receipt_invalid", "receipt_used", "internal"] + class SubscriptionStatus_active(TypedDict): type: Literal["active"] diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 1c93a74eed..f78b061101 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -42,7 +42,6 @@ library Simplex.Chat.Badges.CLI Simplex.Chat.Badges.Code Simplex.Chat.Badges.Ledger - Simplex.Chat.Badges.Service Simplex.Chat.Badges.Types Simplex.Chat.Names Simplex.Chat.Call @@ -95,6 +94,7 @@ library Simplex.Chat.Store.Remote Simplex.Chat.Store.Shared Simplex.Chat.Store.Wallets + Simplex.Chat.StoreService Simplex.Chat.Styled Simplex.Chat.Types Simplex.Chat.Types.MemberRelations diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 0522e3c8a6..481026b142 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -71,7 +71,7 @@ defaultChatConfig = chatVRange = supportedChatVRange, callVRange = supportedCallVRange, badgePublicKeys = M.mapKeys fromIntegral entitlementIssuerKeys, - badgeServiceAddress = Just $ either error id $ strDecode "https://smp5.simplex.im/a#ooSNWlEZTO2RPE0Ff5ZoybAs5zEhWLMlQrXesnhaZHM", + storeServiceAddress = Just $ either error id $ strDecode "https://smp5.simplex.im/a#ooSNWlEZTO2RPE0Ff5ZoybAs5zEhWLMlQrXesnhaZHM", badgeCurrentTime = getCurrentTime, badgeRetryInterval = RetryInterval {initialInterval = 30_000000, increaseAfter = 0, maxInterval = 3600_000000}, confirmMigrations = MCConsole, diff --git a/src/Simplex/Chat/Badges.hs b/src/Simplex/Chat/Badges.hs index 437b55e24d..4f6b802432 100644 --- a/src/Simplex/Chat/Badges.hs +++ b/src/Simplex/Chat/Badges.hs @@ -188,7 +188,7 @@ mkBadgeStatus now verified BadgeInfo {badgeExpiry = e} = case verified of -- A badge credential (own, secret) and a proof (a presentation) are independent records. -- badgeKeyIdx is the issuer key index: it tells verifiers which configured key to use. --- Only proofs ride the wire (in a profile); credentials come from the badge service. Neither is +-- Only proofs ride the wire (in a profile); credentials come from the store service. Neither is -- ever serialized as a sum - each travels as its own record, so the JSON carries no credential/proof tag. data BadgeCredential = BadgeCredential diff --git a/src/Simplex/Chat/Badges/Code.hs b/src/Simplex/Chat/Badges/Code.hs index 035ed2e9c5..e9a131a461 100644 --- a/src/Simplex/Chat/Badges/Code.hs +++ b/src/Simplex/Chat/Badges/Code.hs @@ -1,7 +1,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} --- | Badge redemption codes, shared by the client, the badge service and the checkout site. +-- | Badge redemption codes, shared by the client, the store service and the checkout site. -- -- A code is "SB-" and 20 Crockford base32 characters in four groups of five: -- 19 payload characters and a final check character. diff --git a/src/Simplex/Chat/Badges/Ledger.hs b/src/Simplex/Chat/Badges/Ledger.hs index 2f31773d2d..4663bfdf33 100644 --- a/src/Simplex/Chat/Badges/Ledger.hs +++ b/src/Simplex/Chat/Badges/Ledger.hs @@ -25,7 +25,7 @@ import Data.Time.Calendar (addDays, addGregorianMonthsClip, toGregorian) import Data.Time.Calendar.WeekDate (toWeekDate) import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime) import Simplex.Chat.Badges (BadgeType) -import Simplex.Chat.Badges.Service (StatementCreditType (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..)) +import Simplex.Chat.StoreService (StatementCreditType (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..)) -- The calendar difference overshoots by at most one month, so one comparison settles it. monthsBetween :: UTCTime -> UTCTime -> Integer diff --git a/src/Simplex/Chat/Badges/Types.hs b/src/Simplex/Chat/Badges/Types.hs index 1038b2b64f..3b89b33052 100644 --- a/src/Simplex/Chat/Badges/Types.hs +++ b/src/Simplex/Chat/Badges/Types.hs @@ -19,7 +19,7 @@ module Simplex.Chat.Badges.Types LedgerCreditType (..), LedgerDebitType (..), BadgeAlertKind (..), - BadgeServiceErrorCode (..), + StoreServiceErrorCode (..), BadgeIssueFailure (..), BadgeIssueError (..), BadgeFunding (..), @@ -225,31 +225,31 @@ data BadgeAlertPrice = BadgeAlertPrice } deriving (Show) -data BadgeServiceErrorCode - = BSEBadRequest - | BSEUnsupportedVersion - | BSEUnknownPurchaseKey - | BSEUnknownOfferId - | BSEOfferDisabled - | BSEOfferMismatch - | BSEProductUnavailable - | BSEPaymentNotEntitled - | BSEPaymentPending - | BSEProviderUnavailable - | BSERateLimited - | BSECodeInvalid - | BSECodeUsed - | BSECodeExpired - | BSEReceiptInvalid - | BSEReceiptUsed - | BSEInternal - | BSEUnknown Text -- forwards-compatible: service is deployed ahead of clients +data StoreServiceErrorCode + = STEBadRequest + | STEUnsupportedVersion + | STEUnknownPurchaseKey + | STEUnknownOfferId + | STEOfferDisabled + | STEOfferMismatch + | STEProductUnavailable + | STEPaymentNotEntitled + | STEPaymentPending + | STEProviderUnavailable + | STERateLimited + | STECodeInvalid + | STECodeUsed + | STECodeExpired + | STEReceiptInvalid + | STEReceiptUsed + | STEInternal + | STEUnknown Text -- forwards-compatible: service is deployed ahead of clients deriving (Eq, Show) -- | Why a renewal request ended without a credential stored. retryable is the service's own view of -- transience: it gave retryAfter, which holds for codes this version does not know. data BadgeIssueFailure - = BIFServiceError {code :: BadgeServiceErrorCode, retryable :: Bool} + = BIFServiceError {code :: StoreServiceErrorCode, retryable :: Bool} | BIFServiceTimeout | BIFNetwork {agentError :: Text} | BIFInvalidCredential @@ -313,52 +313,52 @@ instance FromField BadgeCodePaymentStatus where fromField = fromTextField_ textD instance ToField BadgeCodePaymentStatus where toField = toField . textEncode -instance TextEncoding BadgeServiceErrorCode where +instance TextEncoding StoreServiceErrorCode where textEncode = \case - BSEBadRequest -> "bad_request" - BSEUnsupportedVersion -> "unsupported_version" - BSEUnknownPurchaseKey -> "unknown_purchase_key" - BSEUnknownOfferId -> "unknown_offer_id" - BSEOfferDisabled -> "offer_disabled" - BSEOfferMismatch -> "offer_mismatch" - BSEProductUnavailable -> "product_unavailable" - BSEPaymentNotEntitled -> "payment_not_entitled" - BSEPaymentPending -> "payment_pending" - BSEProviderUnavailable -> "provider_unavailable" - BSERateLimited -> "rate_limited" - BSECodeInvalid -> "code_invalid" - BSECodeUsed -> "code_used" - BSECodeExpired -> "code_expired" - BSEReceiptInvalid -> "receipt_invalid" - BSEReceiptUsed -> "receipt_used" - BSEInternal -> "internal" - BSEUnknown t -> t + STEBadRequest -> "bad_request" + STEUnsupportedVersion -> "unsupported_version" + STEUnknownPurchaseKey -> "unknown_purchase_key" + STEUnknownOfferId -> "unknown_offer_id" + STEOfferDisabled -> "offer_disabled" + STEOfferMismatch -> "offer_mismatch" + STEProductUnavailable -> "product_unavailable" + STEPaymentNotEntitled -> "payment_not_entitled" + STEPaymentPending -> "payment_pending" + STEProviderUnavailable -> "provider_unavailable" + STERateLimited -> "rate_limited" + STECodeInvalid -> "code_invalid" + STECodeUsed -> "code_used" + STECodeExpired -> "code_expired" + STEReceiptInvalid -> "receipt_invalid" + STEReceiptUsed -> "receipt_used" + STEInternal -> "internal" + STEUnknown t -> t textDecode s = Just $ case s of - "bad_request" -> BSEBadRequest - "unsupported_version" -> BSEUnsupportedVersion - "unknown_purchase_key" -> BSEUnknownPurchaseKey - "unknown_offer_id" -> BSEUnknownOfferId - "offer_disabled" -> BSEOfferDisabled - "offer_mismatch" -> BSEOfferMismatch - "product_unavailable" -> BSEProductUnavailable - "payment_not_entitled" -> BSEPaymentNotEntitled - "payment_pending" -> BSEPaymentPending - "provider_unavailable" -> BSEProviderUnavailable - "rate_limited" -> BSERateLimited - "code_invalid" -> BSECodeInvalid - "code_used" -> BSECodeUsed - "code_expired" -> BSECodeExpired - "receipt_invalid" -> BSEReceiptInvalid - "receipt_used" -> BSEReceiptUsed - "internal" -> BSEInternal - t -> BSEUnknown t + "bad_request" -> STEBadRequest + "unsupported_version" -> STEUnsupportedVersion + "unknown_purchase_key" -> STEUnknownPurchaseKey + "unknown_offer_id" -> STEUnknownOfferId + "offer_disabled" -> STEOfferDisabled + "offer_mismatch" -> STEOfferMismatch + "product_unavailable" -> STEProductUnavailable + "payment_not_entitled" -> STEPaymentNotEntitled + "payment_pending" -> STEPaymentPending + "provider_unavailable" -> STEProviderUnavailable + "rate_limited" -> STERateLimited + "code_invalid" -> STECodeInvalid + "code_used" -> STECodeUsed + "code_expired" -> STECodeExpired + "receipt_invalid" -> STEReceiptInvalid + "receipt_used" -> STEReceiptUsed + "internal" -> STEInternal + t -> STEUnknown t -instance ToJSON BadgeServiceErrorCode where +instance ToJSON StoreServiceErrorCode where toJSON = textToJSON toEncoding = textToEncoding -instance FromJSON BadgeServiceErrorCode where - parseJSON = textParseJSON "BadgeServiceErrorCode" +instance FromJSON StoreServiceErrorCode where + parseJSON = textParseJSON "StoreServiceErrorCode" instance StrEncoding BadgeIssueFailure where strEncode = \case @@ -388,7 +388,7 @@ instance StrEncoding BadgeIssueFailure where "retry" -> pure True "final" -> pure False _ -> fail "bad BadgeIssueFailure retry flag" - codeP = A.takeByteString >>= maybe (fail "bad BadgeServiceErrorCode") pure . textDecode . safeDecodeUtf8 + codeP = A.takeByteString >>= maybe (fail "bad StoreServiceErrorCode") pure . textDecode . safeDecodeUtf8 restP = safeDecodeUtf8 <$> (A.space *> A.takeByteString) instance ToField BadgeIssueFailure where toField = toField . decodeLatin1 . strEncode diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index c984748142..da06813fcb 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -85,7 +85,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Chat.Badges (BadgeCredential, FileSizeLimits, LocalBadge) -import Simplex.Chat.Badges.Service (BadgeServiceErrorCode, StatementEntry) +import Simplex.Chat.StoreService (StatementEntry, StoreServiceErrorCode) import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeAlertKind, BadgeState (..)) import Simplex.Messaging.Crypto.BBS (BBSPublicKey) import Simplex.Messaging.Crypto.File (CryptoFile (..)) @@ -148,8 +148,8 @@ data ChatConfig = ChatConfig callVRange :: VersionRangeCall, -- issuer public keys by index: credentials and proofs name the key that signed them, for rotation badgePublicKeys :: Map Int BBSPublicKey, - -- Nothing until the badge service is deployed - badgeServiceAddress :: Maybe (ConnectTarget 'CMContact), + -- Nothing until the store service is deployed + storeServiceAddress :: Maybe (ConnectTarget 'CMContact), -- the only clock badge code reads, so tests can shift it; production arithmetic is unchanged badgeCurrentTime :: IO UTCTime, -- how long a badge worker waits before repeating a renewal that failed for a passing reason @@ -667,7 +667,7 @@ data ChatCommand | UpdateProfileImage (Maybe ImageData) -- UserId (not used in UI) | UpdateProfileImageFromFile FilePath -- set profile image from a .png/.jpg/.jpeg file | AddBadge BadgeCredential -- attach an issued badge credential (testing; credential from `simplex-chat badge sign`) - | APIRedeemBadgeCode {userId :: UserId, code :: Text} -- redeem a badge code with the configured badge service + | APIRedeemBadgeCode {userId :: UserId, code :: Text} -- redeem a badge code with the configured store service | APIGetBadgeState {userId :: UserId} -- the user's badges, their balances and any current alert | APIGetBadgeLedger {userId :: UserId, badgePurchaseId :: Int64} -- the purchase's ledger, oldest first -- episode is last because it is free text: it is the value that makes one occurrence of an @@ -1525,7 +1525,7 @@ data BadgeRedeemError = BREInvalidCode -- format or check character | BREServiceNotConfigured | BREBadgeActive - | BREServiceError {serviceError :: BadgeServiceErrorCode} + | BREServiceError {serviceError :: StoreServiceErrorCode} | BREInvalidResponse {message :: String} | BREUnknownKeyIndex | BRECredentialNotVerified diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index a56000d80d..bd50795477 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -63,7 +63,7 @@ import Simplex.Chat.Badges (BadgeCredential (..), BadgeInfo (..), BadgeMasterKey import qualified Simplex.Chat.Badges.Ledger as L import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeAlertKind (..), BadgeIssueError (..), BadgeIssueFailure (..), BadgeState (..)) import Simplex.Chat.Badges.Code (badgeCodeText, parseBadgeCode) -import Simplex.Chat.Badges.Service (BadgeBalance (..), BadgeServiceCommand (..), BadgeServiceErrorCode (..), BadgeServiceRequest (..), BadgeServiceResponse (..), BadgeStatement (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..), currentBadgeServiceVersion) +import Simplex.Chat.StoreService (BadgeBalance (..), BadgeStatement (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..), StoreServiceCommand (..), StoreServiceErrorCode (..), StoreServiceRequest (..), StoreServiceResponse (..), currentStoreServiceVersion) import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim) import Simplex.Chat.Wallet (deriveAccount, masterMnemonic, newEntropy) import Simplex.Chat.Call @@ -5432,7 +5432,7 @@ presentUserBadgeToContacts user'@User {userId, profile = LocalProfile {localBadg redeemBadgeCode :: NetworkRequestMode -> User -> Text -> CM ChatResponse redeemBadgeCode nm user@User {userId} codeText = do code <- maybe (throwRedeemError BREInvalidCode) pure $ parseBadgeCode codeText - sendTarget <- asks (badgeServiceAddress . config) >>= maybe (throwRedeemError BREServiceNotConfigured) pure + sendTarget <- asks (storeServiceAddress . config) >>= maybe (throwRedeemError BREServiceNotConfigured) pure g <- asks random now <- liftIO getCurrentTime let codeSent = badgeCodeText code @@ -5446,15 +5446,15 @@ redeemBadgeCode nm user@User {userId} codeText = do unless replaying $ whenM (withStore' (`userHasBadge` user)) $ throwRedeemError BREBadgeActive redemption@BadgeCodeRedemption {purchaseKey, purchasePrivKey, masterKey} <- maybe (withStore' $ \db -> createBadgeCodeRedemption db g user codeSent now) pure redemption_ - let req = BadgeServiceRequest {version = currentBadgeServiceVersion, purchaseKey = Just purchaseKey, request = BSCRedeemBadgeCode {masterKey, code = codeSent}} + let req = StoreServiceRequest {version = currentStoreServiceVersion, purchaseKey = Just purchaseKey, request = STCRedeemBadgeCode {masterKey, code = codeSent}} respBytes <- sendServiceRequestBytes nm user sendTarget Nothing (Just purchasePrivKey) req respData <- either (const $ throwRedeemError $ BREInvalidResponse "not JSON") pure $ J.eitherDecodeStrict' respBytes case J.fromJSON (J.Object respData) of - J.Error _ -> throwRedeemError $ BREInvalidResponse "not a badge service response" - J.Success BSPError {code = errCode} -> do + J.Error _ -> throwRedeemError $ BREInvalidResponse "not a store service response" + J.Success STPError {code = errCode} -> do when (terminalCodeError errCode) $ withStore' $ \db -> deleteBadgeCodeRedemption db (redemptionId redemption) throwRedeemError $ BREServiceError errCode - J.Success BSPBadgeCredential {credential = Just cred, statement} -> storeRedeemedBadge user redemption cred statement + J.Success STPBadgeCredential {credential = Just cred, statement} -> storeRedeemedBadge user redemption cred statement J.Success _ -> throwRedeemError $ BREInvalidResponse "unexpected response type" -- outside the badge lock: the chat lock must not be taken under it mapM_ presentUserBadgeToContacts present_ @@ -5462,23 +5462,23 @@ redeemBadgeCode nm user@User {userId} codeText = do where -- the code will never work, so the keys stashed for it are dead; a timeout keeps them terminalCodeError = \case - BSECodeInvalid -> True - BSECodeUsed -> True - BSECodeExpired -> True + STECodeInvalid -> True + STECodeUsed -> True + STECodeExpired -> True _ -> False throwRedeemError :: BadgeRedeemError -> CM a throwRedeemError = throwChatError . CEBadgeRedeemError -badgeServiceErrorText :: BadgeServiceErrorCode -> Text -badgeServiceErrorText = textEncode . boundedServiceErrorCode +storeServiceErrorText :: StoreServiceErrorCode -> Text +storeServiceErrorText = textEncode . boundedServiceErrorCode -- | An unknown code is reported and recorded, since the service is deployed ahead of clients, but -- its text is the service's - so it is bounded and stripped before it reaches a terminal that acts -- on controls, or a sentence the app shows the user as its own. -boundedServiceErrorCode :: BadgeServiceErrorCode -> BadgeServiceErrorCode +boundedServiceErrorCode :: StoreServiceErrorCode -> StoreServiceErrorCode boundedServiceErrorCode = \case - BSEUnknown t -> BSEUnknown $ case T.filter errorCodeChar (T.take 32 t) of + STEUnknown t -> STEUnknown $ case T.filter errorCodeChar (T.take 32 t) of "" -> "unknown" t' -> t' code -> code @@ -5492,7 +5492,7 @@ badgeNow = asks (badgeCurrentTime . config) >>= liftIO -- | A signal carries nothing: each pass derives its work from stored state, so a signal lost or -- duplicated changes no outcome. startBadgeWork :: User -> CM' () -startBadgeWork user = whenM (isJust <$> asks (badgeServiceAddress . config)) $ void $ getBadgeWorker user +startBadgeWork user = whenM (isJust <$> asks (storeServiceAddress . config)) $ void $ getBadgeWorker user -- | Exactly one caller starts the thread and the rest wait for it: the lookup and the create cannot -- be one transaction, because starting a thread is not STM. @@ -5751,7 +5751,7 @@ badgeIssueFailure e = case e of badgeFailureTransient :: BadgeIssueFailure -> Bool badgeFailureTransient = \case -- the service withholds retryAfter from internal to avoid being pressed while failing, not because it is final - BIFServiceError {code = BSEInternal} -> True + BIFServiceError {code = STEInternal} -> True BIFServiceError {retryable} -> retryable BIFServiceTimeout -> True BIFNetwork {} -> True @@ -5770,21 +5770,21 @@ badgeErrorRetry = badgeFailureTransient . badgeIssueFailure -- nothing else records one. requestBadgeIssue :: UserId -> UserBadgePurchase -> UTCTime -> CM (Either UTCTime StatementEntry) requestBadgeIssue userId UserBadgePurchase {badgePurchaseId, badgeType, purchaseKey, purchasePrivKey, masterKey} now = do - sendTarget <- asks (badgeServiceAddress . config) >>= maybe (throwCmdError "badge service not configured") pure + sendTarget <- asks (storeServiceAddress . config) >>= maybe (throwCmdError "store service not configured") pure withEntityLock "badgeIssue" (CLBadgeUser userId) $ do user <- withStore $ \db -> getUser db userId lastEntry <- withStore' (`getBadgeLedgerLastEntry` badgePurchaseId) >>= maybe (throwCmdError "badge ledger has no entry to assert") pure let req = - BadgeServiceRequest - { version = currentBadgeServiceVersion, + StoreServiceRequest + { version = currentStoreServiceVersion, purchaseKey = Just purchaseKey, - request = BSCIssueBadge {balance = BadgeBalance {lastEntry}} + request = STCIssueBadge {balance = BadgeBalance {lastEntry}} } respData <- sendServiceRequestTo NRMBackground user sendTarget Nothing (Just purchasePrivKey) req `catchAllErrors` \e -> recordFailure (badgeIssueFailure e) >> throwError e case J.fromJSON (J.Object respData) of - J.Success BSPBadgeCredential {credential = sentCred_, statement} -> do + J.Success STPBadgeCredential {credential = sentCred_, statement} -> do verifiedCred_ <- verifyIssuedCredential masterKey sentCred_ g <- asks random -- read again: now was taken before a lock wait and an untimed request, and the check reads @@ -5798,11 +5798,11 @@ requestBadgeIssue userId UserBadgePurchase {badgePurchaseId, badgeType, purchase (Just _, Nothing, _) -> recordFailure BIFInvalidCredential (_, _, False) -> recordUnexpected "issued badge credential has no ledger row to store it against" -- no credential is the service saying the months ran out, which its statement then shows - (Nothing, _, _) | maybe False ((> 0) . balanceMonths) balance_ -> recordUnexpected "badge service issued no credential" + (Nothing, _, _) | maybe False ((> 0) . balanceMonths) balance_ -> recordUnexpected "store service issued no credential" _ -> pure () maybe (throwCmdError "badge ledger has no balance") (pure . Right) balance_ - J.Success BSPError {code, retryAfter} -> do - eToView $ ChatError $ CECommandError $ "badge service error: " <> T.unpack (badgeServiceErrorText code) + J.Success STPError {code, retryAfter} -> do + eToView $ ChatError $ CECommandError $ "store service error: " <> T.unpack (storeServiceErrorText code) recordFailure BIFServiceError {code = boundedServiceErrorCode code, retryable = isJust retryAfter} ri <- asks $ badgeRetryInterval . config pure $ Left $ badgeRetryAfter ri retryAfter `addUTCTime` now @@ -5810,7 +5810,7 @@ requestBadgeIssue userId UserBadgePurchase {badgePurchaseId, badgeType, purchase recordFailure BIFUnexpected {message = unexpectedResponse} throwCmdError $ T.unpack unexpectedResponse where - unexpectedResponse = "unexpected badge service response" + unexpectedResponse = "unexpected store service response" recordUnexpected message = do eToView $ ChatError $ CEInternalError $ T.unpack message recordFailure BIFUnexpected {message} diff --git a/src/Simplex/Chat/Store/Badges.hs b/src/Simplex/Chat/Store/Badges.hs index 19d6d2efef..851ebe71c6 100644 --- a/src/Simplex/Chat/Store/Badges.hs +++ b/src/Simplex/Chat/Store/Badges.hs @@ -40,7 +40,7 @@ import Data.Text (Text) import Data.Time.Clock (UTCTime) import Simplex.Chat.Badges import Simplex.Chat.Badges.Ledger -import Simplex.Chat.Badges.Service (StatementCreditType (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..)) +import Simplex.Chat.StoreService (StatementCreditType (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..)) import Simplex.Chat.Badges.Types (BadgeAlertKind, BadgeIssueError (..), BadgeIssueFailure, BadgePurchaseStatus (..)) import Simplex.Chat.Store.Shared (insertedRowId) import Simplex.Chat.Types diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260915_user_badges.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260915_user_badges.hs index 98bdd53db7..25058fc403 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/M20260915_user_badges.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260915_user_badges.hs @@ -7,7 +7,7 @@ import Data.Text (Text) import qualified Data.Text as T import Text.RawString.QQ (r) --- the same tables in the client and in the badge service; '@' is replaced with the table name prefix +-- the same tables in the client and in the store service; '@' is replaced with the table name prefix badgeSchema :: Text -> Text badgeSchema pfx = withPrefix pfx badgeSchemaTables diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260915_user_badges.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260915_user_badges.hs index 3462db9bc1..0a5b722dd8 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260915_user_badges.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260915_user_badges.hs @@ -8,7 +8,7 @@ import qualified Data.Text as T import Database.SQLite.Simple (Query (..)) import Database.SQLite.Simple.QQ (sql) --- the same tables in the client and in the badge service; '@' is replaced with the table name prefix +-- the same tables in the client and in the store service; '@' is replaced with the table name prefix badgeSchema :: Text -> Query badgeSchema pfx = withPrefix pfx badgeSchemaTables diff --git a/src/Simplex/Chat/Badges/Service.hs b/src/Simplex/Chat/StoreService.hs similarity index 79% rename from src/Simplex/Chat/Badges/Service.hs rename to src/Simplex/Chat/StoreService.hs index 725009d839..8e40d9fb2d 100644 --- a/src/Simplex/Chat/Badges/Service.hs +++ b/src/Simplex/Chat/StoreService.hs @@ -5,19 +5,19 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TemplateHaskell #-} -module Simplex.Chat.Badges.Service - ( BadgeServiceRequest (..), - BadgeServiceCommand (..), - BadgeServiceVersion, - VersionBadgeService, - VersionRangeBadgeService, - pattern VersionBadgeService, - initialBadgeServiceVersion, - currentBadgeServiceVersion, - supportedBadgeServiceVRange, +module Simplex.Chat.StoreService + ( StoreServiceRequest (..), + StoreServiceCommand (..), + StoreServiceVersion, + VersionStoreService, + VersionRangeStoreService, + pattern VersionStoreService, + initialStoreServiceVersion, + currentStoreServiceVersion, + supportedStoreServiceVRange, BadgeUpgrade (..), - BadgeServiceResponse (..), - BadgeServiceErrorCode (..), + StoreServiceResponse (..), + StoreServiceErrorCode (..), BadgeCatalog (..), BadgePrice (..), BadgeOffer (..), @@ -46,60 +46,60 @@ import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON) import Simplex.Messaging.Version (VersionRange, VersionScope, mkVersionRange) import Simplex.Messaging.Version.Internal (Version (..)) -data BadgeServiceVersion +data StoreServiceVersion -instance VersionScope BadgeServiceVersion +instance VersionScope StoreServiceVersion -type VersionBadgeService = Version BadgeServiceVersion +type VersionStoreService = Version StoreServiceVersion -pattern VersionBadgeService :: Word16 -> VersionBadgeService -pattern VersionBadgeService v = Version v +pattern VersionStoreService :: Word16 -> VersionStoreService +pattern VersionStoreService v = Version v -type VersionRangeBadgeService = VersionRange BadgeServiceVersion +type VersionRangeStoreService = VersionRange StoreServiceVersion -initialBadgeServiceVersion :: VersionBadgeService -initialBadgeServiceVersion = VersionBadgeService 1 +initialStoreServiceVersion :: VersionStoreService +initialStoreServiceVersion = VersionStoreService 1 -currentBadgeServiceVersion :: VersionBadgeService -currentBadgeServiceVersion = VersionBadgeService 1 +currentStoreServiceVersion :: VersionStoreService +currentStoreServiceVersion = VersionStoreService 1 -- the service is deployed ahead of app releases, so it answers within the client's version -supportedBadgeServiceVRange :: VersionRangeBadgeService -supportedBadgeServiceVRange = mkVersionRange initialBadgeServiceVersion currentBadgeServiceVersion +supportedStoreServiceVRange :: VersionRangeStoreService +supportedStoreServiceVRange = mkVersionRange initialStoreServiceVersion currentStoreServiceVersion -data BadgeServiceRequest = BadgeServiceRequest - { version :: VersionBadgeService, - purchaseKey :: Maybe C.PublicKeyEd25519, -- optional for BSCGetBadgeCatalog, required for other commands - request :: BadgeServiceCommand +data StoreServiceRequest = StoreServiceRequest + { version :: VersionStoreService, + purchaseKey :: Maybe C.PublicKeyEd25519, -- optional for STCGetBadgeCatalog, required for other commands + request :: StoreServiceCommand } -data BadgeServiceCommand - = BSCGetBadgeCatalog - | BSCGetBadgeInvoice +data StoreServiceCommand + = STCGetBadgeCatalog + | STCGetBadgeInvoice { priceId :: BadgePriceId, offerId :: Maybe BadgeOfferId, -- absent for 1 month at the badge price badgeInfo :: BadgeInfo, paymentVia :: ServicePaymentMethod, upgrade :: Maybe BadgeUpgrade -- upgrade non-store badge } - | BSCRedeemBadgeCode + | STCRedeemBadgeCode { masterKey :: BadgeMasterKey, code :: Text -- no badgeRequest: a code carries no tier for the client to state } - | BSCPurchaseBadge + | STCPurchaseBadge { badgeRequest :: BadgeRequest, payment :: ServicePayment, upgrade :: Maybe BadgeUpgrade -- upgrade store 1-time badge } - | BSCUpgradeBadgeSubscription + | STCUpgradeBadgeSubscription { badgeRequest :: BadgeRequest, payment :: ServicePayment, -- store payments only balance :: BadgeBalance } - | BSCIssueBadge + | STCIssueBadge { balance :: BadgeBalance -- no badgeRequest: the service holds the key, the tier and the expiry } - | BSCPauseBadge + | STCPauseBadge data BadgeUpgrade = BadgeUpgrade { fromPurchaseKey :: C.PublicKeyEd25519, @@ -108,23 +108,23 @@ data BadgeUpgrade = BadgeUpgrade balance :: BadgeBalance } -data BadgeServiceResponse - = BSPBadgeCatalog +data StoreServiceResponse + = STPBadgeCatalog { catalog :: BadgeCatalog, badgeStatement :: Maybe BadgeStatement -- for signed getBadgeCatalog } - | BSPBadgeInvoice + | STPBadgeInvoice { invoice :: ServiceInvoice, badgeType :: BadgeType, months :: Word8 } - | BSPBadgeCredential + | STPBadgeCredential { credential :: Maybe BadgeCredential, -- Nothing when no balance to issueBadge or no current credential for pause receipt :: Maybe Text, -- not provided for lifetime badges statement :: BadgeStatement } - | BSPError - { code :: BadgeServiceErrorCode, + | STPError + { code :: StoreServiceErrorCode, message :: Maybe Text, retryAfter :: Maybe Word32 } @@ -246,9 +246,9 @@ $(JQ.deriveJSON defaultJSON ''BadgeBalance) $(JQ.deriveJSON defaultJSON ''BadgeUpgrade) -$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "BSC") ''BadgeServiceCommand) +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "STC") ''StoreServiceCommand) -$(JQ.deriveJSON defaultJSON ''BadgeServiceRequest) +$(JQ.deriveJSON defaultJSON ''StoreServiceRequest) $(JQ.deriveJSON defaultJSON ''BadgePrice) @@ -256,4 +256,4 @@ $(JQ.deriveJSON defaultJSON ''BadgeOffer) $(JQ.deriveJSON defaultJSON ''BadgeCatalog) -$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "BSP") ''BadgeServiceResponse) +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "STP") ''StoreServiceResponse) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 59c056bdf4..34ec32396b 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -41,11 +41,11 @@ import Numeric (showFFloat) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Help -import Simplex.Chat.Library.Commands (badgeServiceErrorText, maxImageSize) +import Simplex.Chat.Library.Commands (maxImageSize, storeServiceErrorText) import Simplex.Chat.Markdown import Simplex.Chat.Badges (BadgeInfo (..), BadgeStatus (..), BadgeType (..), LocalBadge, localBadgeInfo, localBadgeStatus) import Simplex.Chat.Badges.Ledger (creditTypeTag, debitTypeTag) -import Simplex.Chat.Badges.Service (StatementEntry (..), StatementEntryType (..)) +import Simplex.Chat.StoreService (StatementEntry (..), StatementEntryType (..)) import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeIssueError (..), BadgeState (..)) import Simplex.Chat.Messages hiding (NewChatItem (..)) import Simplex.Chat.Messages.CIContent @@ -2921,9 +2921,9 @@ viewChatError isCmd logLevel testView = \case CEBadgeRedeemError e -> let reason = case e of BREInvalidCode -> "invalid code" - BREServiceNotConfigured -> "badge service not configured" + BREServiceNotConfigured -> "store service not configured" BREBadgeActive -> "badge already active" - BREServiceError code -> "badge service error: " <> T.unpack (badgeServiceErrorText code) + BREServiceError code -> "store service error: " <> T.unpack (storeServiceErrorText code) BREInvalidResponse m -> "invalid service response: " <> m BREUnknownKeyIndex -> "credential names an unknown badge key index" BRECredentialNotVerified -> "credential does not verify against configured key" diff --git a/tests/BadgeTests.hs b/tests/BadgeTests.hs index 0d2e953d2c..d6ba48bac2 100644 --- a/tests/BadgeTests.hs +++ b/tests/BadgeTests.hs @@ -28,11 +28,11 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Chat.Badges import Simplex.Chat.Badges.Code import Simplex.Chat.Badges.Ledger -import Simplex.Chat.Badges.Service +import Simplex.Chat.StoreService import Simplex.Chat.Badges.Types (BadgeIssueFailure (..), BadgeOfferId (..), BadgePriceId (..)) import Simplex.Chat (defaultChatConfig) import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..), badgeRetryInterval, chatErrorAgent) -import Simplex.Chat.Library.Commands (badgeErrorRetry, badgeFailureTransient, badgeIssueFailure, badgeRetryAfter, badgeServiceErrorText, badgeStalledInterval) +import Simplex.Chat.Library.Commands (badgeErrorRetry, badgeFailureTransient, badgeIssueFailure, badgeRetryAfter, badgeStalledInterval, storeServiceErrorText) import Simplex.Chat.PaymentService import Simplex.Messaging.Agent.Protocol (AgentErrorType (..), AgentServiceError (..), SMPAgentError (..)) import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay) @@ -704,7 +704,7 @@ testRetryClassification = do -- terminal: the same request would fail the same way, and repeating it would spin retryFor (AGENT (A_SERVICE ASEBadSignature)) `shouldBe` False retryFor (AGENT (A_SERVICE (ASERejected "no"))) `shouldBe` False - badgeErrorRetry (ChatError (CECommandError "unexpected badge service response")) `shouldBe` False + badgeErrorRetry (ChatError (CECommandError "unexpected store service response")) `shouldBe` False -- A failure that never clears is repeated for as long as the balance funds a month, so the wait -- has to grow: at a fixed interval an annual code would ask hundreds of times a day, all year. @@ -743,15 +743,15 @@ testServiceRetryFloor = do -- schedule, so the set is the protocol's and not a judgement to make per call site. testServiceRetryAfter :: IO () testServiceRetryAfter = do - badgeErrorRetryAfter BSEPaymentPending `shouldBe` Just 300 - badgeErrorRetryAfter BSEProviderUnavailable `shouldBe` Just 300 - badgeErrorRetryAfter BSERateLimited `shouldBe` Just 60 + badgeErrorRetryAfter STEPaymentPending `shouldBe` Just 300 + badgeErrorRetryAfter STEProviderUnavailable `shouldBe` Just 300 + badgeErrorRetryAfter STERateLimited `shouldBe` Just 60 -- internal is the one most likely to clear on its own, and is still terminal: repeating it on -- the service's cadence presses a service already failing, and the client has its own floor - badgeErrorRetryAfter BSEInternal `shouldBe` Nothing + badgeErrorRetryAfter STEInternal `shouldBe` Nothing mapM_ (\code -> badgeErrorRetryAfter code `shouldBe` Nothing) - [BSEBadRequest, BSEUnsupportedVersion, BSEUnknownPurchaseKey, BSECodeInvalid, BSECodeUsed, BSECodeExpired, BSEUnknown "future_code"] + [STEBadRequest, STEUnsupportedVersion, STEUnknownPurchaseKey, STECodeInvalid, STECodeUsed, STECodeExpired, STEUnknown "future_code"] -- The app shows the recorded failure in a sentence, so an agent error is stored as the agent -- error and not as the chat error wrapping it, whether or not it can clear on its own. @@ -763,18 +763,18 @@ testIssueFailureClassification = do failureFor (AGENT (A_SERVICE ASETimeout)) `shouldBe` BIFServiceTimeout failureFor timeout `shouldBe` BIFNetwork {agentError = tshow timeout} failureFor auth `shouldBe` BIFUnexpected {message = tshow auth} - badgeIssueFailure (ChatError (CECommandError "unexpected badge service response")) `shouldBe` BIFUnexpected {message = "unexpected badge service response"} + badgeIssueFailure (ChatError (CECommandError "unexpected store service response")) `shouldBe` BIFUnexpected {message = "unexpected store service response"} -- A refusal the service marks transient is not worth a word before the credential lapses. Internal -- comes without retryAfter so that a failing service is not pressed, not because the fault is -- final, so it waits the same way; any other refusal is told at once. testServiceErrorTransience :: IO () testServiceErrorTransience = do - badgeFailureTransient BIFServiceError {code = BSERateLimited, retryable = True} `shouldBe` True - badgeFailureTransient BIFServiceError {code = BSEInternal, retryable = False} `shouldBe` True - badgeFailureTransient BIFServiceError {code = BSEUnknownPurchaseKey, retryable = False} `shouldBe` False - badgeFailureTransient BIFServiceError {code = BSEUnknown "future_code", retryable = False} `shouldBe` False - badgeFailureTransient BIFServiceError {code = BSEUnknown "future_code", retryable = True} `shouldBe` True + badgeFailureTransient BIFServiceError {code = STERateLimited, retryable = True} `shouldBe` True + badgeFailureTransient BIFServiceError {code = STEInternal, retryable = False} `shouldBe` True + badgeFailureTransient BIFServiceError {code = STEUnknownPurchaseKey, retryable = False} `shouldBe` False + badgeFailureTransient BIFServiceError {code = STEUnknown "future_code", retryable = False} `shouldBe` False + badgeFailureTransient BIFServiceError {code = STEUnknown "future_code", retryable = True} `shouldBe` True -- The failure is stored as text and read back by getBadgePurchase, so every value this version -- writes must read back as itself, and any other value must still read: a row it cannot parse @@ -783,20 +783,20 @@ testIssueFailureEncoding :: IO () testIssueFailureEncoding = do mapM_ (\f -> strDecode (strEncode f) `shouldBe` Right f) - [ BIFServiceError {code = BSEUnknownPurchaseKey, retryable = False}, - BIFServiceError {code = BSERateLimited, retryable = True}, - BIFServiceError {code = BSEUnknown "future_code", retryable = True}, - BIFServiceError {code = BSEUnknown "code with spaces", retryable = False}, - BIFServiceError {code = BSEUnknown "", retryable = False}, + [ BIFServiceError {code = STEUnknownPurchaseKey, retryable = False}, + BIFServiceError {code = STERateLimited, retryable = True}, + BIFServiceError {code = STEUnknown "future_code", retryable = True}, + BIFServiceError {code = STEUnknown "code with spaces", retryable = False}, + BIFServiceError {code = STEUnknown "", retryable = False}, BIFServiceTimeout, BIFNetwork {agentError = "BROKER \"smp://x\" NETWORK"}, BIFInvalidCredential, - BIFUnexpected {message = "unexpected badge service response"}, + BIFUnexpected {message = "unexpected store service response"}, BIFUnexpected {message = "several words and : punctuation"} ] -- the row the CLI prints, and what a reader of the database sees - strEncode BIFServiceError {code = BSECodeUsed, retryable = False} `shouldBe` "service_error final code_used" - strEncode BIFServiceError {code = BSERateLimited, retryable = True} `shouldBe` "service_error retry rate_limited" + strEncode BIFServiceError {code = STECodeUsed, retryable = False} `shouldBe` "service_error final code_used" + strEncode BIFServiceError {code = STERateLimited, retryable = True} `shouldBe` "service_error retry rate_limited" strEncode BIFServiceTimeout `shouldBe` "service_timeout" -- a row a later version wrote, or one edited by hand, still reads - as the text it holds strDecode "future_failure with text" `shouldBe` Right BIFUnexpected {message = "future_failure with text"} @@ -807,11 +807,11 @@ testIssueFailureEncoding = do -- a sentence of its own - so it is bounded and stripped of anything but a code before it is stored. testServiceErrorCodeBounded :: IO () testServiceErrorCodeBounded = do - badgeServiceErrorText BSECodeUsed `shouldBe` "code_used" - badgeServiceErrorText (BSEUnknown "future_code") `shouldBe` "future_code" - badgeServiceErrorText (BSEUnknown "two words") `shouldBe` "twowords" - badgeServiceErrorText (BSEUnknown (T.replicate 100 "a")) `shouldBe` T.replicate 32 "a" - badgeServiceErrorText (BSEUnknown "Visit evil.example.com!") `shouldBe` "isitevilexamplecom" + storeServiceErrorText STECodeUsed `shouldBe` "code_used" + storeServiceErrorText (STEUnknown "future_code") `shouldBe` "future_code" + storeServiceErrorText (STEUnknown "two words") `shouldBe` "twowords" + storeServiceErrorText (STEUnknown (T.replicate 100 "a")) `shouldBe` T.replicate 32 "a" + storeServiceErrorText (STEUnknown "Visit evil.example.com!") `shouldBe` "isitevilexamplecom" -- The client replicates entry_credit_type / entry_debit_type verbatim, so a stored tag that -- disagreed with the wire tag would put a different row on each side. @@ -847,7 +847,7 @@ testRedeemRequestJSON = do mk <- generateMasterKey drg (k, _) <- atomically $ C.generateKeyPair drg :: IO (C.KeyPair 'C.Ed25519) code <- randomBadgeCode drg - let req = BadgeServiceRequest {version = Version 1, purchaseKey = Just k, request = BSCRedeemBadgeCode {masterKey = mk, code = badgeCodeText code}} + let req = StoreServiceRequest {version = Version 1, purchaseKey = Just k, request = STCRedeemBadgeCode {masterKey = mk, code = badgeCodeText code}} J.toJSON req `shouldBe` J.object [ "version" J..= (1 :: Int), @@ -855,7 +855,7 @@ testRedeemRequestJSON = do "request" J..= J.object ["type" J..= ("redeemBadgeCode" :: T.Text), "masterKey" J..= mk, "code" J..= badgeCodeText code] ] -- purchaseKey is optional in the schema, and a nullary command is a bare tagged object - J.toJSON BadgeServiceRequest {version = Version 1, purchaseKey = Nothing, request = BSCGetBadgeCatalog} + J.toJSON StoreServiceRequest {version = Version 1, purchaseKey = Nothing, request = STCGetBadgeCatalog} `shouldBe` J.object ["version" J..= (1 :: Int), "request" J..= J.object ["type" J..= ("getBadgeCatalog" :: T.Text)]] roundTrips req @@ -866,7 +866,7 @@ testCredentialResponseJSON = do mk <- generateMasterKey drg let info = BadgeInfo {badgeType = BTSupporter, badgeExpiry = futureTime, badgeExtra = ""} Right cred <- issueBadge testKeyIdx sk (VerifiedBadgeRequest BadgeRequest {masterKey = mk, badgeInfo = info}) - let resp = BSPBadgeCredential {credential = Just cred, receipt = Nothing, statement = BadgeStatement {entries = [], previousEntryId = Nothing}} + let resp = STPBadgeCredential {credential = Just cred, receipt = Nothing, statement = BadgeStatement {entries = [], previousEntryId = Nothing}} J.toJSON resp `shouldBe` J.object [ "type" J..= ("badgeCredential" :: T.Text), @@ -877,9 +877,9 @@ testCredentialResponseJSON = do testErrorResponseJSON :: IO () testErrorResponseJSON = do - let resp = BSPError {code = BSECodeInvalid, message = Nothing, retryAfter = Nothing} + let resp = STPError {code = STECodeInvalid, message = Nothing, retryAfter = Nothing} J.toJSON resp `shouldBe` J.object ["type" J..= ("error" :: T.Text), "code" J..= ("code_invalid" :: T.Text)] - J.toJSON BSPError {code = BSERateLimited, message = Just "slow down", retryAfter = Just 30} + J.toJSON STPError {code = STERateLimited, message = Just "slow down", retryAfter = Just 30} `shouldBe` J.object ["type" J..= ("error" :: T.Text), "code" J..= ("rate_limited" :: T.Text), "message" J..= ("slow down" :: T.Text), "retryAfter" J..= (30 :: Int)] testEntry :: StatementEntry @@ -947,39 +947,39 @@ testServiceWireTags = do paymentTo = SPDCard {provider = CPStripe, url = "https://checkout.example/inv-1"} } commands = - [ BSCGetBadgeCatalog, - BSCGetBadgeInvoice {priceId = BadgePriceId "price-1", offerId = Just (BadgeOfferId "offer-1"), badgeInfo, paymentVia = SPMCard {provider = CPStripe}, upgrade = Nothing}, - BSCRedeemBadgeCode {masterKey, code = "SB-4K2P7-TQ9M1-ZX3RB-8HJ5W"}, - BSCPurchaseBadge {badgeRequest, payment, upgrade = Nothing}, - BSCUpgradeBadgeSubscription {badgeRequest, payment, balance}, - BSCIssueBadge {balance}, - BSCPauseBadge + [ STCGetBadgeCatalog, + STCGetBadgeInvoice {priceId = BadgePriceId "price-1", offerId = Just (BadgeOfferId "offer-1"), badgeInfo, paymentVia = SPMCard {provider = CPStripe}, upgrade = Nothing}, + STCRedeemBadgeCode {masterKey, code = "SB-4K2P7-TQ9M1-ZX3RB-8HJ5W"}, + STCPurchaseBadge {badgeRequest, payment, upgrade = Nothing}, + STCUpgradeBadgeSubscription {badgeRequest, payment, balance}, + STCIssueBadge {balance}, + STCPauseBadge ] responses = - [ BSPBadgeCatalog {catalog = BadgeCatalog {prices = [], offers = []}, badgeStatement = Just statement}, - BSPBadgeInvoice {invoice, badgeType = BTSupporter, months = 3}, - BSPBadgeCredential {credential = Nothing, receipt = Just "receipt", statement}, - BSPError {code = BSEInternal, message = Nothing, retryAfter = Nothing} + [ STPBadgeCatalog {catalog = BadgeCatalog {prices = [], offers = []}, badgeStatement = Just statement}, + STPBadgeInvoice {invoice, badgeType = BTSupporter, months = 3}, + STPBadgeCredential {credential = Nothing, receipt = Just "receipt", statement}, + STPError {code = STEInternal, message = Nothing, retryAfter = Nothing} ] errors = - [ BSEBadRequest, - BSEUnsupportedVersion, - BSEUnknownPurchaseKey, - BSEUnknownOfferId, - BSEOfferDisabled, - BSEOfferMismatch, - BSEProductUnavailable, - BSEPaymentNotEntitled, - BSEPaymentPending, - BSEProviderUnavailable, - BSERateLimited, - BSECodeInvalid, - BSECodeUsed, - BSECodeExpired, - BSEReceiptInvalid, - BSEReceiptUsed, - BSEInternal, - BSEUnknown "future_code" + [ STEBadRequest, + STEUnsupportedVersion, + STEUnknownPurchaseKey, + STEUnknownOfferId, + STEOfferDisabled, + STEOfferMismatch, + STEProductUnavailable, + STEPaymentNotEntitled, + STEPaymentPending, + STEProviderUnavailable, + STERateLimited, + STECodeInvalid, + STECodeUsed, + STECodeExpired, + STEReceiptInvalid, + STEReceiptUsed, + STEInternal, + STEUnknown "future_code" ] forM_ commands $ pinsTypeTag commandTag forM_ responses $ pinsTypeTag responseTag @@ -988,37 +988,37 @@ testServiceWireTags = do J.fromJSON (J.String $ errorTag e) `shouldBe` J.Success e where commandTag = \case - BSCGetBadgeCatalog -> "getBadgeCatalog" - BSCGetBadgeInvoice {} -> "getBadgeInvoice" - BSCRedeemBadgeCode {} -> "redeemBadgeCode" - BSCPurchaseBadge {} -> "purchaseBadge" - BSCUpgradeBadgeSubscription {} -> "upgradeBadgeSubscription" - BSCIssueBadge {} -> "issueBadge" - BSCPauseBadge -> "pauseBadge" + STCGetBadgeCatalog -> "getBadgeCatalog" + STCGetBadgeInvoice {} -> "getBadgeInvoice" + STCRedeemBadgeCode {} -> "redeemBadgeCode" + STCPurchaseBadge {} -> "purchaseBadge" + STCUpgradeBadgeSubscription {} -> "upgradeBadgeSubscription" + STCIssueBadge {} -> "issueBadge" + STCPauseBadge -> "pauseBadge" responseTag = \case - BSPBadgeCatalog {} -> "badgeCatalog" - BSPBadgeInvoice {} -> "badgeInvoice" - BSPBadgeCredential {} -> "badgeCredential" - BSPError {} -> "error" + STPBadgeCatalog {} -> "badgeCatalog" + STPBadgeInvoice {} -> "badgeInvoice" + STPBadgeCredential {} -> "badgeCredential" + STPError {} -> "error" errorTag = \case - BSEBadRequest -> "bad_request" - BSEUnsupportedVersion -> "unsupported_version" - BSEUnknownPurchaseKey -> "unknown_purchase_key" - BSEUnknownOfferId -> "unknown_offer_id" - BSEOfferDisabled -> "offer_disabled" - BSEOfferMismatch -> "offer_mismatch" - BSEProductUnavailable -> "product_unavailable" - BSEPaymentNotEntitled -> "payment_not_entitled" - BSEPaymentPending -> "payment_pending" - BSEProviderUnavailable -> "provider_unavailable" - BSERateLimited -> "rate_limited" - BSECodeInvalid -> "code_invalid" - BSECodeUsed -> "code_used" - BSECodeExpired -> "code_expired" - BSEReceiptInvalid -> "receipt_invalid" - BSEReceiptUsed -> "receipt_used" - BSEInternal -> "internal" - BSEUnknown t -> t + STEBadRequest -> "bad_request" + STEUnsupportedVersion -> "unsupported_version" + STEUnknownPurchaseKey -> "unknown_purchase_key" + STEUnknownOfferId -> "unknown_offer_id" + STEOfferDisabled -> "offer_disabled" + STEOfferMismatch -> "offer_mismatch" + STEProductUnavailable -> "product_unavailable" + STEPaymentNotEntitled -> "payment_not_entitled" + STEPaymentPending -> "payment_pending" + STEProviderUnavailable -> "provider_unavailable" + STERateLimited -> "rate_limited" + STECodeInvalid -> "code_invalid" + STECodeUsed -> "code_used" + STECodeExpired -> "code_expired" + STEReceiptInvalid -> "receipt_invalid" + STEReceiptUsed -> "receipt_used" + STEInternal -> "internal" + STEUnknown t -> t -- the tag is checked in both encodings, and decoding must land on the constructor the tag names pinsTypeTag :: (HasCallStack, J.ToJSON a, J.FromJSON a) => (a -> T.Text) -> a -> IO () diff --git a/tests/Bots/BadgeService/BotTests.hs b/tests/Bots/BadgeService/BotTests.hs index 129824fed8..4b0c4fc33a 100644 --- a/tests/Bots/BadgeService/BotTests.hs +++ b/tests/Bots/BadgeService/BotTests.hs @@ -46,7 +46,7 @@ import Data.Time.Format (defaultTimeLocale, formatTime) import Simplex.Chat.Badges (BadgeCredential (..), BadgeInfo (..), BadgeMasterKey, BadgeType (..), generateMasterKey) import Simplex.Chat.Badges.Code (BadgeCode, badgeCodeHash, badgeCodeText, formatBadgeCode, parseBadgeCode, randomBadgeCode) import Simplex.Chat.Badges.Ledger (addMonths, creditTypeTag, debitTypeTag, endOfMondayAfter) -import Simplex.Chat.Badges.Service +import Simplex.Chat.StoreService import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..)) import Simplex.Chat.Bot.Store (withDB') import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatError (..), ChatErrorType (..), ChatResponse (CRCustomChatResponse)) @@ -206,7 +206,7 @@ withBadgeServiceEnv ps test = do bs <## "auto_accept off" pure sLink let clientCfg = - svcCfg {badgeServiceAddress = Just $ either (error . ("bad badge service address: " <>)) id $ strDecode (B.pack bsLink)} + svcCfg {storeServiceAddress = Just $ either (error . ("bad badge service address: " <>)) id $ strDecode (B.pack bsLink)} -- Second start: badge service takes the ShowMyAddress branch, then serves the test body. runBadgeService ps svcCfg opts $ \env -> do cc <- atomically $ readTMVar $ serviceCC env @@ -305,7 +305,7 @@ testRedeemUnknownCode ps = g <- C.newRandom unknown <- randomBadgeCode g alice ##> ("/_redeem_badge_code 1 " <> codeArg unknown) - alice <## "cannot redeem badge code: badge service error: code_invalid" + alice <## "cannot redeem badge code: store service error: code_invalid" -- a failed check character is refused before anything leaves the device alice ##> "/_redeem_badge_code 1 SB-00000-00000-00000-00001" alice <## "cannot redeem badge code: invalid code" @@ -383,21 +383,21 @@ testRedeemSameCodeOtherProfile ps = alice ##> "/create user alisa" showActiveUser alice "alisa" alice ##> ("/_redeem_badge_code 2 " <> codeArg code) - alice <## "cannot redeem badge code: badge service error: code_used" + alice <## "cannot redeem badge code: store service error: code_used" alice ##> "/p" showActiveUser alice "alisa" alice ##> "/user alice" showActiveUser alice "alice (Alice, * supporter)" -serviceCmd :: HasCallStack => BadgeServiceEnv -> C.PublicKeyEd25519 -> BadgeServiceCommand -> IO BadgeServiceResponse +serviceCmd :: HasCallStack => BadgeServiceEnv -> C.PublicKeyEd25519 -> StoreServiceCommand -> IO StoreServiceResponse serviceCmd BadgeServiceEnv {bsIssuerKey, bsController} = serviceCmdWith bsIssuerKey bsController Nothing -serviceCmdWith :: HasCallStack => BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> C.PublicKeyEd25519 -> BadgeServiceCommand -> IO BadgeServiceResponse +serviceCmdWith :: HasCallStack => BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> C.PublicKeyEd25519 -> StoreServiceCommand -> IO StoreServiceResponse serviceCmdWith key cc trackerQ_ purchaseKey request = badgeServiceResponse key cc trackerQ_ (Just purchaseKey) (requestObject purchaseKey request) -requestObject :: HasCallStack => C.PublicKeyEd25519 -> BadgeServiceCommand -> J.Object -requestObject purchaseKey request = case J.toJSON BadgeServiceRequest {version = currentBadgeServiceVersion, purchaseKey = Just purchaseKey, request} of +requestObject :: HasCallStack => C.PublicKeyEd25519 -> StoreServiceCommand -> J.Object +requestObject purchaseKey request = case J.toJSON StoreServiceRequest {version = currentStoreServiceVersion, purchaseKey = Just purchaseKey, request} of J.Object o -> o _ -> error "badge service request must encode as an object" @@ -412,19 +412,19 @@ entryTag StatementEntry {entryType} = case entryType of SECredit c -> creditTypeTag c SEDebit d -> debitTypeTag d -statementOf :: HasCallStack => BadgeServiceResponse -> ([StatementEntry], Maybe Text) +statementOf :: HasCallStack => StoreServiceResponse -> ([StatementEntry], Maybe Text) statementOf = \case - BSPBadgeCredential {statement = BadgeStatement {entries, previousEntryId}} -> (entries, previousEntryId) + STPBadgeCredential {statement = BadgeStatement {entries, previousEntryId}} -> (entries, previousEntryId) r -> error $ "expected badgeCredential, got " <> show (J.toJSON r) -credentialOf :: HasCallStack => BadgeServiceResponse -> Maybe BadgeCredential +credentialOf :: HasCallStack => StoreServiceResponse -> Maybe BadgeCredential credentialOf = \case - BSPBadgeCredential {credential} -> credential + STPBadgeCredential {credential} -> credential r -> error $ "expected badgeCredential, got " <> show (J.toJSON r) -shouldAnswerError :: HasCallStack => BadgeServiceResponse -> BadgeServiceErrorCode -> IO () +shouldAnswerError :: HasCallStack => StoreServiceResponse -> StoreServiceErrorCode -> IO () shouldAnswerError r expected = case r of - BSPError {code = ec} -> ec `shouldBe` expected + STPError {code = ec} -> ec `shouldBe` expected _ -> expectationFailure $ "expected " <> show expected <> ", got: " <> show (J.toJSON r) codeCounts :: DBStore -> ByteString -> IO (Maybe (Int, Int)) @@ -442,28 +442,28 @@ newPurchaseKeys = do (purchaseKey, _) <- atomically $ C.generateKeyPair g :: IO (C.KeyPair 'C.Ed25519) (purchaseKey,) <$> generateMasterKey g -redeemAsNewPurchase :: HasCallStack => BadgeServiceEnv -> BadgeCode -> IO BadgeServiceResponse +redeemAsNewPurchase :: HasCallStack => BadgeServiceEnv -> BadgeCode -> IO StoreServiceResponse redeemAsNewPurchase BadgeServiceEnv {bsIssuerKey, bsController} = redeemWithQueue bsIssuerKey bsController Nothing . badgeCodeText -redeemWithKeys :: HasCallStack => BadgeServiceEnv -> (C.PublicKeyEd25519, BadgeMasterKey) -> BadgeCode -> IO BadgeServiceResponse -redeemWithKeys env (purchaseKey, masterKey) code = serviceCmd env purchaseKey BSCRedeemBadgeCode {masterKey, code = badgeCodeText code} +redeemWithKeys :: HasCallStack => BadgeServiceEnv -> (C.PublicKeyEd25519, BadgeMasterKey) -> BadgeCode -> IO StoreServiceResponse +redeemWithKeys env (purchaseKey, masterKey) code = serviceCmd env purchaseKey STCRedeemBadgeCode {masterKey, code = badgeCodeText code} -redeemWithQueue :: HasCallStack => BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> Text -> IO BadgeServiceResponse +redeemWithQueue :: HasCallStack => BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> Text -> IO StoreServiceResponse redeemWithQueue key cc trackerQ_ codeText = do (purchaseKey, masterKey) <- newPurchaseKeys - serviceCmdWith key cc trackerQ_ purchaseKey BSCRedeemBadgeCode {masterKey, code = codeText} + serviceCmdWith key cc trackerQ_ purchaseKey STCRedeemBadgeCode {masterKey, code = codeText} -assertBalance :: HasCallStack => BadgeServiceEnv -> C.PublicKeyEd25519 -> StatementEntry -> IO BadgeServiceResponse +assertBalance :: HasCallStack => BadgeServiceEnv -> C.PublicKeyEd25519 -> StatementEntry -> IO StoreServiceResponse assertBalance env purchaseKey lastEntry = - serviceCmd env purchaseKey BSCIssueBadge {balance = BadgeBalance {lastEntry}} + serviceCmd env purchaseKey STCIssueBadge {balance = BadgeBalance {lastEntry}} -expiryOf :: HasCallStack => BadgeServiceResponse -> Maybe UTCTime +expiryOf :: HasCallStack => StoreServiceResponse -> Maybe UTCTime expiryOf r = (\(BadgeCredential _ _ _ BadgeInfo {badgeExpiry}) -> badgeExpiry) <$> credentialOf r -badgeTypeOf :: HasCallStack => BadgeServiceResponse -> Maybe BadgeType +badgeTypeOf :: HasCallStack => StoreServiceResponse -> Maybe BadgeType badgeTypeOf r = (\(BadgeCredential _ _ _ BadgeInfo {badgeType}) -> badgeType) <$> credentialOf r -masterKeyOf :: HasCallStack => BadgeServiceResponse -> Maybe BadgeMasterKey +masterKeyOf :: HasCallStack => StoreServiceResponse -> Maybe BadgeMasterKey masterKeyOf r = (\(BadgeCredential _ mk _ _) -> mk) <$> credentialOf r testCodeMonthsRenew :: HasCallStack => TestParams -> IO () @@ -525,7 +525,7 @@ testMultiUseRepeatAfterExpiry ps = waitShownBadge (chatController alice) Nothing codeUses cc code `shouldReturn` Just (2, 1) withNewTestChatCfg ps bsClientCfg "bob" bobProfile $ \bob -> redeemFirstBadge bob code - redeemAsNewPurchase env code >>= (`shouldAnswerError` BSECodeUsed) + redeemAsNewPurchase env code >>= (`shouldAnswerError` STECodeUsed) testMultiUseWithoutGroup :: HasCallStack => TestParams -> IO () testMultiUseWithoutGroup ps = @@ -540,7 +540,7 @@ testMultiUseWithoutGroup ps = expiryOf redeemed `shouldBe` Just (endOfMondayAfter (nextDue entries)) masterKeyOf redeemed `shouldBe` Just masterKey codeUses cc code `shouldReturn` Just (2, claimed) - redeemAsNewPurchase env code >>= (`shouldAnswerError` BSECodeUsed) + redeemAsNewPurchase env code >>= (`shouldAnswerError` STECodeUsed) testUnreadableCredentialIsInternal :: HasCallStack => TestParams -> IO () testUnreadableCredentialIsInternal ps = @@ -551,11 +551,11 @@ testUnreadableCredentialIsInternal ps = redeem holder >>= (`shouldSatisfy` isJust) . credentialOf withTransaction (chatStore cc) $ \db -> DB.execute db "UPDATE sx_badge_service_badge_issuances SET credential = ?" (Only (Binary ("not a credential" :: ByteString))) - redeem holder >>= (`shouldAnswerError` BSEInternal) + redeem holder >>= (`shouldAnswerError` STEInternal) codeUses cc code `shouldReturn` Just (2, 1) newPurchaseKeys >>= redeem >>= (`shouldSatisfy` isJust) . credentialOf codeUses cc code `shouldReturn` Just (2, 2) - redeem holder >>= (`shouldAnswerError` BSEInternal) + redeem holder >>= (`shouldAnswerError` STEInternal) codeUses cc code `shouldReturn` Just (2, 2) -- Multi-use codes come from the group command, and this service has no group. @@ -858,7 +858,7 @@ testRevokedMultiUseHolderRenews ps = redeemFirstBadge alice code redeemed <- ledgerRows (chatController alice) "badge_ledger" revokeCodeAs cc code `shouldReturn` Right "Revoked." - redeemAsNewPurchase env code >>= (`shouldAnswerError` BSECodeInvalid) + redeemAsNewPurchase env code >>= (`shouldAnswerError` STECodeInvalid) codeUses cc code `shouldReturn` Just (2, 1) -- A holder whose reply was lost retries with the same key and still gets its badge. alice ##> ("/_redeem_badge_code 1 " <> codeArg code) @@ -1234,13 +1234,13 @@ testNoCredentialMonthsLeft ps = setClockAt bsClock $ snd $ renewalMoments rows alice ##> "/_app activate" alice <## "ok" - alice <## "internal chat error: badge service issued no credential" + alice <## "internal chat error: store service issued no credential" alice <##. "badge alert: issue_failed " alice <##. "1: supporter" alice <##. "renewal failing since " alice <##. "badge alert: issue_failed " (_, reason) <- issueErrorRow (chatController alice) - reason `shouldBe` Just "unexpected badge service issued no credential" + reason `shouldBe` Just "unexpected store service issued no credential" -- | A debit the service wrote on its own, taking back the months the purchase had: copied from -- the tip so that it follows it in every column the client checks. @@ -1262,7 +1262,7 @@ zeroServiceLedgerTip ChatController {chatStore} = DB.execute_ db "UPDATE sx_badge_service_badge_ledger SET balance_months = 0 WHERE entry_id = (SELECT MAX(entry_id) FROM sx_badge_service_badge_ledger)" badgeServiceRefused :: String -badgeServiceRefused = "bad chat command: badge service error: unknown_purchase_key" +badgeServiceRefused = "bad chat command: store service error: unknown_purchase_key" -- | The alert's episode is the start of the run of failures, as the ack command spells it. episodeOf :: HasCallStack => Maybe UTCTime -> Text @@ -1498,7 +1498,7 @@ testRedeemUnpaidCode ps = withNewTestChatCfg ps clientCfg "alice" aliceProfile $ \alice -> do unpaid <- issueCodeAs cc BTSupporter 1 "unpaid" alice ##> ("/_redeem_badge_code 1 " <> codeArg unpaid) - alice <## "cannot redeem badge code: badge service error: payment_pending" + alice <## "cannot redeem badge code: store service error: payment_pending" paid <- issueCodeAs cc BTSupporter 1 "paid" alice ##> ("/_redeem_badge_code 1 " <> codeArg paid) alice <## "badge redeemed" @@ -1515,7 +1515,7 @@ testExpiredCode ps = withDB' "markCodePaid" cc (\db -> markCodePaid db (badgeCodeHash code) (addUTCTime (-60) now)) `shouldReturn` Right () alice ##> ("/_redeem_badge_code 1 " <> codeArg code) - alice <## "cannot redeem badge code: badge service error: code_expired" + alice <## "cannot redeem badge code: store service error: code_expired" testRedeemedBeforeTheDeadline :: HasCallStack => TestParams -> IO () testRedeemedBeforeTheDeadline ps = @@ -1569,7 +1569,7 @@ testRevokedCode ps = paid <- issueCodeAs cc BTSupporter 1 "paid" revokeCodeAs cc paid `shouldReturn` Right "Revoked." alice ##> ("/_redeem_badge_code 1 " <> codeArg paid) - alice <## "cannot redeem badge code: badge service error: code_invalid" + alice <## "cannot redeem badge code: store service error: code_invalid" revokeCodeAs cc paid `shouldReturn` Right "Already revoked." testRevokedUnpaidCode :: HasCallStack => TestParams -> IO () @@ -1577,7 +1577,7 @@ testRevokedUnpaidCode ps = withBadgeServiceEnv ps $ \env@BadgeServiceEnv {bsController = cc} -> do code <- issueCodeAs cc BTSupporter 1 "unpaid" revokeCodeAs cc code `shouldReturn` Right "Revoked." - redeemAsNewPurchase env code >>= (`shouldAnswerError` BSECodeInvalid) + redeemAsNewPurchase env code >>= (`shouldAnswerError` STECodeInvalid) testRevokeRejectsTrailingInput :: HasCallStack => TestParams -> IO () testRevokeRejectsTrailingInput ps = diff --git a/tests/Bots/BadgeService/CatalogTests.hs b/tests/Bots/BadgeService/CatalogTests.hs index 9e729cd8bb..080d366f21 100644 --- a/tests/Bots/BadgeService/CatalogTests.hs +++ b/tests/Bots/BadgeService/CatalogTests.hs @@ -15,7 +15,7 @@ import Data.Time.Clock (UTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.Word (Word32, Word8) import Simplex.Chat.Badges (BadgeType (..)) -import Simplex.Chat.Badges.Service (BadgeOffer (..), BadgePrice (..)) +import Simplex.Chat.StoreService (BadgeOffer (..), BadgePrice (..)) import Simplex.Chat.Badges.Types (BadgeItemStatus (..), BadgeOfferId (..), BadgePriceId (..), OfferDiscount (..)) import Simplex.Chat.PaymentService.Types (CurrencyAmount (..)) import Test.Hspec diff --git a/tests/Bots/BadgeService/GroupIntegrationTests.hs b/tests/Bots/BadgeService/GroupIntegrationTests.hs index 8b4cbc249a..6919be7d47 100644 --- a/tests/Bots/BadgeService/GroupIntegrationTests.hs +++ b/tests/Bots/BadgeService/GroupIntegrationTests.hs @@ -40,7 +40,7 @@ import Network.Socket (Family (..), SockAddr (..), SocketType (..), close, conne import Network.Wai.Handler.Warp (openFreePort) import Simplex.Chat.Badges (BadgeType (..)) import Simplex.Chat.Badges.Code (formatBadgeCode, parseBadgeCode, randomBadgeCode) -import Simplex.Chat.Badges.Service +import Simplex.Chat.StoreService import Simplex.Chat.Controller (ChatCommand (APIDeleteChatItem), ChatConfig (..), ChatController (..), ChatResponse (CRChatItemsDeleted)) import Simplex.Chat.Core (sendChatCmd, sendChatCmdStr) import Simplex.Chat.Messages (ChatRef (ChatRef), ChatType (CTGroup)) @@ -564,7 +564,7 @@ testBulkIssue ps = r <- redeemViaService gsKey cc env code badgeTypeOf r `shouldBe` Just BTLegend map (\e -> let (c, m, _) = entryOf e in (c, m)) (fst $ statementOf r) `shouldBe` [(2, 2), (-1, 1)] - forM_ codes $ \code -> redeemViaService gsKey cc env code >>= (`shouldAnswerError` BSECodeUsed) + forM_ codes $ \code -> redeemViaService gsKey cc env code >>= (`shouldAnswerError` STECodeUsed) countRows cc "sx_badge_service_badge_codes WHERE group_item_id IS NOT NULL" `shouldReturn` 0 testIssueMonths :: HasCallStack => TestParams -> IO () @@ -640,7 +640,7 @@ testMultiUseTracker ps = redeemOk gsKey cc env code usedUp <- waitItemText cc trackerItemId "All 2 uses redeemed" sentItemsWithCode cc code `shouldReturn` [usedUp] - redeemViaService gsKey cc env code >>= (`shouldAnswerError` BSECodeUsed) + redeemViaService gsKey cc env code >>= (`shouldAnswerError` STECodeUsed) testStaleRefreshIgnored :: HasCallStack => TestParams -> IO () testStaleRefreshIgnored ps = @@ -811,7 +811,7 @@ testGroupRevoke ps = code <- extractCode <$> replyTo cc alice "/issue supporter" other <- extractCode <$> replyTo cc alice "/issue legend" revokeInGroup cc alice code "Revoked." - redeemViaService gsKey cc env code >>= (`shouldAnswerError` BSECodeInvalid) + redeemViaService gsKey cc env code >>= (`shouldAnswerError` STECodeInvalid) redeemOk gsKey cc env other revokeInGroup cc alice code "Already revoked." -- The reply follows the tracker step, so the revoke has already passed it here. @@ -959,7 +959,7 @@ testRevokeRetiresTracker ps = awaitLane cc env readItemText cc trackerItemId `shouldReturn` retired revokedTrackerCount cc `shouldReturn` 1 - redeemViaService gsKey cc env code >>= (`shouldAnswerError` BSECodeInvalid) + redeemViaService gsKey cc env code >>= (`shouldAnswerError` STECodeInvalid) testServiceRevokeRetiresTracker :: HasCallStack => TestParams -> IO () testServiceRevokeRetiresTracker ps = @@ -1000,7 +1000,7 @@ redeemOkWith key cc trackerQ_ code = do r <- redeemWithQueue key cc trackerQ_ code credentialOf r `shouldSatisfy` isJust -redeemViaService :: HasCallStack => BadgeIssuerKey -> ChatController -> ServiceState -> Text -> IO BadgeServiceResponse +redeemViaService :: HasCallStack => BadgeIssuerKey -> ChatController -> ServiceState -> Text -> IO StoreServiceResponse redeemViaService key cc env = redeemWithQueue key cc (Just (groupEventQ env)) -- The reply to the made-up request id fails after the redemption, and the service only logs it. @@ -1008,7 +1008,7 @@ queueRedemption :: HasCallStack => ChatController -> ServiceState -> Text -> IO queueRedemption cc env codeText = do user <- readTVarIO (currentUser cc) >>= maybe (error "no current user") pure (purchaseKey, masterKey) <- newPurchaseKeys - let request = requestObject purchaseKey BSCRedeemBadgeCode {masterKey, code = codeText} + let request = requestObject purchaseKey STCRedeemBadgeCode {masterKey, code = codeText} atomically $ writeTQueue (serviceRequestQ env) (user, AgentInvId "test-request", Just purchaseKey, request) redeemLosingRefresh :: HasCallStack => BadgeIssuerKey -> ChatController -> Text -> IO () diff --git a/tests/Bots/BadgeService/WebTests.hs b/tests/Bots/BadgeService/WebTests.hs index 15203c75ac..577d54c313 100644 --- a/tests/Bots/BadgeService/WebTests.hs +++ b/tests/Bots/BadgeService/WebTests.hs @@ -59,7 +59,7 @@ import Network.HTTP.Types (Header, HeaderName, hCacheControl, hContentType) import Network.HTTP.Types.Status (statusCode) import qualified Network.Wai.Handler.Warp as Warp import Simplex.Chat.Badges (BadgeCredential (..), BadgeInfo (..), BadgeMasterKey (..), BadgeType (..)) -import Simplex.Chat.Badges.Service (BadgeOffer (..), BadgePrice (..)) +import Simplex.Chat.StoreService (BadgeOffer (..), BadgePrice (..)) import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..), BadgeItemStatus (..), BadgeOfferId (..), BadgePriceId (..), OfferDiscount (..)) import Simplex.Chat.PaymentService.Types (CryptoCurrency (..), CurrencyAmount (..), InvoiceId (..), InvoiceStatus (..), PaymentProvider (..), PaymentStatus (..), ServicePaymentDestination (..), ServicePaymentMethod (..)) import Simplex.Messaging.Agent.Store.Common (DBStore (..), withConnection, withTransaction)