Skip to main content

syrup_rail_postgres/subscription_billing_service/
enrollment.rs

1use super::*;
2
3impl SubscriptionBillingService {
4    /// Runs one complete initial-subscription payment boundary.
5    ///
6    /// Matching replay and conflict are resolved before host admission. No
7    /// database transaction or lock is held across host admission, gateway
8    /// resolution, readiness I/O, or the one provider mutation.
9    pub async fn enroll(
10        &self,
11        command: EnrollSubscription,
12    ) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionBillingServiceError> {
13        match self.preflight(&command).await? {
14            SubscriptionEnrollmentPreflightOutcome::Continue => {}
15            SubscriptionEnrollmentPreflightOutcome::Replay(attempt) => {
16                return self.payment_result(*attempt).await;
17            }
18            SubscriptionEnrollmentPreflightOutcome::IdempotencyConflict => {
19                return Err(SubscriptionBillingServiceError::IdempotencyConflict);
20            }
21        }
22
23        self.admit_subscriber_mutation(
24            command.billing_scope_id(),
25            command.subscriber_id(),
26            EndUserMutationOperation::SubscriptionInitial,
27        )
28        .await?;
29
30        let (account, gateway) = self
31            .resolve_active_gateway(
32                command.billing_scope_id(),
33                command.gateway_configuration_id(),
34            )
35            .await?;
36        let mut reservation = SubscriptionEnrollmentReservation::from_command(
37            &command,
38            &gateway,
39            self.required_gateway_account_mode,
40        )
41        .map_err(map_reservation_build_error)?;
42
43        let attempt = match self.reserve(&reservation).await? {
44            SubscriptionEnrollmentReservationOutcome::Reserved(attempt)
45            | SubscriptionEnrollmentReservationOutcome::Replay(attempt)
46                if attempt.status() == PaymentAttemptStatus::Pending
47                    && attempt.state().timestamps().submitted_at().is_none() =>
48            {
49                attempt
50            }
51            SubscriptionEnrollmentReservationOutcome::Replay(attempt) => {
52                return self.payment_result(attempt).await;
53            }
54            SubscriptionEnrollmentReservationOutcome::IdempotencyConflict => {
55                return Err(SubscriptionBillingServiceError::IdempotencyConflict);
56            }
57            SubscriptionEnrollmentReservationOutcome::Rejected(
58                SubscriptionEnrollmentReservationRejection::GatewayAccountModeChanged,
59            ) => {
60                return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
61            }
62            SubscriptionEnrollmentReservationOutcome::Rejected(reason) => {
63                return Err(SubscriptionBillingServiceError::ReservationRejected(reason));
64            }
65            SubscriptionEnrollmentReservationOutcome::Reserved(_) => {
66                return Err(SubscriptionBillingServiceError::InvalidState(
67                    INVALID_SERVICE_STATE,
68                ));
69            }
70        };
71        if reservation.identity().attempt_id() != attempt.identity().attempt_id() {
72            reservation = SubscriptionEnrollmentReservation::from_command_for_attempt(
73                &command,
74                &gateway,
75                attempt.identity().attempt_id(),
76                self.required_gateway_account_mode,
77            )
78            .map_err(map_reservation_build_error)?;
79        }
80
81        if let Some(scope) = self.active_cooldown(&account).await? {
82            return self
83                .resolve_subscriber_readiness_failure(
84                    SubscriberInitiatedReservation::Initial(&reservation),
85                    SubscriberReadinessFailure::Cooldown(scope),
86                    OutcomeResolutionBoundary::Prepared,
87                )
88                .await;
89        }
90        let verified_gateway = match subscriber_gateway_readiness(
91            &gateway,
92            self.required_gateway_account_mode,
93        )
94        .await
95        {
96            Ok(verified_gateway) => verified_gateway,
97            Err(failure) => {
98                return self
99                    .resolve_subscriber_readiness_failure(
100                        SubscriberInitiatedReservation::Initial(&reservation),
101                        failure,
102                        OutcomeResolutionBoundary::Prepared,
103                    )
104                    .await;
105            }
106        };
107
108        let admission = match admit_subscription_enrollment_submission(
109            &self.pool,
110            self.offers.as_ref(),
111            &reservation,
112        )
113        .await?
114        {
115            SubscriptionEnrollmentAdmissionOutcome::Admitted(admission) => *admission,
116            SubscriptionEnrollmentAdmissionOutcome::AlreadyAdmitted(attempt) => {
117                return self.payment_result(attempt).await;
118            }
119            SubscriptionEnrollmentAdmissionOutcome::Rejected {
120                reason: SubscriptionEnrollmentSubmissionRejection::GatewayAccountModeChanged,
121                ..
122            } => {
123                return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
124            }
125            SubscriptionEnrollmentAdmissionOutcome::Rejected { reason, .. } => {
126                return Err(SubscriptionBillingServiceError::SubmissionRejected(reason));
127            }
128        };
129
130        if let Some(scope) = self.active_cooldown(&account).await? {
131            return self
132                .resolve_subscriber_readiness_failure(
133                    SubscriberInitiatedReservation::Initial(&reservation),
134                    SubscriberReadinessFailure::Cooldown(scope),
135                    OutcomeResolutionBoundary::AdmittedNotSubmitted,
136                )
137                .await;
138        }
139        match submit_admitted_subscription_enrollment(
140            &self.pool,
141            self.coordinator.as_ref(),
142            admission,
143            &command,
144            verified_gateway,
145        )
146        .await?
147        {
148            SubscriptionEnrollmentProviderResult::Payment(payment) => Ok(payment),
149            SubscriptionEnrollmentProviderResult::NotSubmitted { error, .. } => {
150                Err(SubscriptionBillingServiceError::GatewayNotSubmitted(error))
151            }
152        }
153    }
154
155    pub(super) async fn preflight(
156        &self,
157        command: &EnrollSubscription,
158    ) -> Result<SubscriptionEnrollmentPreflightOutcome, SubscriptionBillingServiceError> {
159        let mut transaction = self.pool.begin().await?;
160        let outcome =
161            preflight_subscription_enrollment_in_transaction(&mut transaction, command).await?;
162        transaction.commit().await?;
163        Ok(outcome)
164    }
165
166    pub(super) async fn reserve(
167        &self,
168        reservation: &SubscriptionEnrollmentReservation,
169    ) -> Result<SubscriptionEnrollmentReservationOutcome, SubscriptionBillingServiceError> {
170        let mut transaction = self.pool.begin().await?;
171        let outcome = reserve_subscription_enrollment_in_transaction(
172            &mut transaction,
173            self.offers.as_ref(),
174            reservation,
175        )
176        .await?;
177        transaction.commit().await?;
178        Ok(outcome)
179    }
180
181    pub(super) async fn payment_result(
182        &self,
183        attempt: PaymentAttempt,
184    ) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionBillingServiceError> {
185        let mut transaction = self.pool.begin().await?;
186        let result = payment_result_for_attempt(&mut transaction, attempt).await?;
187        transaction.commit().await?;
188        Ok(result)
189    }
190}