/** * Subscription Entity * * Represents an organization's subscription, including their plan, * Stripe integration, and billing period information. */ import { SubscriptionPlan, SubscriptionPlanType, } from '../value-objects/subscription-plan.vo'; import { SubscriptionStatus, SubscriptionStatusType, } from '../value-objects/subscription-status.vo'; import { InvalidSubscriptionDowngradeException, SubscriptionNotActiveException, } from '../exceptions/subscription.exceptions'; export interface SubscriptionProps { readonly id: string; readonly organizationId: string; readonly plan: SubscriptionPlan; readonly status: SubscriptionStatus; readonly stripeCustomerId: string | null; readonly stripeSubscriptionId: string | null; readonly currentPeriodStart: Date | null; readonly currentPeriodEnd: Date | null; readonly cancelAtPeriodEnd: boolean; readonly createdAt: Date; readonly updatedAt: Date; } export class Subscription { private readonly props: SubscriptionProps; private constructor(props: SubscriptionProps) { this.props = props; } /** * Create a new subscription (defaults to FREE plan) */ static create(props: { id: string; organizationId: string; plan?: SubscriptionPlan; stripeCustomerId?: string | null; stripeSubscriptionId?: string | null; }): Subscription { const now = new Date(); return new Subscription({ id: props.id, organizationId: props.organizationId, plan: props.plan ?? SubscriptionPlan.free(), status: SubscriptionStatus.active(), stripeCustomerId: props.stripeCustomerId ?? null, stripeSubscriptionId: props.stripeSubscriptionId ?? null, currentPeriodStart: null, currentPeriodEnd: null, cancelAtPeriodEnd: false, createdAt: now, updatedAt: now, }); } /** * Reconstitute from persistence */ static fromPersistence(props: { id: string; organizationId: string; plan: SubscriptionPlanType; status: SubscriptionStatusType; stripeCustomerId: string | null; stripeSubscriptionId: string | null; currentPeriodStart: Date | null; currentPeriodEnd: Date | null; cancelAtPeriodEnd: boolean; createdAt: Date; updatedAt: Date; }): Subscription { return new Subscription({ id: props.id, organizationId: props.organizationId, plan: SubscriptionPlan.create(props.plan), status: SubscriptionStatus.create(props.status), stripeCustomerId: props.stripeCustomerId, stripeSubscriptionId: props.stripeSubscriptionId, currentPeriodStart: props.currentPeriodStart, currentPeriodEnd: props.currentPeriodEnd, cancelAtPeriodEnd: props.cancelAtPeriodEnd, createdAt: props.createdAt, updatedAt: props.updatedAt, }); } // Getters get id(): string { return this.props.id; } get organizationId(): string { return this.props.organizationId; } get plan(): SubscriptionPlan { return this.props.plan; } get status(): SubscriptionStatus { return this.props.status; } get stripeCustomerId(): string | null { return this.props.stripeCustomerId; } get stripeSubscriptionId(): string | null { return this.props.stripeSubscriptionId; } get currentPeriodStart(): Date | null { return this.props.currentPeriodStart; } get currentPeriodEnd(): Date | null { return this.props.currentPeriodEnd; } get cancelAtPeriodEnd(): boolean { return this.props.cancelAtPeriodEnd; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } // Business logic /** * Get the maximum number of licenses allowed by this subscription */ get maxLicenses(): number { return this.props.plan.maxLicenses; } /** * Check if the subscription has unlimited licenses */ isUnlimited(): boolean { return this.props.plan.isUnlimited(); } /** * Check if the subscription is active and allows access */ isActive(): boolean { return this.props.status.allowsAccess(); } /** * Check if the subscription is in good standing */ isInGoodStanding(): boolean { return this.props.status.isInGoodStanding(); } /** * Check if the subscription requires user action */ requiresAction(): boolean { return this.props.status.requiresAction(); } /** * Check if this is a free subscription */ isFree(): boolean { return this.props.plan.isFree(); } /** * Check if this is a paid subscription */ isPaid(): boolean { return this.props.plan.isPaid(); } /** * Check if the subscription is scheduled to be canceled */ isScheduledForCancellation(): boolean { return this.props.cancelAtPeriodEnd; } /** * Check if a given number of licenses can be allocated */ canAllocateLicenses(currentCount: number, additionalCount: number = 1): boolean { if (!this.isActive()) return false; if (this.isUnlimited()) return true; return currentCount + additionalCount <= this.maxLicenses; } /** * Check if upgrade to target plan is possible */ canUpgradeTo(targetPlan: SubscriptionPlan): boolean { return this.props.plan.canUpgradeTo(targetPlan); } /** * Check if downgrade to target plan is possible given current user count */ canDowngradeTo(targetPlan: SubscriptionPlan, currentUserCount: number): boolean { return this.props.plan.canDowngradeTo(targetPlan, currentUserCount); } /** * Update the subscription plan */ updatePlan(newPlan: SubscriptionPlan, currentUserCount: number): Subscription { if (!this.isActive()) { throw new SubscriptionNotActiveException(this.props.id, this.props.status.value); } // Check if downgrade is valid if (!newPlan.canAccommodateUsers(currentUserCount)) { throw new InvalidSubscriptionDowngradeException( this.props.plan.value, newPlan.value, currentUserCount, newPlan.maxLicenses, ); } return new Subscription({ ...this.props, plan: newPlan, updatedAt: new Date(), }); } /** * Update subscription status */ updateStatus(newStatus: SubscriptionStatus): Subscription { return new Subscription({ ...this.props, status: newStatus, updatedAt: new Date(), }); } /** * Update Stripe customer ID */ updateStripeCustomerId(stripeCustomerId: string): Subscription { return new Subscription({ ...this.props, stripeCustomerId, updatedAt: new Date(), }); } /** * Update Stripe subscription details */ updateStripeSubscription(params: { stripeSubscriptionId: string; currentPeriodStart: Date; currentPeriodEnd: Date; cancelAtPeriodEnd?: boolean; }): Subscription { return new Subscription({ ...this.props, stripeSubscriptionId: params.stripeSubscriptionId, currentPeriodStart: params.currentPeriodStart, currentPeriodEnd: params.currentPeriodEnd, cancelAtPeriodEnd: params.cancelAtPeriodEnd ?? this.props.cancelAtPeriodEnd, updatedAt: new Date(), }); } /** * Mark subscription as scheduled for cancellation at period end */ scheduleCancellation(): Subscription { return new Subscription({ ...this.props, cancelAtPeriodEnd: true, updatedAt: new Date(), }); } /** * Unschedule cancellation */ unscheduleCancellation(): Subscription { return new Subscription({ ...this.props, cancelAtPeriodEnd: false, updatedAt: new Date(), }); } /** * Cancel the subscription immediately */ cancel(): Subscription { return new Subscription({ ...this.props, status: SubscriptionStatus.canceled(), cancelAtPeriodEnd: false, updatedAt: new Date(), }); } /** * Convert to plain object for persistence */ toObject(): { id: string; organizationId: string; plan: SubscriptionPlanType; status: SubscriptionStatusType; stripeCustomerId: string | null; stripeSubscriptionId: string | null; currentPeriodStart: Date | null; currentPeriodEnd: Date | null; cancelAtPeriodEnd: boolean; createdAt: Date; updatedAt: Date; } { return { id: this.props.id, organizationId: this.props.organizationId, plan: this.props.plan.value, status: this.props.status.value, stripeCustomerId: this.props.stripeCustomerId, stripeSubscriptionId: this.props.stripeSubscriptionId, currentPeriodStart: this.props.currentPeriodStart, currentPeriodEnd: this.props.currentPeriodEnd, cancelAtPeriodEnd: this.props.cancelAtPeriodEnd, createdAt: this.props.createdAt, updatedAt: this.props.updatedAt, }; } }