mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-11 13:57:42 +00:00
core: alert a subscriber that a payment failed, a renewal nears, or it was cancelled or ended
A subscription's alerts were the prepaid ones: a retired subscription said "support ended", and nothing told the user a payment failed, an annual renewal was near, or a cancelled subscription still runs. The derived alert for a subscription is now, in order: subscription_ended once retired, payment_issue while the store retries, issue_failed as before, subscription_cancelled before paidThrough, and renewal_approaching from three days before an annual plan renews, with a wake at that moment. Monthly plans get no reminder, as there is no opt-out yet. The unreleased migration's down step clears an acked subscription_cancelled, which an older core cannot read. Run: 003-badge-subscriptions-ui, slice 7
This commit is contained in:
1 parent
a3b2e5c9f1
commit
e051b50323
6 files changed
+169
-21
No files matched your search
@@ -120,13 +120,14 @@ data LedgerDebitType
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- unconfirmed draft
|
||||
data BadgeAlertKind = BARenewalApproaching | BAPaymentIssue | BASubscriptionEnded | BAPrepaidEnding | BASupportEnded | BAIssueFailed
|
||||
data BadgeAlertKind = BARenewalApproaching | BAPaymentIssue | BASubscriptionCancelled | BASubscriptionEnded | BAPrepaidEnding | BASupportEnded | BAIssueFailed
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance TextEncoding BadgeAlertKind where
|
||||
textEncode = \case
|
||||
BARenewalApproaching -> "renewal_approaching"
|
||||
BAPaymentIssue -> "payment_issue"
|
||||
BASubscriptionCancelled -> "subscription_cancelled"
|
||||
BASubscriptionEnded -> "subscription_ended"
|
||||
BAPrepaidEnding -> "prepaid_ending"
|
||||
BASupportEnded -> "support_ended"
|
||||
@@ -134,6 +135,7 @@ instance TextEncoding BadgeAlertKind where
|
||||
textDecode = \case
|
||||
"renewal_approaching" -> Just BARenewalApproaching
|
||||
"payment_issue" -> Just BAPaymentIssue
|
||||
"subscription_cancelled" -> Just BASubscriptionCancelled
|
||||
"subscription_ended" -> Just BASubscriptionEnded
|
||||
"prepaid_ending" -> Just BAPrepaidEnding
|
||||
"support_ended" -> Just BASupportEnded
|
||||
|
||||
@@ -5637,7 +5637,10 @@ updateUserBadge userId emitted now = do
|
||||
UserBadgePurchase {alertSnoozeUntil, issueError = failureAfter, purchaseKind = kindAfter} = p'
|
||||
snoozeAt = find (> now) alertSnoozeUntil
|
||||
stalledAt = if requestDue && not issued then Just $ badgeStalledInterval `addUTCTime` now else Nothing
|
||||
wakeAt = earliestTime [serviceAt, snoozeAt, stalledAt, badgeBoundary now (shownBadgeCredential user'' p') balance']
|
||||
reminderAt = case kindAfter of
|
||||
PKSubscription s -> find (> now) $ renewalReminderAt s balance'
|
||||
PKPrepaid -> Nothing
|
||||
wakeAt = earliestTime [serviceAt, snoozeAt, stalledAt, reminderAt, badgeBoundary now (shownBadgeCredential user'' p') balance']
|
||||
failed = failureAfter /= failureBefore
|
||||
subscriptionChanged = kindAfter /= purchaseKind
|
||||
-- written before the event, so the state it reports carries the attempt it leads to
|
||||
@@ -5645,21 +5648,35 @@ updateUserBadge userId emitted now = do
|
||||
when (retired || issued || failed || subscriptionChanged) $ toView . CEvtBadgeChanged user'' =<< getUserBadgeState user''
|
||||
pure wakeAt
|
||||
|
||||
-- | The other kinds need subscriptions, and warning before a prepaid badge ends is not actionable
|
||||
-- while topping up cannot credit months without issuing.
|
||||
-- | Warning before a prepaid badge ends is not actionable while topping up cannot credit months without issuing.
|
||||
-- TODO [badges] BAPrepaidEnding belongs here, three days before paidThrough, once that exists.
|
||||
derivedBadgeAlert :: UTCTime -> UserBadgePurchase -> Maybe BadgeCredential -> StatementEntry -> Maybe BadgeAlert
|
||||
derivedBadgeAlert now p@UserBadgePurchase {purchaseKind, shown} shownCred b
|
||||
-- a subscription still shown past paidThrough is waiting for its store to charge, not ended
|
||||
| endsAt <= now && not (subscribed && shown) = Just $ alertOf BASupportEnded endsAt
|
||||
| otherwise = (\BadgeIssueError {failedSince} -> alertOf BAIssueFailed failedSince) <$> shownIssueError now p shownCred
|
||||
derivedBadgeAlert now p@UserBadgePurchase {purchaseKind, shown} shownCred b = case purchaseKind of
|
||||
PKPrepaid
|
||||
| endsAt <= now -> Just $ alertOf BASupportEnded endsAt
|
||||
| otherwise -> issueFailed
|
||||
PKSubscription s@BadgeSubscription {status}
|
||||
-- a subscription still shown past paidThrough is waiting for its store to charge, not ended
|
||||
| endsAt <= now && not shown -> Just $ alertOf BASubscriptionEnded endsAt
|
||||
| status == Just SSBillingRetry -> Just $ alertOf BAPaymentIssue endsAt
|
||||
| isJust issueFailed -> issueFailed
|
||||
| now < endsAt && status == Just SSCancelled -> Just $ alertOf BASubscriptionCancelled endsAt
|
||||
| now < endsAt && maybe False (<= now) (renewalReminderAt s b) -> Just $ alertOf BARenewalApproaching endsAt
|
||||
| otherwise -> Nothing
|
||||
where
|
||||
subscribed = case purchaseKind of
|
||||
PKSubscription _ -> True
|
||||
PKPrepaid -> False
|
||||
endsAt = L.paidThrough b
|
||||
issueFailed = (\BadgeIssueError {failedSince} -> alertOf BAIssueFailed failedSince) <$> shownIssueError now p shownCred
|
||||
alertOf kind date = BadgeAlert {kind, episode = safeDecodeUtf8 $ strEncode date, date, price = Nothing}
|
||||
|
||||
-- | Only an annual plan is reminded of: with no way to turn reminders off, a monthly one would come every month.
|
||||
renewalReminderAt :: BadgeSubscription -> StatementEntry -> Maybe UTCTime
|
||||
renewalReminderAt BadgeSubscription {planMonths, status} b
|
||||
| planMonths == 12 && status == Just SSRenewing = Just $ negate renewalReminderLead `addUTCTime` L.paidThrough b
|
||||
| otherwise = Nothing
|
||||
|
||||
renewalReminderLead :: NominalDiffTime
|
||||
renewalReminderLead = 3 * nominalDay
|
||||
|
||||
-- | A failure that can clear on its own is only shown once the credential lapses and contacts see it.
|
||||
shownIssueError :: UTCTime -> UserBadgePurchase -> Maybe BadgeCredential -> Maybe BadgeIssueError
|
||||
shownIssueError now UserBadgePurchase {issueError} shownCred = case issueError of
|
||||
|
||||
@@ -43,6 +43,8 @@ ALTER TABLE badge_purchases ADD COLUMN subscription_status TEXT;
|
||||
down_m20260925_badge_store_receipts :: Text
|
||||
down_m20260925_badge_store_receipts =
|
||||
[r|
|
||||
UPDATE badge_purchases SET alert_acked_kind = NULL, alert_acked_episode = NULL, alert_snooze_until = NULL WHERE alert_acked_kind = 'subscription_cancelled';
|
||||
|
||||
ALTER TABLE badge_purchases DROP COLUMN subscription_status;
|
||||
|
||||
DROP INDEX idx_badge_purchases_store_receipt;
|
||||
|
||||
@@ -42,6 +42,8 @@ ALTER TABLE badge_purchases ADD COLUMN subscription_status TEXT;
|
||||
down_m20260925_badge_store_receipts :: Query
|
||||
down_m20260925_badge_store_receipts =
|
||||
[sql|
|
||||
UPDATE badge_purchases SET alert_acked_kind = NULL, alert_acked_episode = NULL, alert_snooze_until = NULL WHERE alert_acked_kind = 'subscription_cancelled';
|
||||
|
||||
ALTER TABLE badge_purchases DROP COLUMN subscription_status;
|
||||
|
||||
DROP INDEX idx_badge_purchases_store_receipt;
|
||||
|
||||
+59
-2
@@ -35,13 +35,14 @@ import Simplex.Chat.Badges
|
||||
import Simplex.Chat.Badges.Code
|
||||
import Simplex.Chat.Badges.Ledger
|
||||
import Simplex.Chat.Badges.Service
|
||||
import Simplex.Chat.Badges.Types (BadgeIssueFailure (..), BadgeSubscription (..))
|
||||
import Simplex.Chat.Badges.Types (BadgeAlertKind (..), BadgeIssueError (..), BadgeIssueFailure (..), BadgeSubscription (..))
|
||||
import qualified Simplex.Chat.Badges.Types as BT
|
||||
import Simplex.Chat (defaultChatConfig)
|
||||
import Simplex.Chat.Controller (BadgeRedeemError (..), ChatError (..), ChatErrorType (..), badgeRetryInterval, chatErrorAgent)
|
||||
import Simplex.Chat.Library.Commands (badgeErrorRetry, badgeFailureTransient, badgeIssueFailure, badgeRenewal, badgeRetryAfter, badgeServiceErrorText, badgeStalledInterval, boundedSubscriptionState, storeTransactionRef)
|
||||
import Simplex.Chat.Library.Commands (badgeErrorRetry, badgeFailureTransient, badgeIssueFailure, badgeRenewal, badgeRetryAfter, badgeServiceErrorText, badgeStalledInterval, boundedSubscriptionState, derivedBadgeAlert, storeTransactionRef)
|
||||
import Simplex.Chat.PaymentService (AppleTransactionFacts (..), ServicePayment (..), appleTransactionFacts, appleTransactionRef)
|
||||
import Simplex.Chat.PaymentService.Types (InvoiceId (..), PaymentProvider (..), StoreTransactionRef (..))
|
||||
import qualified Simplex.Chat.Store.Badges as SB
|
||||
import Simplex.Chat.Types (BoolDef (..))
|
||||
import Simplex.Messaging.Agent.Protocol (AgentErrorType (..), AgentServiceError (..), SMPAgentError (..))
|
||||
import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay)
|
||||
@@ -113,6 +114,7 @@ badgeTests = do
|
||||
it "statement entries round-trip, unknown entry types verbatim" testStatementJSON
|
||||
describe "badge state" $ do
|
||||
it "carries a subscription's plan and last seen state, renewing only when it was seen renewing, and bounds a state it does not know" testBadgeSubscriptionJSON
|
||||
it "derives one alert for a subscription in order of urgency, dated paidThrough, and leaves prepaid alerts as they were" testSubscriptionAlertDerivation
|
||||
describe "store purchases" $ do
|
||||
it "keys a purchase by the store's transaction id, not by the evidence signed over it" testStoreTransactionRef
|
||||
it "claims an Apple subscription by its original transaction id, and any other transaction by its own" testAppleTransactionRef
|
||||
@@ -1119,6 +1121,61 @@ testBadgeSubscriptionJSON = do
|
||||
boundedSubscriptionState (SSUnknown (T.replicate 100 "a")) `shouldBe` SSUnknown (T.replicate 32 "a")
|
||||
boundedSubscriptionState (SSUnknown "") `shouldBe` SSUnknown "unknown"
|
||||
|
||||
testSubscriptionAlertDerivation :: IO ()
|
||||
testSubscriptionAlertDerivation = do
|
||||
drg <- C.newRandom
|
||||
(k, pk) <- atomically $ C.generateKeyPair drg :: IO (C.KeyPair 'C.Ed25519)
|
||||
mk <- generateMasterKey drg
|
||||
let start = at 2026 3 10
|
||||
ends = addMonths 1 start
|
||||
balance = grant start 1 (newBalance start)
|
||||
failedAt = at 2026 3 20
|
||||
failure = BadgeIssueError {failedSince = failedAt, lastAttemptAt = failedAt, reason = BIFServiceError {code = BSEUnknownPurchaseKey, retryable = False}}
|
||||
early = addUTCTime (-10 * nominalDay) ends
|
||||
soon = addUTCTime (-nominalDay) ends
|
||||
past = addUTCTime nominalDay ends
|
||||
subscription months st = SB.PKSubscription BadgeSubscription {planMonths = months, status = st}
|
||||
alertOf t kind' isShown err =
|
||||
let p = SB.UserBadgePurchase {badgePurchaseId = 1, purchaseKey = k, purchasePrivKey = pk, masterKey = mk, badgeType = BTSupporter, shown = isShown, alertAcked = Nothing, alertSnoozeUntil = Nothing, issueError = err, nextWakeAt = Nothing, purchaseKind = kind'}
|
||||
in (\BT.BadgeAlert {kind, date} -> (kind, date)) <$> derivedBadgeAlert t p Nothing balance
|
||||
renewing = Just SSRenewing
|
||||
cancelled = Just SSCancelled
|
||||
billingRetry = Just SSBillingRetry
|
||||
mapM_
|
||||
(\(t, kind', isShown, err, expected) -> alertOf t kind' isShown err `shouldBe` expected)
|
||||
[ -- retired at or after paidThrough, whatever else holds
|
||||
(past, subscription 1 renewing, False, Nothing, Just (BASubscriptionEnded, ends)),
|
||||
(past, subscription 12 billingRetry, False, Just failure, Just (BASubscriptionEnded, ends)),
|
||||
(past, subscription 1 cancelled, False, Nothing, Just (BASubscriptionEnded, ends)),
|
||||
-- still shown past paidThrough: waiting for the store to charge
|
||||
(past, subscription 1 renewing, True, Nothing, Nothing),
|
||||
(past, subscription 12 Nothing, True, Nothing, Nothing),
|
||||
(past, subscription 1 cancelled, True, Nothing, Nothing),
|
||||
-- billing retry before and after paidThrough, over a renewal failure
|
||||
(early, subscription 12 billingRetry, True, Nothing, Just (BAPaymentIssue, ends)),
|
||||
(past, subscription 1 billingRetry, True, Just failure, Just (BAPaymentIssue, ends)),
|
||||
-- a renewal failure over a cancellation and a reminder
|
||||
(soon, subscription 1 cancelled, True, Just failure, Just (BAIssueFailed, failedAt)),
|
||||
(soon, subscription 12 renewing, True, Just failure, Just (BAIssueFailed, failedAt)),
|
||||
(early, subscription 1 cancelled, True, Nothing, Just (BASubscriptionCancelled, ends)),
|
||||
(soon, subscription 12 cancelled, True, Nothing, Just (BASubscriptionCancelled, ends)),
|
||||
-- reminded only for a renewing annual plan, and only in the last three days
|
||||
(soon, subscription 12 renewing, True, Nothing, Just (BARenewalApproaching, ends)),
|
||||
(early, subscription 12 renewing, True, Nothing, Nothing),
|
||||
(soon, subscription 1 renewing, True, Nothing, Nothing),
|
||||
(soon, subscription 12 Nothing, True, Nothing, Nothing),
|
||||
(soon, subscription 12 (Just $ SSUnknown "paused"), True, Nothing, Nothing),
|
||||
(soon, subscription 1 (Just SSEnded), True, Nothing, Nothing),
|
||||
-- prepaid, as before
|
||||
(past, SB.PKPrepaid, True, Nothing, Just (BASupportEnded, ends)),
|
||||
(past, SB.PKPrepaid, False, Just failure, Just (BASupportEnded, ends)),
|
||||
(soon, SB.PKPrepaid, True, Just failure, Just (BAIssueFailed, failedAt)),
|
||||
(soon, SB.PKPrepaid, True, Nothing, Nothing)
|
||||
]
|
||||
-- the reminder starts exactly three days before paidThrough
|
||||
alertOf (addUTCTime (-3 * nominalDay) ends) (subscription 12 renewing) True Nothing `shouldBe` Just (BARenewalApproaching, ends)
|
||||
alertOf (addUTCTime (-3 * nominalDay - 1) ends) (subscription 12 renewing) True Nothing `shouldBe` Nothing
|
||||
|
||||
testErrorResponseJSON :: IO ()
|
||||
testErrorResponseJSON = do
|
||||
let resp = BSPError {code = BSECodeInvalid, message = Nothing, retryAfter = Nothing}
|
||||
|
||||
@@ -201,6 +201,8 @@ badgeServiceTests = do
|
||||
it "should keep the state the service answered when it credited and when it renewed past paid coverage, and announce a change" testSubscriptionStateKept
|
||||
it "should keep the state from the credit through an issuance the service answers from its stored state" testAnnualStateNotFromStoredAnswer
|
||||
it "should keep the device store's view of a credited subscription, except renewing over billing retry, and nothing for any other transaction" testSubscriptionStateFromStore
|
||||
it "should alert a cancelled subscription until acknowledged, and not remind a monthly plan of its renewal" testSubscriptionAlerts
|
||||
it "should remind an annual plan three days before it renews, from a wake of its own, until acknowledged" testRenewalReminder
|
||||
it "should resolve an Apple renewal to the subscription's record, and credit a held record that adopted it" testAppleRenewalResolvesToPurchase
|
||||
it "should adopt into a held Apple record only another transaction bought later and not revoked" testAppleAdoptionRules
|
||||
it "should credit an adopted Apple transaction at once though the attempt with the one before it failed" testAdoptedReceiptNotOverwritten
|
||||
@@ -2291,7 +2293,13 @@ testSubscriptionAwaitsCharge ps =
|
||||
checks <- subscriptionChecks store
|
||||
setClockAt bsClock requestAt
|
||||
alice `send` "/_app activate"
|
||||
alice <### ["/_app activate", "ok", Predicate (", subscription billingRetry" `isInfixOf`)]
|
||||
alice
|
||||
<### [ "/_app activate",
|
||||
"ok",
|
||||
paymentIssue,
|
||||
Predicate (\l -> "1: supporter" `isPrefixOf` l && ", subscription billingRetry" `isInfixOf` l),
|
||||
paymentIssue
|
||||
]
|
||||
-- nothing issued and no failure: the badge stays, and the next request is a day on
|
||||
waitWakeFrom (chatController alice) (addUTCTime (nominalDay - 60) requestAt)
|
||||
subscriptionChecks store `shouldReturn` checks + 1
|
||||
@@ -2322,9 +2330,9 @@ testSubscriptionEnds ps =
|
||||
alice
|
||||
<### [ "/_app activate",
|
||||
"ok",
|
||||
StartsWith "badge alert: support_ended ",
|
||||
StartsWith "badge alert: subscription_ended ",
|
||||
StartsWith "1: supporter",
|
||||
StartsWith "badge alert: support_ended "
|
||||
StartsWith "badge alert: subscription_ended "
|
||||
]
|
||||
waitShownBadge (chatController alice) Nothing
|
||||
-- a retired purchase is not asked about again
|
||||
@@ -2351,9 +2359,68 @@ testSubscriptionStateKept ps =
|
||||
storeReports store $ subscriptionCheck SSBillingRetry Nothing
|
||||
setClockAt bsClock $ fst $ renewalMoments rows
|
||||
alice `send` "/_app activate"
|
||||
alice <### ["/_app activate", "ok", Predicate (", subscription billingRetry" `isInfixOf`)]
|
||||
alice <### ["/_app activate", "ok", Predicate (", subscription billingRetry" `isInfixOf`), paymentIssue, paymentIssue]
|
||||
subscriptionStatuses (chatController alice) `shouldReturn` [Just "billingRetry"]
|
||||
|
||||
nextWakes :: ChatController -> IO [Maybe UTCTime]
|
||||
nextWakes ChatController {chatStore} =
|
||||
withTransaction chatStore $ \db -> map fromOnly <$> DB.query_ db "SELECT next_wake_at FROM badge_purchases"
|
||||
|
||||
-- | A monthly plan is not reminded of its renewal; a cancellation is alerted until acknowledged.
|
||||
testSubscriptionAlerts :: HasCallStack => TestParams -> IO ()
|
||||
testSubscriptionAlerts ps =
|
||||
withBadgeServiceEnv ps $ \env@BadgeServiceEnv {bsClock, bsClientCfg} ->
|
||||
withNewTestChatCfg ps bsClientCfg "alice" aliceProfile $ \alice -> do
|
||||
(firstStart, _) <- clientSubscribedMonthly env alice
|
||||
let paidThrough = addMonths 1 firstStart
|
||||
episode = T.unpack $ safeDecodeUtf8 $ strEncode paidThrough
|
||||
armWakeAt alice bsClock $ addUTCTime (-3 * nominalDay) paidThrough
|
||||
nextWakes (chatController alice) >>= (`shouldSatisfy` all (maybe False (>= paidThrough)))
|
||||
threadDelay 2000000
|
||||
(alice </)
|
||||
alice ##> ("/_badge subscription cancelled " <> paymentArg subscriptionPlay)
|
||||
alice <### ["ok", StartsWith "1: supporter", subscriptionCancelled, subscriptionCancelled]
|
||||
alice ##> ("/_badge ack 1 1 subscription_cancelled off " <> episode)
|
||||
alice <##. "1: supporter"
|
||||
ackedEpisode (chatController alice) `shouldReturn` (Just "subscription_cancelled", Just (T.pack episode))
|
||||
alice ##> "/_badge state 1"
|
||||
alice <##. "1: supporter"
|
||||
(alice </)
|
||||
|
||||
-- | An annual plan is reminded three days before it renews, by a wake of its own.
|
||||
testRenewalReminder :: HasCallStack => TestParams -> IO ()
|
||||
testRenewalReminder ps =
|
||||
withBadgeServiceEnv ps $ \env@BadgeServiceEnv {bsClock, bsClientCfg, bsStore = store@FakeStore {appleSubscriptionJWS}} ->
|
||||
withNewTestChatCfg ps bsClientCfg "alice" aliceProfile $ \alice -> do
|
||||
firstStart <- addUTCTime (-3600) <$> serviceNow env
|
||||
setSubscription store (SubscriptionApple appleSubscriptionOriginalId) $ subscriptionCheck SSRenewing $ Just $ storeCharge "SUBSCR_BADGE_SUPPORTER_YEAR_01" firstStart 12
|
||||
alice ##> ("/_badge purchase 1 " <> paymentArg SPApple {jws = appleSubscriptionJWS})
|
||||
inAnyOrder alice [[openPurchase ""], ["1: supporter"]]
|
||||
waitWakeArmed (chatController alice)
|
||||
let paidThrough = addMonths 12 firstStart
|
||||
remindAt = addUTCTime (-3 * nominalDay) paidThrough
|
||||
episode = T.unpack $ safeDecodeUtf8 $ strEncode paidThrough
|
||||
-- with the last month issued, the reminder is the next thing due
|
||||
setClockAt bsClock $ addUTCTime (-20 * nominalDay) paidThrough
|
||||
alice ##> "/_app activate"
|
||||
alice <## "ok"
|
||||
alice <##. "1: supporter, 0 months left"
|
||||
waitWakeFrom (chatController alice) remindAt
|
||||
nextWakes (chatController alice) `shouldReturn` [Just remindAt]
|
||||
armWakeAt alice bsClock remindAt
|
||||
alice <##. "badge alert: renewal_approaching "
|
||||
alice ##> ("/_badge ack 1 1 renewal_approaching off " <> episode)
|
||||
alice <##. "1: supporter"
|
||||
alice ##> "/_badge state 1"
|
||||
alice <##. "1: supporter"
|
||||
(alice </)
|
||||
|
||||
paymentIssue :: ConsoleResponse
|
||||
paymentIssue = StartsWith "badge alert: payment_issue "
|
||||
|
||||
subscriptionCancelled :: ConsoleResponse
|
||||
subscriptionCancelled = StartsWith "badge alert: subscription_cancelled "
|
||||
|
||||
-- | The service answers a request made while coverage runs from the state it stored, which for an annual plan is up to a year old.
|
||||
testAnnualStateNotFromStoredAnswer :: HasCallStack => TestParams -> IO ()
|
||||
testAnnualStateNotFromStoredAnswer ps =
|
||||
@@ -2362,7 +2429,7 @@ testAnnualStateNotFromStoredAnswer ps =
|
||||
firstStart <- addUTCTime (-3600) <$> serviceNow env
|
||||
storeReports store $ subscriptionCheck SSCancelled $ Just $ storeCharge "subscr-badge-supporter-year-01" firstStart 12
|
||||
alice ##> ("/_badge purchase 1 " <> paymentArg subscriptionPlay)
|
||||
inAnyOrder alice [[openPurchase ""], ["1: supporter"]]
|
||||
inAnyOrder alice [[openPurchase ""], ["1: supporter", "badge alert: subscription_cancelled "], ["badge alert: subscription_cancelled "]]
|
||||
waitWakeArmed (chatController alice)
|
||||
rows <- ledgerRows (chatController alice) "badge_ledger"
|
||||
map (\(_, ch, m, _, _, t) -> (ch, m, t)) rows `shouldBe` [(12, 12, Just "charge"), (-1, 11, Just "badge")]
|
||||
@@ -2373,7 +2440,7 @@ testAnnualStateNotFromStoredAnswer ps =
|
||||
alice ##> "/_app activate"
|
||||
alice <## "ok"
|
||||
_ <- waitLedgerRows (chatController alice) 3
|
||||
alice <### [Predicate (\l -> "1: supporter, 10 months left" `isPrefixOf` l && ", subscription cancelled" `isInfixOf` l)]
|
||||
alice <### [Predicate (\l -> "1: supporter, 10 months left" `isPrefixOf` l && ", subscription cancelled" `isInfixOf` l), subscriptionCancelled]
|
||||
subscriptionChecks store `shouldReturn` checks
|
||||
subscriptionPayments cc `shouldReturn` [("google", googlePurchaseRef googleSubscriptionToken, Just "renewing", Just googleSubscriptionToken)]
|
||||
subscriptionStatuses (chatController alice) `shouldReturn` [Just "cancelled"]
|
||||
@@ -2385,7 +2452,7 @@ testSubscriptionStateFromStore ps =
|
||||
(_, rows) <- clientSubscribedMonthly env alice
|
||||
let report state payment = alice ##> ("/_badge subscription " <> state <> " " <> paymentArg payment)
|
||||
report "cancelled" subscriptionPlay
|
||||
alice <### ["ok", Predicate (", subscription cancelled" `isInfixOf`)]
|
||||
alice <### ["ok", Predicate (", subscription cancelled" `isInfixOf`), subscriptionCancelled, subscriptionCancelled]
|
||||
subscriptionStatuses (chatController alice) `shouldReturn` [Just "cancelled"]
|
||||
report "cancelled" subscriptionPlay
|
||||
alice <## "ok"
|
||||
@@ -2393,7 +2460,7 @@ testSubscriptionStateFromStore ps =
|
||||
storeReports store $ subscriptionCheck SSBillingRetry Nothing
|
||||
setClockAt bsClock $ fst $ renewalMoments rows
|
||||
alice `send` "/_app activate"
|
||||
alice <### ["/_app activate", "ok", Predicate (", subscription billingRetry" `isInfixOf`)]
|
||||
alice <### ["/_app activate", "ok", Predicate (", subscription billingRetry" `isInfixOf`), paymentIssue, paymentIssue]
|
||||
checks <- subscriptionChecks store
|
||||
report "renewing" subscriptionPlay
|
||||
alice <## "ok"
|
||||
@@ -2401,6 +2468,7 @@ testSubscriptionStateFromStore ps =
|
||||
-- a held transaction, here a one-time purchase handed over while the badge is shown, and an unknown one change nothing
|
||||
alice ##> ("/_badge purchase 1 " <> paymentArg supporterPlay)
|
||||
alice <##. "1: supporter"
|
||||
alice <##. "badge alert: payment_issue "
|
||||
alice <##. openPurchase ""
|
||||
report "cancelled" supporterPlay
|
||||
alice <## "ok"
|
||||
|
||||
Reference in new issue
Block a user