syrup_rail_postgres/subscription_billing_service/
payment_method_replacement.rs1use super::*;
2
3impl SubscriptionBillingService {
4 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 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}