Skip to main content

syrup_rail_postgres/subscription_billing_service/
payment_method_replacement.rs

1use super::*;
2
3impl SubscriptionBillingService {
4    /// Runs one complete stored payment-method replacement boundary.
5    pub async fn replace_payment_method(
6        &self,
7        command: ReplaceSubscriptionPaymentMethod,
8    ) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionBillingServiceError> {
9        let prepared_attempt = match self.preflight_payment_method_replacement(&command).await? {
10            SubscriptionPaymentMethodReplacementPreflightOutcome::Continue => None,
11            SubscriptionPaymentMethodReplacementPreflightOutcome::Replay(attempt)
12                if attempt_is_prepared(&attempt) =>
13            {
14                Some(*attempt)
15            }
16            SubscriptionPaymentMethodReplacementPreflightOutcome::Replay(attempt) => {
17                return self.payment_result(*attempt).await;
18            }
19            SubscriptionPaymentMethodReplacementPreflightOutcome::IdempotencyConflict => {
20                return Err(SubscriptionBillingServiceError::IdempotencyConflict);
21            }
22        };
23        if prepared_attempt.as_ref().is_some_and(|attempt| {
24            attempt.identity().required_gateway_account_mode() != self.required_gateway_account_mode
25        }) {
26            return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
27        }
28        self.admit_subscriber_mutation(
29            command.billing_scope_id(),
30            command.subscriber_id(),
31            EndUserMutationOperation::SubscriptionPaymentMethodUpdate,
32        )
33        .await?;
34        let (account, gateway) = self
35            .resolve_active_gateway(
36                command.billing_scope_id(),
37                command.gateway_configuration_id(),
38            )
39            .await?;
40        let (reservation, attempt) = if let Some(attempt) = prepared_attempt {
41            payment_method_replacement_from_prepared_attempt(attempt, &gateway)?
42        } else {
43            match self
44                .reserve_payment_method_replacement(&command, &gateway)
45                .await?
46            {
47                SubscriptionPaymentMethodReplacementReservationOutcome::Reserved(
48                    reservation,
49                    attempt,
50                ) => (*reservation, *attempt),
51                SubscriptionPaymentMethodReplacementReservationOutcome::Replay(attempt)
52                    if attempt_is_prepared(&attempt) =>
53                {
54                    payment_method_replacement_from_prepared_attempt(*attempt, &gateway)?
55                }
56                SubscriptionPaymentMethodReplacementReservationOutcome::Replay(attempt) => {
57                    return self.payment_result(*attempt).await;
58                }
59                SubscriptionPaymentMethodReplacementReservationOutcome::IdempotencyConflict => {
60                    return Err(SubscriptionBillingServiceError::IdempotencyConflict);
61                }
62                SubscriptionPaymentMethodReplacementReservationOutcome::Rejected(
63                    SubscriptionPaymentMethodReplacementRejection::GatewayAccountModeChanged,
64                ) => {
65                    return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
66                }
67                SubscriptionPaymentMethodReplacementReservationOutcome::Rejected(reason) => {
68                    return Err(
69                        SubscriptionBillingServiceError::PaymentMethodReplacementReservationRejected(
70                            reason,
71                        ),
72                    );
73                }
74            }
75        };
76        if attempt.status() != PaymentAttemptStatus::Pending
77            || attempt.state().timestamps().submitted_at().is_some()
78            || attempt.identity() != reservation.identity()
79        {
80            return Err(SubscriptionBillingServiceError::InvalidState(
81                INVALID_SERVICE_STATE,
82            ));
83        }
84        if reservation.identity().required_gateway_account_mode()
85            != self.required_gateway_account_mode
86        {
87            return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
88        }
89        if let Some(scope) = self.active_cooldown(&account).await? {
90            return self
91                .resolve_subscriber_readiness_failure(
92                    SubscriberInitiatedReservation::PaymentMethodReplacement(&reservation),
93                    SubscriberReadinessFailure::Cooldown(scope),
94                    OutcomeResolutionBoundary::Prepared,
95                )
96                .await;
97        }
98        let verified_gateway = match subscriber_gateway_readiness(
99            &gateway,
100            self.required_gateway_account_mode,
101        )
102        .await
103        {
104            Ok(verified_gateway) => verified_gateway,
105            Err(failure) => {
106                return self
107                    .resolve_subscriber_readiness_failure(
108                        SubscriberInitiatedReservation::PaymentMethodReplacement(&reservation),
109                        failure,
110                        OutcomeResolutionBoundary::Prepared,
111                    )
112                    .await;
113            }
114        };
115        let admission =
116            match admit_subscription_payment_method_replacement(&self.pool, &reservation).await? {
117                SubscriptionPaymentMethodReplacementAdmissionOutcome::Admitted(admission) => {
118                    *admission
119                }
120                SubscriptionPaymentMethodReplacementAdmissionOutcome::AlreadyAdmitted(attempt) => {
121                    return self.payment_result(attempt).await;
122                }
123                SubscriptionPaymentMethodReplacementAdmissionOutcome::Rejected {
124                    attempt, ..
125                } => {
126                    return self.payment_result(attempt).await;
127                }
128            };
129        // Admission can race with a cooldown observed by another request;
130        // recheck before the capability performs provider I/O.
131        if let Some(scope) = self.active_cooldown(&account).await? {
132            return self
133                .resolve_subscriber_readiness_failure(
134                    SubscriberInitiatedReservation::PaymentMethodReplacement(&reservation),
135                    SubscriberReadinessFailure::Cooldown(scope),
136                    OutcomeResolutionBoundary::AdmittedNotSubmitted,
137                )
138                .await;
139        }
140        match submit_admitted_subscription_payment_method_replacement(
141            &self.pool,
142            self.coordinator.as_ref(),
143            admission,
144            &command,
145            verified_gateway,
146        )
147        .await?
148        {
149            SubscriptionPaymentMethodReplacementProviderResult::Payment(payment) => Ok(payment),
150            SubscriptionPaymentMethodReplacementProviderResult::NotSubmitted { error, .. } => {
151                Err(SubscriptionBillingServiceError::GatewayNotSubmitted(error))
152            }
153        }
154    }
155
156    pub(super) async fn preflight_payment_method_replacement(
157        &self,
158        command: &ReplaceSubscriptionPaymentMethod,
159    ) -> Result<SubscriptionPaymentMethodReplacementPreflightOutcome, SubscriptionBillingServiceError>
160    {
161        let mut transaction = self.pool.begin().await?;
162        let outcome = preflight_subscription_payment_method_replacement_in_transaction(
163            &mut transaction,
164            command,
165        )
166        .await?;
167        transaction.commit().await?;
168        Ok(outcome)
169    }
170
171    pub(super) async fn reserve_payment_method_replacement(
172        &self,
173        command: &ReplaceSubscriptionPaymentMethod,
174        gateway: &syrup_rail::ResolvedGateway,
175    ) -> Result<
176        SubscriptionPaymentMethodReplacementReservationOutcome,
177        SubscriptionBillingServiceError,
178    > {
179        let mut transaction = self.pool.begin().await?;
180        let outcome = reserve_subscription_payment_method_replacement_in_transaction(
181            &mut transaction,
182            command,
183            gateway,
184            self.required_gateway_account_mode,
185        )
186        .await?;
187        transaction.commit().await?;
188        Ok(outcome)
189    }
190}
191
192fn payment_method_replacement_from_prepared_attempt(
193    attempt: PaymentAttempt,
194    gateway: &syrup_rail::ResolvedGateway,
195) -> Result<(SubscriptionPaymentMethodReplacement, PaymentAttempt), SubscriptionBillingServiceError>
196{
197    if !attempt_is_prepared(&attempt) {
198        return Err(SubscriptionBillingServiceError::InvalidState(
199            INVALID_SERVICE_STATE,
200        ));
201    }
202    if !resolved_gateway_matches_attempt(gateway, &attempt) {
203        return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
204    }
205    let reservation = SubscriptionPaymentMethodReplacement::from_attempt(
206        &attempt,
207        gateway.provider_key().clone(),
208    )
209    .map_err(|_| SubscriptionBillingServiceError::InvalidState(INVALID_SERVICE_STATE))?;
210    Ok((reservation, attempt))
211}