Skip to main content

syrup_rail_postgres/
attempts.rs

1use std::fmt;
2
3use chrono::{DateTime, Utc};
4use sqlx::{PgConnection, Postgres, Row, Transaction, postgres::PgRow};
5use syrup_rail::{
6    BillingContactSnapshot, BillingPeriod, BillingScopeId, ChargeAmount, CumulativeRefundCents,
7    CurrencyCode, DiscountClaimId, DiscountCodeId, GatewayAccountId, GatewayAccountMode,
8    GatewayConfigurationId, GatewayDiagnostic, GatewayLifecycleState, GatewayOrderId,
9    GatewayPaymentDescriptor, GatewayPaymentMethodReference, GatewayProviderKey,
10    GatewayTransactionId, HostChargeTargetId, IdempotencyKey, LimitedDiscountMonths, Money,
11    PaidTrialTerms, PaymentAttempt, PaymentAttemptFingerprint, PaymentAttemptId,
12    PaymentAttemptIdentity, PaymentAttemptKind, PaymentAttemptLifecycle, PaymentAttemptRequest,
13    PaymentAttemptState, PaymentAttemptStatus, PaymentAttemptTarget, PaymentAttemptTimestamps,
14    PaymentMethodId, PaymentMethodUpdateSnapshot, PaymentResolutionCode, PercentOffBasisPoints,
15    PlanKey, PositiveDiscountCents, ProcessorEvidence, RecurringSubscriptionTerms, SubscriberId,
16    SubscriptionDiscountCode, SubscriptionDiscountDuration, SubscriptionDiscountKind,
17    SubscriptionDiscountSnapshot, SubscriptionEnrollmentDiscountSnapshot,
18    SubscriptionEnrollmentPreflightOutcome, SubscriptionEnrollmentReservation,
19    SubscriptionEnrollmentReservationOutcome, SubscriptionEnrollmentReservationRejection,
20    SubscriptionEnrollmentSubmissionOutcome, SubscriptionEnrollmentSubmissionRejection,
21    SubscriptionEnrollmentTermsVersion, SubscriptionId, SubscriptionInitialApplication,
22    SubscriptionOffer, SubscriptionPaymentMethodReplacement,
23    SubscriptionPaymentMethodReplacementLockedTerms,
24    SubscriptionPaymentMethodReplacementPreflightOutcome,
25    SubscriptionPaymentMethodReplacementRejection,
26    SubscriptionPaymentMethodReplacementReservationOutcome,
27    SubscriptionPaymentMethodReplacementSubmissionOutcome,
28    SubscriptionPaymentMethodReplacementSubmissionRejection, SubscriptionPaymentStateSnapshot,
29    SubscriptionRecoveryLockedTerms, SubscriptionRecoveryPreflightOutcome,
30    SubscriptionRecoveryReservation, SubscriptionRecoveryReservationOutcome,
31    SubscriptionRecoveryReservationRejection, SubscriptionRecoverySubmissionOutcome,
32    SubscriptionRecoverySubmissionRejection, SubscriptionRenewalLockedTerms,
33    SubscriptionRenewalReservation, SubscriptionRenewalReservationOutcome,
34    SubscriptionRenewalReservationRejection, SubscriptionRenewalSubmissionOutcome,
35    SubscriptionRenewalSubmissionRejection, SubscriptionStart, SubscriptionStatus,
36};
37use thiserror::Error;
38use uuid::Uuid;
39
40use crate::subscription_persistence::{
41    RenewalFailurePolicyScalars, SubscriptionPeriodRuleScalars, SubscriptionPersistenceCodecError,
42    renewal_failure_policy_from_scalars, subscription_period_rule_from_scalars,
43};
44
45mod initial;
46mod payment_method_replacement;
47mod persistence;
48mod recovery;
49mod renewal;
50mod shared;
51mod transitions;
52
53pub use initial::{
54    admit_subscription_enrollment_submission_in_transaction,
55    preflight_subscription_enrollment_in_transaction,
56    reserve_subscription_enrollment_in_transaction,
57};
58pub use payment_method_replacement::{
59    admit_subscription_payment_method_replacement_in_transaction,
60    preflight_subscription_payment_method_replacement_in_transaction,
61    reserve_subscription_payment_method_replacement_in_transaction,
62};
63pub(crate) use persistence::find_payment_attempt_by_idempotency_in_transaction;
64use persistence::map_subscription_persistence_error;
65pub(crate) use persistence::{
66    PAYMENT_ATTEMPT_SELECT, find_payment_attempt_by_id_on_connection,
67    lock_payment_attempt_by_id_on_connection, payment_attempt_from_row,
68    processor_evidence_from_row,
69};
70pub use persistence::{
71    find_payment_attempt_by_id_in_transaction, lock_payment_attempt_by_idempotency_in_transaction,
72};
73pub use recovery::{
74    admit_subscription_recovery_submission_in_transaction,
75    preflight_subscription_recovery_in_transaction, reserve_subscription_recovery_in_transaction,
76};
77pub use renewal::{
78    admit_subscription_renewal_submission_in_transaction,
79    reserve_subscription_renewal_in_transaction,
80};
81use shared::*;
82pub(crate) use shared::{
83    AttemptReplayDisposition, LocalAttemptPolicy, STALE_UNSUBMITTED_RECOVERY_TEXT,
84    STALE_UNSUBMITTED_RENEWAL_TEXT, attempt_replay_disposition,
85    blocking_payment_method_update_exists, expire_stale_initial_attempts,
86    fail_stale_unsubmitted_payment_method_updates, fail_stale_unsubmitted_subscription_charges,
87    lock_initial_attempt_rows, lock_initial_charge_rows, lock_subscription_aggregate,
88    prepared_replay_required_mode_changed, set_enrollment_timeouts,
89    try_lock_subscription_aggregate,
90};
91use transitions::reject_prepared_attempt;
92pub(crate) use transitions::{
93    AttemptApproval, AttemptResolutionStatus, AttemptTransition, admit_prepared_attempt,
94    persist_attempt_transition,
95};
96
97const INVALID_ATTEMPT_STATE: &str = "canonical payment attempt state is invalid";
98#[derive(Error)]
99pub enum PaymentAttemptStoreError {
100    #[error("payment attempt storage operation failed")]
101    Sql(#[from] sqlx::Error),
102    #[error("{0}")]
103    InvalidState(&'static str),
104}
105
106impl fmt::Debug for PaymentAttemptStoreError {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::Sql(_) => formatter.write_str("PaymentAttemptStoreError::Sql"),
110            Self::InvalidState(detail) => formatter
111                .debug_tuple("PaymentAttemptStoreError::InvalidState")
112                .field(detail)
113                .finish(),
114        }
115    }
116}
117
118const fn invalid_state() -> PaymentAttemptStoreError {
119    PaymentAttemptStoreError::InvalidState(INVALID_ATTEMPT_STATE)
120}
121
122#[cfg(test)]
123mod tests;