356 lines
8.6 KiB
TypeScript
356 lines
8.6 KiB
TypeScript
/**
|
|
* 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,
|
|
};
|
|
}
|
|
}
|