/** * Subscription Status Value Object * * Represents the different statuses a subscription can have. * Follows Stripe subscription lifecycle states. */ export type SubscriptionStatusType = | 'ACTIVE' | 'PAST_DUE' | 'CANCELED' | 'INCOMPLETE' | 'INCOMPLETE_EXPIRED' | 'TRIALING' | 'UNPAID' | 'PAUSED'; interface StatusDetails { readonly label: string; readonly description: string; readonly allowsAccess: boolean; readonly requiresAction: boolean; } const STATUS_DETAILS: Record = { ACTIVE: { label: 'Active', description: 'Subscription is active and fully paid', allowsAccess: true, requiresAction: false, }, PAST_DUE: { label: 'Past Due', description: 'Payment failed but subscription still active. Action required.', allowsAccess: true, // Grace period requiresAction: true, }, CANCELED: { label: 'Canceled', description: 'Subscription has been canceled', allowsAccess: false, requiresAction: false, }, INCOMPLETE: { label: 'Incomplete', description: 'Initial payment failed during subscription creation', allowsAccess: false, requiresAction: true, }, INCOMPLETE_EXPIRED: { label: 'Incomplete Expired', description: 'Subscription creation payment window expired', allowsAccess: false, requiresAction: false, }, TRIALING: { label: 'Trialing', description: 'Subscription is in trial period', allowsAccess: true, requiresAction: false, }, UNPAID: { label: 'Unpaid', description: 'All payment retry attempts have failed', allowsAccess: false, requiresAction: true, }, PAUSED: { label: 'Paused', description: 'Subscription has been paused', allowsAccess: false, requiresAction: false, }, }; // Status transitions that are valid const VALID_TRANSITIONS: Record = { ACTIVE: ['PAST_DUE', 'CANCELED', 'PAUSED'], PAST_DUE: ['ACTIVE', 'CANCELED', 'UNPAID'], CANCELED: [], // Terminal state INCOMPLETE: ['ACTIVE', 'INCOMPLETE_EXPIRED'], INCOMPLETE_EXPIRED: [], // Terminal state TRIALING: ['ACTIVE', 'PAST_DUE', 'CANCELED'], UNPAID: ['ACTIVE', 'CANCELED'], PAUSED: ['ACTIVE', 'CANCELED'], }; export class SubscriptionStatus { private constructor(private readonly status: SubscriptionStatusType) {} static create(status: SubscriptionStatusType): SubscriptionStatus { if (!STATUS_DETAILS[status]) { throw new Error(`Invalid subscription status: ${status}`); } return new SubscriptionStatus(status); } static fromString(value: string): SubscriptionStatus { const upperValue = value.toUpperCase().replace(/-/g, '_') as SubscriptionStatusType; if (!STATUS_DETAILS[upperValue]) { throw new Error(`Invalid subscription status: ${value}`); } return new SubscriptionStatus(upperValue); } static fromStripeStatus(stripeStatus: string): SubscriptionStatus { // Map Stripe status to our internal status const mapping: Record = { active: 'ACTIVE', past_due: 'PAST_DUE', canceled: 'CANCELED', incomplete: 'INCOMPLETE', incomplete_expired: 'INCOMPLETE_EXPIRED', trialing: 'TRIALING', unpaid: 'UNPAID', paused: 'PAUSED', }; const mappedStatus = mapping[stripeStatus.toLowerCase()]; if (!mappedStatus) { throw new Error(`Unknown Stripe subscription status: ${stripeStatus}`); } return new SubscriptionStatus(mappedStatus); } static active(): SubscriptionStatus { return new SubscriptionStatus('ACTIVE'); } static canceled(): SubscriptionStatus { return new SubscriptionStatus('CANCELED'); } static pastDue(): SubscriptionStatus { return new SubscriptionStatus('PAST_DUE'); } static trialing(): SubscriptionStatus { return new SubscriptionStatus('TRIALING'); } get value(): SubscriptionStatusType { return this.status; } get label(): string { return STATUS_DETAILS[this.status].label; } get description(): string { return STATUS_DETAILS[this.status].description; } /** * Returns true if this status allows access to the platform */ allowsAccess(): boolean { return STATUS_DETAILS[this.status].allowsAccess; } /** * Returns true if this status requires user action (e.g., update payment method) */ requiresAction(): boolean { return STATUS_DETAILS[this.status].requiresAction; } /** * Returns true if this is a terminal state (cannot transition out) */ isTerminal(): boolean { return VALID_TRANSITIONS[this.status].length === 0; } /** * Returns true if the subscription is in good standing */ isInGoodStanding(): boolean { return this.status === 'ACTIVE' || this.status === 'TRIALING'; } /** * Check if transition to new status is valid */ canTransitionTo(newStatus: SubscriptionStatus): boolean { return VALID_TRANSITIONS[this.status].includes(newStatus.value); } /** * Transition to new status if valid */ transitionTo(newStatus: SubscriptionStatus): SubscriptionStatus { if (!this.canTransitionTo(newStatus)) { throw new Error( `Invalid status transition from ${this.status} to ${newStatus.value}`, ); } return newStatus; } equals(other: SubscriptionStatus): boolean { return this.status === other.status; } toString(): string { return this.status; } /** * Convert to Stripe-compatible status string */ toStripeStatus(): string { return this.status.toLowerCase().replace(/_/g, '-'); } }