1use std::fmt;
2
3use sqlx::{PgConnection, PgPool};
4use syrup_rail::{
5 ApprovedProcessorEvidence, BillingEvent, BillingEventSubject, BillingScopeId,
6 GatewayMutationError, GatewayNotSubmittedError, GatewayPaymentOutcome, GatewayPaymentStatus,
7 GatewayProviderKey, GatewaySaleIntent, GatewaySaleRequest, PaymentAttempt, PaymentAttemptId,
8 PaymentAttemptStatus, PaymentResolutionCode, ProcessorChargeProgression, ProcessorChargeRole,
9 ProcessorEvidence, SubscriptionEnrollmentPaymentResult, SubscriptionRenewalReservation,
10 SubscriptionRenewalSubmissionOutcome, SubscriptionRenewalSubmissionRejection,
11};
12
13use crate::{
14 BillingTransactionCoordinator, BillingTransactionSubjectState,
15 attempts::find_payment_attempt_by_id_on_connection,
16 processor_charges::{ObservedCharge, observe_processor_charge, transition_charge},
17 renewal_failure::{RenewalFailureApplication, apply_resolved_automatic_renewal_failure},
18};
19
20use super::{
21 APPROVED_APPLICATION_ATTEMPTS, APPROVED_EVIDENCE_RETRY_DELAY, APPROVED_EVIDENCE_WRITE_ATTEMPTS,
22 AttemptResolutionStatus, AttemptTransition, BILLING_LOCK_TIMEOUT, GatewayNotSubmittedPolicy,
23 INVALID_APPLICATION_STATE, OutcomeApplication, OutcomeReservation, OutcomeResolutionBoundary,
24 OutcomeResolutionCommand, PreparedAttemptReplay, RENEWAL_APPROVED_STORAGE_FAILURE_TEXT,
25 RENEWAL_INCOMPLETE_APPROVAL_TEXT, RENEWAL_STALE_STATE_TEXT, RateLimitCooldown,
26 SubscriptionEnrollmentApplicationError, advance_subscription_discount_after_successful_charge,
27 clear_resolved_attempt_submission, commit_rate_limit_cooldown, finalize_approved_application,
28 is_retryable_evidence_error, load_applied_subscription, load_subscription,
29 lock_expected_reservation_attempt, lock_payment_method_domain, lock_subscription_aggregate,
30 map_attempt_transition_error, mark_attempt_approved, mutation_error_evidence,
31 park_locked_attempt, payment_result_for_attempt, payment_result_for_reservation_attempt,
32 persist_attempt_transition, renewal_subscription_matches, resolve_pool_outcome,
33 set_application_timeouts,
34};
35
36pub struct AdmittedSubscriptionRenewal {
39 reservation: SubscriptionRenewalReservation,
40 attempt: PaymentAttempt,
41 payment_method_reference: syrup_rail::GatewayPaymentMethodReference,
42}
43
44impl AdmittedSubscriptionRenewal {
45 pub const fn attempt(&self) -> &PaymentAttempt {
46 &self.attempt
47 }
48}
49
50impl fmt::Debug for AdmittedSubscriptionRenewal {
51 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52 formatter
53 .debug_struct("AdmittedSubscriptionRenewal")
54 .field("attempt", &self.attempt)
55 .field("has_submission_authority", &true)
56 .field("has_payment_method_reference", &true)
57 .finish()
58 }
59}
60
61#[derive(Debug)]
62pub enum SubscriptionRenewalAdmissionOutcome {
63 Admitted(Box<AdmittedSubscriptionRenewal>),
64 AlreadyAdmitted(PaymentAttempt),
65 Rejected {
66 attempt: PaymentAttempt,
67 reason: SubscriptionRenewalSubmissionRejection,
68 },
69}
70
71#[derive(Debug)]
72pub enum SubscriptionRenewalProviderResult {
73 Payment(SubscriptionEnrollmentPaymentResult),
74 NotSubmitted {
78 payment: SubscriptionEnrollmentPaymentResult,
79 error: GatewayNotSubmittedError,
80 },
81}
82
83pub async fn admit_subscription_renewal_submission(
85 pool: &PgPool,
86 reservation: &SubscriptionRenewalReservation,
87) -> Result<SubscriptionRenewalAdmissionOutcome, SubscriptionEnrollmentApplicationError> {
88 let mut transaction = pool.begin().await?;
89 let outcome =
90 crate::admit_subscription_renewal_submission_in_transaction(&mut transaction, reservation)
91 .await?;
92 let outcome = match outcome {
93 SubscriptionRenewalSubmissionOutcome::Admitted(attempt) => {
94 let identity = reservation.identity();
95 let reference = sqlx::query_scalar::<_, String>(
96 r#"
97 SELECT gateway_payment_method_reference
98 FROM billing_payment_methods
99 WHERE id = $1 AND billing_scope_id = $2 AND subscriber_id = $3
100 AND gateway_account_id = $4 AND status = 'active'
101 FOR SHARE
102 "#,
103 )
104 .bind(reservation.expected_state().payment_method_id().as_uuid())
105 .bind(identity.billing_scope_id().as_uuid())
106 .bind(identity.subscriber_id().as_uuid())
107 .bind(identity.gateway_account_id().as_uuid())
108 .fetch_optional(&mut *transaction)
109 .await?
110 .ok_or(SubscriptionEnrollmentApplicationError::InvalidState(
111 INVALID_APPLICATION_STATE,
112 ))?;
113 let payment_method_reference =
114 syrup_rail::GatewayPaymentMethodReference::new(reference).map_err(|_| {
115 SubscriptionEnrollmentApplicationError::InvalidState(INVALID_APPLICATION_STATE)
116 })?;
117 SubscriptionRenewalAdmissionOutcome::Admitted(Box::new(AdmittedSubscriptionRenewal {
118 reservation: reservation.clone(),
119 attempt,
120 payment_method_reference,
121 }))
122 }
123 SubscriptionRenewalSubmissionOutcome::AlreadyAdmitted(attempt) => {
124 SubscriptionRenewalAdmissionOutcome::AlreadyAdmitted(attempt)
125 }
126 SubscriptionRenewalSubmissionOutcome::Rejected { attempt, reason } => {
127 SubscriptionRenewalAdmissionOutcome::Rejected { attempt, reason }
128 }
129 };
130 transaction.commit().await?;
131 Ok(outcome)
132}
133
134pub async fn submit_admitted_subscription_renewal(
136 pool: &PgPool,
137 coordinator: &dyn BillingTransactionCoordinator,
138 admission: AdmittedSubscriptionRenewal,
139 gateway: crate::ModeVerifiedGateway<'_>,
140) -> Result<SubscriptionRenewalProviderResult, SubscriptionEnrollmentApplicationError> {
141 let identity = admission.reservation.identity();
142 let resolved_gateway = gateway.resolved_gateway();
143 if admission.attempt.identity() != identity
144 || admission.attempt.request() != admission.reservation.request()
145 || admission.attempt.status() != PaymentAttemptStatus::Pending
146 || admission
147 .attempt
148 .state()
149 .timestamps()
150 .submitted_at()
151 .is_none()
152 || resolved_gateway.provider_key() != admission.reservation.provider_key()
153 {
154 return Err(SubscriptionEnrollmentApplicationError::SubmissionIdentityMismatch);
155 }
156 let Some(gateway) = gateway.authorize_attempt(&identity) else {
157 return Err(SubscriptionEnrollmentApplicationError::SubmissionIdentityMismatch);
158 };
159 let charge =
160 syrup_rail::ChargeAmount::try_from(admission.attempt.request().amount()).map_err(|_| {
161 SubscriptionEnrollmentApplicationError::InvalidState(INVALID_APPLICATION_STATE)
162 })?;
163 let request = GatewaySaleRequest::new(
164 charge,
165 admission.attempt.request().gateway_order_id().clone(),
166 GatewaySaleIntent::RecurringStoredCredential {
167 payment_method_reference: admission.payment_method_reference,
168 initial_transaction_id: admission
169 .reservation
170 .expected_state()
171 .initial_transaction_id()
172 .clone(),
173 },
174 None,
175 );
176 match gateway.sale(request).await {
177 Ok(outcome) => apply_subscription_renewal_gateway_outcome(
178 pool,
179 coordinator,
180 &admission.reservation,
181 &outcome,
182 )
183 .await
184 .map(SubscriptionRenewalProviderResult::Payment),
185 Err(GatewayMutationError::NotSubmitted(error)) => {
186 let evidence = mutation_error_evidence(error.detail());
187 let policy = GatewayNotSubmittedPolicy::for_error(&error);
188 let application = resolve_renewal_non_approved_outcome(
189 pool,
190 coordinator,
191 &admission.reservation,
192 &evidence,
193 OutcomeResolutionCommand::non_approved(
194 AttemptResolutionStatus::Failed,
195 Some(policy.resolution_code()),
196 policy.cooldown(),
197 OutcomeResolutionBoundary::AdmittedNotSubmitted,
198 ),
199 )
200 .await?;
201 if application.should_surface_not_submitted(policy) {
202 Ok(SubscriptionRenewalProviderResult::NotSubmitted {
203 payment: application.payment,
204 error,
205 })
206 } else {
207 Ok(SubscriptionRenewalProviderResult::Payment(
208 application.payment,
209 ))
210 }
211 }
212 Err(GatewayMutationError::RateLimitedIndeterminate(detail)) => {
213 resolve_renewal_unknown_outcome(
214 pool,
215 &admission.reservation,
216 &mutation_error_evidence(&detail),
217 Some(RateLimitCooldown::Provider),
218 )
219 .await
220 .map(SubscriptionRenewalProviderResult::Payment)
221 }
222 Err(GatewayMutationError::Indeterminate(detail)) => resolve_renewal_unknown_outcome(
223 pool,
224 &admission.reservation,
225 &mutation_error_evidence(&detail),
226 None,
227 )
228 .await
229 .map(SubscriptionRenewalProviderResult::Payment),
230 }
231}
232
233pub async fn apply_subscription_renewal_gateway_outcome(
234 pool: &PgPool,
235 coordinator: &dyn BillingTransactionCoordinator,
236 reservation: &SubscriptionRenewalReservation,
237 outcome: &GatewayPaymentOutcome,
238) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionEnrollmentApplicationError> {
239 apply_subscription_renewal_gateway_decision(pool, coordinator, reservation, outcome)
240 .await
241 .map(|result| result.with_gateway_diagnostics(outcome.diagnostics().to_vec()))
242}
243
244async fn apply_subscription_renewal_gateway_decision(
245 pool: &PgPool,
246 coordinator: &dyn BillingTransactionCoordinator,
247 reservation: &SubscriptionRenewalReservation,
248 outcome: &GatewayPaymentOutcome,
249) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionEnrollmentApplicationError> {
250 match outcome.status() {
251 GatewayPaymentStatus::Approved => {
252 let approved_evidence = outcome.approved_evidence().ok_or(
253 SubscriptionEnrollmentApplicationError::InvalidState(INVALID_APPLICATION_STATE),
254 )?;
255 if outcome.transaction_id().is_none() {
256 return park_renewal_approved_outcome(
257 pool,
258 reservation,
259 &approved_evidence,
260 RENEWAL_INCOMPLETE_APPROVAL_TEXT,
261 )
262 .await;
263 }
264 for attempt_index in 0..APPROVED_APPLICATION_ATTEMPTS {
265 match apply_renewal_approved_outcome(coordinator, reservation, &approved_evidence)
266 .await
267 {
268 Ok(result) => return Ok(result),
269 Err(_) if attempt_index + 1 < APPROVED_APPLICATION_ATTEMPTS => {
270 tokio::time::sleep(APPROVED_EVIDENCE_RETRY_DELAY).await;
271 }
272 Err(_) => break,
273 }
274 }
275 park_renewal_approved_outcome(
276 pool,
277 reservation,
278 &approved_evidence,
279 RENEWAL_APPROVED_STORAGE_FAILURE_TEXT,
280 )
281 .await
282 }
283 GatewayPaymentStatus::Declined => resolve_renewal_non_approved_outcome(
284 pool,
285 coordinator,
286 reservation,
287 outcome.evidence(),
288 OutcomeResolutionCommand::non_approved(
289 AttemptResolutionStatus::Declined,
290 None,
291 None,
292 OutcomeResolutionBoundary::Submitted,
293 ),
294 )
295 .await
296 .map(OutcomeApplication::into_payment),
297 GatewayPaymentStatus::Failed => resolve_renewal_non_approved_outcome(
298 pool,
299 coordinator,
300 reservation,
301 outcome.evidence(),
302 OutcomeResolutionCommand::non_approved(
303 AttemptResolutionStatus::Failed,
304 None,
305 None,
306 OutcomeResolutionBoundary::Submitted,
307 ),
308 )
309 .await
310 .map(OutcomeApplication::into_payment),
311 GatewayPaymentStatus::Unknown => {
312 resolve_renewal_unknown_outcome(pool, reservation, outcome.evidence(), None).await
313 }
314 }
315}
316
317pub async fn apply_reconciled_subscription_renewal_gateway_outcome(
318 pool: &PgPool,
319 coordinator: &dyn BillingTransactionCoordinator,
320 billing_scope_id: BillingScopeId,
321 attempt_id: PaymentAttemptId,
322 outcome: &GatewayPaymentOutcome,
323) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionEnrollmentApplicationError> {
324 let mut transaction = pool.begin().await?;
325 let attempt = crate::find_payment_attempt_by_id_in_transaction(
326 &mut transaction,
327 billing_scope_id,
328 attempt_id,
329 )
330 .await?
331 .ok_or(SubscriptionEnrollmentApplicationError::InvalidState(
332 "subscription renewal attempt was not found",
333 ))?;
334 let provider_key = sqlx::query_scalar::<_, String>(
335 "SELECT provider_key FROM billing_gateway_accounts WHERE billing_scope_id = $1 AND id = $2",
336 )
337 .bind(billing_scope_id.as_uuid())
338 .bind(attempt.identity().gateway_account_id().as_uuid())
339 .fetch_optional(&mut *transaction)
340 .await?
341 .ok_or(SubscriptionEnrollmentApplicationError::InvalidState(
342 "subscription renewal gateway account was not found",
343 ))?;
344 transaction.commit().await?;
345 let provider_key = GatewayProviderKey::new(provider_key).map_err(|_| {
346 SubscriptionEnrollmentApplicationError::InvalidState(
347 "subscription renewal gateway provider key is invalid",
348 )
349 })?;
350 let reservation = SubscriptionRenewalReservation::from_attempt(&attempt, provider_key)
351 .map_err(|_| {
352 SubscriptionEnrollmentApplicationError::InvalidState(
353 "reconciled attempt is not a valid subscription renewal",
354 )
355 })?;
356 apply_subscription_renewal_gateway_outcome(pool, coordinator, &reservation, outcome).await
357}
358
359async fn apply_renewal_approved_outcome(
360 coordinator: &dyn BillingTransactionCoordinator,
361 reservation: &SubscriptionRenewalReservation,
362 approved_evidence: &ApprovedProcessorEvidence,
363) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionEnrollmentApplicationError> {
364 let identity = reservation.identity();
365 let mut transaction = coordinator
366 .begin(
367 BillingEventSubject::new(identity.billing_scope_id(), identity.subscriber_id()),
368 BILLING_LOCK_TIMEOUT,
369 )
370 .await?;
371 let subject_state = transaction.subject_state();
372 let application = apply_renewal_approved_on_connection(
373 transaction.connection(),
374 subject_state,
375 reservation,
376 approved_evidence,
377 )
378 .await;
379 finalize_approved_application(transaction, application).await
380}
381
382async fn apply_renewal_approved_on_connection(
383 connection: &mut PgConnection,
384 subject_state: BillingTransactionSubjectState,
385 reservation: &SubscriptionRenewalReservation,
386 approved_evidence: &ApprovedProcessorEvidence,
387) -> Result<
388 (SubscriptionEnrollmentPaymentResult, Option<BillingEvent>),
389 SubscriptionEnrollmentApplicationError,
390> {
391 let evidence = approved_evidence.evidence();
392 set_application_timeouts(connection).await?;
393 let identity = reservation.identity();
394 lock_payment_method_domain(
395 connection,
396 identity.subscriber_id(),
397 identity.gateway_account_id().as_uuid(),
398 )
399 .await?;
400 lock_subscription_aggregate(connection, identity.subscriber_id(), reservation.plan_key())
401 .await?;
402 let attempt =
403 lock_expected_reservation_attempt(connection, OutcomeReservation::Renewal(reservation))
404 .await?;
405 if attempt.status() == PaymentAttemptStatus::Approved {
406 let subscription = load_applied_subscription(connection, &attempt)
407 .await?
408 .ok_or(SubscriptionEnrollmentApplicationError::InvalidState(
409 INVALID_APPLICATION_STATE,
410 ))?;
411 observe_processor_charge(
412 connection,
413 &attempt,
414 evidence,
415 ProcessorChargeProgression::Applied,
416 )
417 .await?;
418 return Ok((
419 SubscriptionEnrollmentPaymentResult::applied(attempt, subscription)?,
420 None,
421 ));
422 }
423 if subject_state != BillingTransactionSubjectState::LiveRecipient {
424 return Err(SubscriptionEnrollmentApplicationError::InvalidState(
425 "a subscription renewal event requires a live recipient",
426 ));
427 }
428 if attempt.status().is_terminal() {
429 let observation = observe_processor_charge(
430 connection,
431 &attempt,
432 evidence,
433 ProcessorChargeProgression::ExternalReversalRequired,
434 )
435 .await?;
436 if let ObservedCharge::Owned(charge) = observation {
437 transition_charge(
438 connection,
439 charge.id,
440 ProcessorChargeProgression::ExternalReversalRequired,
441 None,
442 )
443 .await?;
444 }
445 return Ok((
446 SubscriptionEnrollmentPaymentResult::confirmation_pending(
447 attempt,
448 approved_evidence.clone(),
449 )?,
450 None,
451 ));
452 }
453 let observation = observe_processor_charge(
454 connection,
455 &attempt,
456 evidence,
457 ProcessorChargeProgression::Pending,
458 )
459 .await?;
460 let ObservedCharge::Owned(charge) = observation else {
461 let parked = park_locked_attempt(
462 connection,
463 &attempt,
464 evidence,
465 None,
466 "The approved gateway transaction is already owned by another payment attempt.",
467 )
468 .await?;
469 return Ok((
470 SubscriptionEnrollmentPaymentResult::not_applied(parked)?,
471 None,
472 ));
473 };
474 if charge.role == ProcessorChargeRole::Additional {
475 transition_charge(
476 connection,
477 charge.id,
478 ProcessorChargeProgression::ExternalReversalRequired,
479 None,
480 )
481 .await?;
482 let parked = park_locked_attempt(
483 connection,
484 &attempt,
485 evidence,
486 None,
487 "An additional approved charge requires manual reversal review.",
488 )
489 .await?;
490 return Ok((
491 SubscriptionEnrollmentPaymentResult::not_applied(parked)?,
492 None,
493 ));
494 }
495 if !renewal_subscription_matches(connection, reservation).await? {
496 transition_charge(
497 connection,
498 charge.id,
499 ProcessorChargeProgression::ExternalReversalRequired,
500 Some(PaymentResolutionCode::SubscriptionApprovedRenewalStaleState),
501 )
502 .await?;
503 let parked = park_locked_attempt(
504 connection,
505 &attempt,
506 evidence,
507 Some(PaymentResolutionCode::SubscriptionApprovedRenewalStaleState),
508 RENEWAL_STALE_STATE_TEXT,
509 )
510 .await?;
511 return Ok((
512 SubscriptionEnrollmentPaymentResult::not_applied(parked)?,
513 None,
514 ));
515 }
516 let expected = reservation.expected_state();
517 let updated = sqlx::query(
518 r#"
519 UPDATE billing_subscriptions
520 SET status = 'active', phase = 'recurring', current_period_start_at = $2,
521 current_period_end_at = $3, next_renewal_at = $3,
522 next_payment_attempt_at = $3,
523 updated_at = clock_timestamp()
524 WHERE id = $1 AND billing_scope_id = $4 AND subscriber_id = $5
525 AND gateway_account_id = $6 AND plan_key = $7
526 AND status = $8 AND status IN ('active', 'past_due')
527 AND payment_method_id = $9 AND initial_transaction_id = $10
528 AND amount_cents = $11 AND currency = $12
529 AND next_renewal_at = $2
530 "#,
531 )
532 .bind(reservation.subscription_id().as_uuid())
533 .bind(reservation.period().start_at())
534 .bind(reservation.period().end_at())
535 .bind(identity.billing_scope_id().as_uuid())
536 .bind(identity.subscriber_id().as_uuid())
537 .bind(identity.gateway_account_id().as_uuid())
538 .bind(reservation.plan_key().as_str())
539 .bind(expected.status().as_str())
540 .bind(expected.payment_method_id().as_uuid())
541 .bind(expected.initial_transaction_id().expose())
542 .bind(reservation.request().amount().cents())
543 .bind(reservation.request().amount().currency().as_str())
544 .execute(&mut *connection)
545 .await?;
546 if updated.rows_affected() != 1 {
547 return Err(SubscriptionEnrollmentApplicationError::InvalidState(
548 INVALID_APPLICATION_STATE,
549 ));
550 }
551 advance_subscription_discount_after_successful_charge(
552 connection,
553 reservation.subscription_id(),
554 reservation.plan_key(),
555 )
556 .await?;
557 mark_attempt_approved(
558 connection,
559 &attempt,
560 evidence,
561 reservation.subscription_id(),
562 expected.payment_method_id(),
563 )
564 .await?;
565 transition_charge(
566 connection,
567 charge.id,
568 ProcessorChargeProgression::Applied,
569 None,
570 )
571 .await?;
572 let attempt = find_payment_attempt_by_id_on_connection(
573 connection,
574 identity.billing_scope_id(),
575 identity.attempt_id(),
576 )
577 .await?
578 .ok_or(SubscriptionEnrollmentApplicationError::InvalidState(
579 INVALID_APPLICATION_STATE,
580 ))?;
581 let subscription = load_subscription(
582 connection,
583 identity.billing_scope_id(),
584 reservation.subscription_id(),
585 )
586 .await?
587 .ok_or(SubscriptionEnrollmentApplicationError::InvalidState(
588 INVALID_APPLICATION_STATE,
589 ))?;
590 let event = BillingEvent::SubscriptionRenewed {
591 attempt_id: identity.attempt_id(),
592 subscription_id: reservation.subscription_id(),
593 plan_key: reservation.plan_key().clone(),
594 charge: syrup_rail::ChargeAmount::try_from(attempt.request().amount()).map_err(|_| {
595 SubscriptionEnrollmentApplicationError::InvalidState(INVALID_APPLICATION_STATE)
596 })?,
597 period: reservation.period().clone(),
598 };
599 Ok((
600 SubscriptionEnrollmentPaymentResult::applied(attempt, subscription)?,
601 Some(event),
602 ))
603}
604
605pub(crate) async fn resolve_renewal_non_approved_outcome(
606 pool: &PgPool,
607 coordinator: &dyn BillingTransactionCoordinator,
608 reservation: &SubscriptionRenewalReservation,
609 evidence: &ProcessorEvidence,
610 resolution: OutcomeResolutionCommand,
611) -> Result<OutcomeApplication, SubscriptionEnrollmentApplicationError> {
612 let renewal = reservation;
613 let reservation = OutcomeReservation::Renewal(renewal);
614 let identity = reservation.identity();
615 if let Some(cooldown) = resolution.cooldown {
616 commit_rate_limit_cooldown(pool, reservation, cooldown).await?;
617 }
618 let mut transaction = coordinator
619 .begin(
620 BillingEventSubject::new(identity.billing_scope_id(), identity.subscriber_id()),
621 BILLING_LOCK_TIMEOUT,
622 )
623 .await?;
624 let result = async {
625 let connection = transaction.connection();
626 set_application_timeouts(connection).await?;
627 lock_subscription_aggregate(connection, identity.subscriber_id(), reservation.plan_key())
628 .await?;
629 let attempt = lock_expected_reservation_attempt(connection, reservation).await?;
630 let may_resolve = resolution.may_resolve(
631 attempt.status(),
632 attempt.state().timestamps().submitted_at().is_some(),
633 );
634 let mut events = Vec::new();
635 if may_resolve {
639 let status = resolution.resolved_status(reservation.operation(), attempt.status());
640 persist_attempt_transition(
641 connection,
642 &attempt,
643 evidence,
644 AttemptTransition::Resolved {
645 status,
646 resolution_code: resolution.resolution_code,
647 },
648 )
649 .await
650 .map_err(map_attempt_transition_error)?;
651 if resolution.clears_submitted_at() {
652 clear_resolved_attempt_submission(connection, &attempt).await?;
653 } else if resolution.marks_renewal_past_due(status)
654 && resolution.resolution_code.is_none()
655 {
656 let resolved_attempt = find_payment_attempt_by_id_on_connection(
657 connection,
658 identity.billing_scope_id(),
659 identity.attempt_id(),
660 )
661 .await?
662 .ok_or(SubscriptionEnrollmentApplicationError::InvalidState(
663 INVALID_APPLICATION_STATE,
664 ))?;
665 if let RenewalFailureApplication::Applied {
666 events: applied_events,
667 ..
668 } =
669 apply_resolved_automatic_renewal_failure(connection, &resolved_attempt).await?
670 {
671 events = applied_events;
672 }
673 }
674 }
675 let result = payment_result_for_reservation_attempt(connection, reservation).await?;
676 Ok::<_, SubscriptionEnrollmentApplicationError>((result, events, may_resolve))
677 }
678 .await;
679 let (result, events, applied) = match result {
680 Ok(result) => result,
681 Err(error) => {
682 let _ = transaction.rollback().await;
683 return Err(error);
684 }
685 };
686 for event in &events {
687 if let Err(error) = transaction.append_event(event).await {
688 let _ = transaction.rollback().await;
689 return Err(error.into());
690 }
691 }
692 transaction.commit().await?;
693 Ok(OutcomeApplication {
694 payment: result,
695 applied,
696 prepared_attempt_replay: PreparedAttemptReplay::Unsupported,
697 })
698}
699
700async fn resolve_renewal_unknown_outcome(
701 pool: &PgPool,
702 reservation: &SubscriptionRenewalReservation,
703 evidence: &ProcessorEvidence,
704 cooldown: Option<RateLimitCooldown>,
705) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionEnrollmentApplicationError> {
706 resolve_pool_outcome(
707 pool,
708 OutcomeReservation::Renewal(reservation),
709 evidence,
710 OutcomeResolutionCommand::unknown(cooldown),
711 )
712 .await
713 .map(OutcomeApplication::into_payment)
714}
715
716async fn park_renewal_approved_outcome(
717 pool: &PgPool,
718 reservation: &SubscriptionRenewalReservation,
719 approved_evidence: &ApprovedProcessorEvidence,
720 message: &'static str,
721) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionEnrollmentApplicationError> {
722 let evidence = approved_evidence.evidence();
723 match try_park_renewal_approved_outcome(pool, reservation, evidence, message).await {
724 Ok(result) => Ok(result),
725 Err(_) => {
726 observe_renewal_approved_evidence_with_retry(pool, reservation, evidence).await?;
727 let mut transaction = pool.begin().await?;
728 let attempt = find_payment_attempt_by_id_on_connection(
729 &mut transaction,
730 reservation.identity().billing_scope_id(),
731 reservation.identity().attempt_id(),
732 )
733 .await?
734 .ok_or(SubscriptionEnrollmentApplicationError::InvalidState(
735 INVALID_APPLICATION_STATE,
736 ))?;
737 let result = if attempt.status() == PaymentAttemptStatus::Approved {
738 payment_result_for_attempt(&mut transaction, attempt).await?
739 } else {
740 SubscriptionEnrollmentPaymentResult::confirmation_pending(
741 attempt,
742 approved_evidence.clone(),
743 )?
744 };
745 transaction.commit().await?;
746 Ok(result)
747 }
748 }
749}
750
751async fn try_park_renewal_approved_outcome(
752 pool: &PgPool,
753 reservation: &SubscriptionRenewalReservation,
754 evidence: &ProcessorEvidence,
755 message: &'static str,
756) -> Result<SubscriptionEnrollmentPaymentResult, SubscriptionEnrollmentApplicationError> {
757 let mut transaction = pool.begin().await?;
758 set_application_timeouts(&mut transaction).await?;
759 lock_subscription_aggregate(
760 &mut transaction,
761 reservation.identity().subscriber_id(),
762 reservation.plan_key(),
763 )
764 .await?;
765 let attempt = lock_expected_reservation_attempt(
766 &mut transaction,
767 OutcomeReservation::Renewal(reservation),
768 )
769 .await?;
770 let attempt = if attempt.status() == PaymentAttemptStatus::Approved {
771 observe_processor_charge(
772 &mut transaction,
773 &attempt,
774 evidence,
775 ProcessorChargeProgression::Applied,
776 )
777 .await?;
778 attempt
779 } else if attempt.status().is_terminal() {
780 let progression =
781 if evidence.transaction_id().is_some() && attempt.request().amount().cents() > 0 {
782 ProcessorChargeProgression::ExternalReversalRequired
783 } else {
784 ProcessorChargeProgression::ReconciliationRequired
785 };
786 observe_processor_charge(&mut transaction, &attempt, evidence, progression).await?;
787 attempt
788 } else {
789 observe_processor_charge(
790 &mut transaction,
791 &attempt,
792 evidence,
793 ProcessorChargeProgression::Pending,
794 )
795 .await?;
796 park_locked_attempt(&mut transaction, &attempt, evidence, None, message).await?
797 };
798 let result = payment_result_for_attempt(&mut transaction, attempt).await?;
799 transaction.commit().await?;
800 Ok(result)
801}
802
803async fn observe_renewal_approved_evidence_with_retry(
804 pool: &PgPool,
805 reservation: &SubscriptionRenewalReservation,
806 evidence: &ProcessorEvidence,
807) -> Result<(), SubscriptionEnrollmentApplicationError> {
808 for attempt_index in 0..APPROVED_EVIDENCE_WRITE_ATTEMPTS {
809 let result = async {
810 let mut transaction = pool.begin().await?;
811 set_application_timeouts(&mut transaction).await?;
812 let attempt = lock_expected_reservation_attempt(
813 &mut transaction,
814 OutcomeReservation::Renewal(reservation),
815 )
816 .await?;
817 observe_processor_charge(
818 &mut transaction,
819 &attempt,
820 evidence,
821 ProcessorChargeProgression::Pending,
822 )
823 .await?;
824 transaction.commit().await?;
825 Ok::<(), SubscriptionEnrollmentApplicationError>(())
826 }
827 .await;
828 match result {
829 Ok(()) => return Ok(()),
830 Err(error)
831 if is_retryable_evidence_error(&error)
832 && attempt_index + 1 < APPROVED_EVIDENCE_WRITE_ATTEMPTS =>
833 {
834 tokio::time::sleep(APPROVED_EVIDENCE_RETRY_DELAY).await;
835 }
836 Err(error) if is_retryable_evidence_error(&error) => break,
837 Err(error) => return Err(error),
838 }
839 }
840 Err(SubscriptionEnrollmentApplicationError::ApprovedEvidenceNotDurable)
841}