syrup-rail-postgres 0.5.0

Canonical provider-neutral PostgreSQL schema contract and SQLx orchestration for Syrup Rail
Documentation
use super::*;

async fn policy_subscription(
    database: &TestDatabase,
    account: GatewayAccountFixture,
    gateway: &ResolvedGateway,
    coordinator: &TestCoordinator,
    policy: RenewalFailurePolicy,
    key: &str,
) -> Result<(SubscriberId, syrup_rail::SubscriptionId, DateTime<Utc>), Box<dyn Error>> {
    let offer = paid_trial_offer_with_policy(policy)?;
    let subscriber_id = SubscriberId::new(Uuid::now_v7());
    let initial = approve_enrollment(
        &database.pool,
        &StaticOfferStore {
            offer: offer.clone(),
        },
        gateway,
        coordinator,
        account,
        subscriber_id,
        key,
        SubscriptionEnrollmentExpectedTerms::full_price(offer),
        &format!("{key}_initial"),
        &format!("vault_{key}"),
    )
    .await?;
    let subscription_id = initial
        .subscription()
        .expect("approved trial creates subscription")
        .id();
    let due_at = make_trial_due(&database.pool, subscription_id).await?;
    Ok((subscriber_id, subscription_id, due_at))
}

fn query(account: GatewayAccountFixture, subscriber_id: SubscriberId) -> EntitlementQuery {
    EntitlementQuery::new(
        BillingScopeId::new(account.billing_scope_id),
        subscriber_id,
        PlanKey::new("identity_pro").expect("valid plan key"),
    )
}

#[tokio::test]
async fn access_policies_drive_terminal_and_cancellation_timestamps_without_reinterpreting_terms()
-> Result<(), Box<dyn Error>> {
    let database = TestDatabase::start("pt_access").await?;
    let account = create_gateway_account(&database.pool, "nmi").await?;
    let gateway = resolved_gateway(account)?;
    let events = Arc::new(Mutex::new(Vec::new()));
    let coordinator = TestCoordinator {
        pool: database.pool.clone(),
        events: Arc::clone(&events),
    };
    let scope = BillingScopeId::new(account.billing_scope_id);

    let suspend_policy = RenewalFailurePolicy::new(
        DunningSchedule::from_seconds([60])?,
        DunningExhaustion::MarkUnpaid,
        PastDueAccessPolicy::SuspendImmediately,
    );
    let (suspended_subscriber, suspended_id, suspended_due) = policy_subscription(
        &database,
        account,
        &gateway,
        &coordinator,
        suspend_policy.clone(),
        "suspend_terminal",
    )
    .await?;
    let (_, first_suspended_at) = decline_due_renewal(
        &database.pool,
        &gateway,
        &coordinator,
        scope,
        suspended_id,
        suspended_due,
        "suspend_terminal_failure_1",
    )
    .await?;
    assert!(events.lock().await.iter().any(|event| matches!(
        event,
        BillingEvent::SubscriptionPaymentFailed {
            subscription_id,
            disposition: SubscriptionPaymentFailureDisposition::RetryScheduled { .. },
            access: SubscriptionPaymentFailureAccess::Ended { access_ended_at },
            ..
        } if *subscription_id == suspended_id && *access_ended_at == first_suspended_at
    )));
    assert!(matches!(
        entitlement(&database.pool, &query(account, suspended_subscriber)).await?,
        Entitlement::PastDue {
            access: syrup_rail::PastDueAccess::Suspended,
            ..
        }
    ));
    assert!(matches!(
        require_entitlement_for_update(
            EntitlementWriteTransaction::begin(&database.pool).await?,
            &EntitlementGuard::new(scope, suspended_subscriber, PlanKey::new("identity_pro")?,),
        )
        .await,
        Err(crate::EntitlementGuardError::PastDue)
    ));
    make_retry_due(&database.pool, suspended_id).await?;
    let (_, final_suspended_at) = decline_due_renewal(
        &database.pool,
        &gateway,
        &coordinator,
        scope,
        suspended_id,
        suspended_due,
        "suspend_terminal_failure_2",
    )
    .await?;
    let status: String =
        sqlx::query_scalar("SELECT status FROM billing_subscriptions WHERE id = $1")
            .bind(suspended_id.as_uuid())
            .fetch_one(&database.pool)
            .await?;
    assert_eq!(status, "unpaid");
    let emitted = events.lock().await.clone();
    assert!(emitted.iter().any(|event| matches!(
        event,
        BillingEvent::SubscriptionPaymentFailed {
            subscription_id,
            disposition: SubscriptionPaymentFailureDisposition::SubscriptionEnded { ended_at },
            access: SubscriptionPaymentFailureAccess::Ended { access_ended_at },
            ..
        } if *subscription_id == suspended_id
            && *ended_at == final_suspended_at
            && *access_ended_at == first_suspended_at
    )));
    assert!(emitted.iter().any(|event| matches!(
        event,
        BillingEvent::SubscriptionEnded {
            subscription_id,
            reason: SubscriptionEndReason::NonPayment,
            ended_at,
            access_ends_at,
            ..
        } if *subscription_id == suspended_id
            && *ended_at == final_suspended_at
            && *access_ends_at == first_suspended_at
    )));

    let remain_policy = RenewalFailurePolicy::new(
        DunningSchedule::from_seconds([60])?,
        DunningExhaustion::RemainPastDue,
        PastDueAccessPolicy::ContinueUntilDunningExhausted,
    );
    let (remain_subscriber, remain_id, remain_due) = policy_subscription(
        &database,
        account,
        &gateway,
        &coordinator,
        remain_policy,
        "remain_exhausted",
    )
    .await?;
    decline_due_renewal(
        &database.pool,
        &gateway,
        &coordinator,
        scope,
        remain_id,
        remain_due,
        "remain_failure_1",
    )
    .await?;
    assert!(events.lock().await.iter().any(|event| matches!(
        event,
        BillingEvent::SubscriptionPaymentFailed {
            subscription_id,
            disposition: SubscriptionPaymentFailureDisposition::RetryScheduled { .. },
            access: SubscriptionPaymentFailureAccess::ContinuesDuringDunning,
            ..
        } if *subscription_id == remain_id
    )));
    assert!(matches!(
        entitlement(&database.pool, &query(account, remain_subscriber)).await?,
        Entitlement::PastDue {
            access: syrup_rail::PastDueAccess::AllowedDuringDunning,
            ..
        }
    ));
    make_retry_due(&database.pool, remain_id).await?;
    let (_, remain_exhausted_at) = decline_due_renewal(
        &database.pool,
        &gateway,
        &coordinator,
        scope,
        remain_id,
        remain_due,
        "remain_failure_2",
    )
    .await?;
    let (status, next_payment): (String, Option<DateTime<Utc>>) = sqlx::query_as(
        "SELECT status, next_payment_attempt_at FROM billing_subscriptions WHERE id = $1",
    )
    .bind(remain_id.as_uuid())
    .fetch_one(&database.pool)
    .await?;
    assert_eq!((status.as_str(), next_payment), ("past_due", None));
    assert!(matches!(
        entitlement(&database.pool, &query(account, remain_subscriber)).await?,
        Entitlement::PastDue {
            access: syrup_rail::PastDueAccess::Suspended,
            ..
        }
    ));
    assert!(events.lock().await.iter().any(|event| matches!(
        event,
        BillingEvent::SubscriptionPaymentFailed {
            subscription_id,
            disposition: SubscriptionPaymentFailureDisposition::DunningExhausted {
                exhausted_at,
            },
            access: SubscriptionPaymentFailureAccess::Ended { access_ended_at },
            ..
        } if *subscription_id == remain_id
            && *exhausted_at == remain_exhausted_at
            && *access_ended_at == remain_exhausted_at
    )));
    let remain_cancel =
        CancelSubscription::new(scope, remain_subscriber, PlanKey::new("identity_pro")?);
    let mut transaction = database.pool.begin().await?;
    let outcome = cancel_subscription_in_transaction(&mut transaction, &remain_cancel).await?;
    transaction.commit().await?;
    assert!(matches!(
        outcome,
        CancelSubscriptionOutcome::Canceled {
            event: BillingEvent::SubscriptionCanceled { access_ends_at, .. },
            ..
        } if access_ends_at == remain_exhausted_at
    ));

    let (cancel_subscriber, cancel_id, cancel_due) = policy_subscription(
        &database,
        account,
        &gateway,
        &coordinator,
        suspend_policy,
        "suspend_cancel",
    )
    .await?;
    let (_, suspended_at) = decline_due_renewal(
        &database.pool,
        &gateway,
        &coordinator,
        scope,
        cancel_id,
        cancel_due,
        "suspend_cancel_failure",
    )
    .await?;
    let cancel = CancelSubscription::new(scope, cancel_subscriber, PlanKey::new("identity_pro")?);
    let mut transaction = database.pool.begin().await?;
    let outcome = cancel_subscription_in_transaction(&mut transaction, &cancel).await?;
    transaction.commit().await?;
    assert!(matches!(
        outcome,
        CancelSubscriptionOutcome::Canceled {
            event: BillingEvent::SubscriptionCanceled { access_ends_at, .. },
            ..
        } if access_ends_at == suspended_at
    ));

    database.cleanup().await
}