Skip to main content

syrup_rail_postgres/subscription_billing_service/
subscriber_mutation.rs

1use super::*;
2
3const BILLING_LOCK_TIMEOUT: Duration = Duration::from_millis(250);
4
5impl SubscriptionBillingService {
6    /// Cancels one exact subscriber-owned subscription lifecycle.
7    ///
8    /// Admission runs before any database work. When cancellation changes
9    /// canonical state, its event is appended on the same host-prepared
10    /// transaction before that transaction commits. Replays and semantic
11    /// blockers commit without an event. This operation performs no provider
12    /// resolution or provider I/O.
13    pub async fn cancel(
14        &self,
15        command: CancelSubscription,
16    ) -> Result<CancelSubscriptionOutcome, SubscriptionBillingServiceError> {
17        self.admit_subscriber_mutation(
18            command.billing_scope_id(),
19            command.subscriber_id(),
20            EndUserMutationOperation::SubscriptionCancel,
21        )
22        .await?;
23
24        let mut transaction = self
25            .coordinator
26            .begin(
27                BillingEventSubject::new(command.billing_scope_id(), command.subscriber_id()),
28                BILLING_LOCK_TIMEOUT,
29            )
30            .await?;
31        let outcome = match crate::cancellation::cancel_subscription_on_connection(
32            transaction.connection(),
33            &command,
34        )
35        .await
36        {
37            Ok(outcome) => outcome,
38            Err(error) => {
39                let _ = transaction.rollback().await;
40                return Err(error.into());
41            }
42        };
43        if let CancelSubscriptionOutcome::Canceled { event, .. } = &outcome
44            && let Err(error) = transaction.append_event(event).await
45        {
46            let _ = transaction.rollback().await;
47            return Err(error.into());
48        }
49        transaction.commit().await?;
50        Ok(outcome)
51    }
52
53    /// Claims an eligible discount code for one exact subscriber aggregate.
54    ///
55    /// Admission happens before database work and the offer lock, claim, and
56    /// commit use one local transaction. The operation does not resolve a
57    /// gateway or perform provider I/O.
58    pub async fn claim_discount(
59        &self,
60        command: SubscriptionDiscountClaim,
61    ) -> Result<SubscriptionDiscountClaimOutcome, SubscriptionBillingServiceError> {
62        self.admit_subscriber_mutation(
63            command.billing_scope_id(),
64            command.subscriber_id(),
65            EndUserMutationOperation::SubscriptionDiscountClaim,
66        )
67        .await?;
68
69        let mut transaction = self
70            .pool
71            .begin()
72            .await
73            .map_err(provider_free_transaction_error)?;
74        let outcome = match crate::discounts::claim_subscription_discount_on_connection(
75            &mut transaction,
76            self.offers.as_ref(),
77            &command,
78        )
79        .await
80        {
81            Ok(outcome) => outcome,
82            Err(error) => {
83                let _ = transaction.rollback().await;
84                return Err(error.into());
85            }
86        };
87        transaction
88            .commit()
89            .await
90            .map_err(provider_free_transaction_error)?;
91        Ok(outcome)
92    }
93
94    /// Clears the saved discount claim for one exact subscriber aggregate.
95    ///
96    /// Admission happens before database work. The command has no provider
97    /// identity and this operation performs neither gateway resolution nor
98    /// provider I/O.
99    pub async fn clear_discount(
100        &self,
101        command: ClearSubscriptionDiscount,
102    ) -> Result<SubscriptionDiscountClearOutcome, SubscriptionBillingServiceError> {
103        self.admit_subscriber_mutation(
104            command.billing_scope_id(),
105            command.subscriber_id(),
106            EndUserMutationOperation::SubscriptionDiscountClear,
107        )
108        .await?;
109
110        let mut transaction = self
111            .pool
112            .begin()
113            .await
114            .map_err(provider_free_transaction_error)?;
115        let outcome = match crate::discounts::clear_subscription_discount_on_connection(
116            &mut transaction,
117            command.billing_scope_id(),
118            command.subscriber_id(),
119            command.plan_key(),
120        )
121        .await
122        {
123            Ok(outcome) => outcome,
124            Err(error) => {
125                let _ = transaction.rollback().await;
126                return Err(error.into());
127            }
128        };
129        transaction
130            .commit()
131            .await
132            .map_err(provider_free_transaction_error)?;
133        Ok(outcome)
134    }
135}
136
137#[cfg(test)]
138mod tests;