xpeditis2.0/apps/backend/src/domain/entities/subscription.entity.ts
David 08787c89c8
Some checks failed
Dev CI / Unit Tests (${{ matrix.app }}) (backend) (push) Blocked by required conditions
Dev CI / Unit Tests (${{ matrix.app }}) (frontend) (push) Blocked by required conditions
Dev CI / Notify Failure (push) Blocked by required conditions
Dev CI / Quality (${{ matrix.app }}) (backend) (push) Has been cancelled
Dev CI / Quality (${{ matrix.app }}) (frontend) (push) Has been cancelled
chore: sync full codebase from cicd branch
Aligns dev with the complete application codebase (cicd branch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:56:16 +02:00

384 lines
9.4 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 Bronze/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.bronze(),
status: SubscriptionStatus.active(),
stripeCustomerId: props.stripeCustomerId ?? null,
stripeSubscriptionId: props.stripeSubscriptionId ?? null,
currentPeriodStart: null,
currentPeriodEnd: null,
cancelAtPeriodEnd: false,
createdAt: now,
updatedAt: now,
});
}
/**
* Reconstitute from persistence
*/
/**
* Check if a specific plan feature is available
*/
hasFeature(feature: import('../value-objects/plan-feature.vo').PlanFeature): boolean {
return this.props.plan.hasFeature(feature);
}
/**
* Get the maximum shipments per year allowed
*/
get maxShipmentsPerYear(): number {
return this.props.plan.maxShipmentsPerYear;
}
/**
* Get the commission rate for this subscription's plan
*/
get commissionRatePercent(): number {
return this.props.plan.commissionRatePercent;
}
/**
* Get the status badge for this subscription's plan
*/
get statusBadge(): string {
return this.props.plan.statusBadge;
}
/**
* Reconstitute from persistence (supports legacy plan names)
*/
static fromPersistence(props: {
id: string;
organizationId: string;
plan: string; // Accepts both old and new plan names
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.fromString(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,
};
}
}