406 lines
13 KiB
TypeScript
406 lines
13 KiB
TypeScript
/**
|
|
* Subscription Entity Tests
|
|
*
|
|
* Unit tests for the Subscription domain entity
|
|
*/
|
|
|
|
import { Subscription } from './subscription.entity';
|
|
import { SubscriptionPlan } from '../value-objects/subscription-plan.vo';
|
|
import { SubscriptionStatus } from '../value-objects/subscription-status.vo';
|
|
import {
|
|
InvalidSubscriptionDowngradeException,
|
|
SubscriptionNotActiveException,
|
|
} from '../exceptions/subscription.exceptions';
|
|
|
|
describe('Subscription Entity', () => {
|
|
const createValidSubscription = () => {
|
|
return Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
});
|
|
};
|
|
|
|
describe('create', () => {
|
|
it('should create a subscription with default FREE plan', () => {
|
|
const subscription = createValidSubscription();
|
|
|
|
expect(subscription.id).toBe('sub-123');
|
|
expect(subscription.organizationId).toBe('org-123');
|
|
expect(subscription.plan.value).toBe('FREE');
|
|
expect(subscription.status.value).toBe('ACTIVE');
|
|
expect(subscription.cancelAtPeriodEnd).toBe(false);
|
|
});
|
|
|
|
it('should create a subscription with custom plan', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.starter(),
|
|
});
|
|
|
|
expect(subscription.plan.value).toBe('STARTER');
|
|
});
|
|
|
|
it('should create a subscription with Stripe IDs', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
stripeCustomerId: 'cus_123',
|
|
stripeSubscriptionId: 'sub_stripe_123',
|
|
});
|
|
|
|
expect(subscription.stripeCustomerId).toBe('cus_123');
|
|
expect(subscription.stripeSubscriptionId).toBe('sub_stripe_123');
|
|
});
|
|
});
|
|
|
|
describe('fromPersistence', () => {
|
|
it('should reconstitute a subscription from persistence data', () => {
|
|
const subscription = Subscription.fromPersistence({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: 'PRO',
|
|
status: 'ACTIVE',
|
|
stripeCustomerId: 'cus_123',
|
|
stripeSubscriptionId: 'sub_stripe_123',
|
|
currentPeriodStart: new Date('2024-01-01'),
|
|
currentPeriodEnd: new Date('2024-02-01'),
|
|
cancelAtPeriodEnd: true,
|
|
createdAt: new Date('2024-01-01'),
|
|
updatedAt: new Date('2024-01-15'),
|
|
});
|
|
|
|
expect(subscription.id).toBe('sub-123');
|
|
expect(subscription.plan.value).toBe('PRO');
|
|
expect(subscription.status.value).toBe('ACTIVE');
|
|
expect(subscription.cancelAtPeriodEnd).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('maxLicenses', () => {
|
|
it('should return correct limits for FREE plan', () => {
|
|
const subscription = createValidSubscription();
|
|
expect(subscription.maxLicenses).toBe(2);
|
|
});
|
|
|
|
it('should return correct limits for STARTER plan', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.starter(),
|
|
});
|
|
expect(subscription.maxLicenses).toBe(5);
|
|
});
|
|
|
|
it('should return correct limits for PRO plan', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.pro(),
|
|
});
|
|
expect(subscription.maxLicenses).toBe(20);
|
|
});
|
|
|
|
it('should return -1 for ENTERPRISE plan (unlimited)', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.enterprise(),
|
|
});
|
|
expect(subscription.maxLicenses).toBe(-1);
|
|
});
|
|
});
|
|
|
|
describe('isUnlimited', () => {
|
|
it('should return false for FREE plan', () => {
|
|
const subscription = createValidSubscription();
|
|
expect(subscription.isUnlimited()).toBe(false);
|
|
});
|
|
|
|
it('should return true for ENTERPRISE plan', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.enterprise(),
|
|
});
|
|
expect(subscription.isUnlimited()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isActive', () => {
|
|
it('should return true for ACTIVE status', () => {
|
|
const subscription = createValidSubscription();
|
|
expect(subscription.isActive()).toBe(true);
|
|
});
|
|
|
|
it('should return true for TRIALING status', () => {
|
|
const subscription = Subscription.fromPersistence({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: 'FREE',
|
|
status: 'TRIALING',
|
|
stripeCustomerId: null,
|
|
stripeSubscriptionId: null,
|
|
currentPeriodStart: null,
|
|
currentPeriodEnd: null,
|
|
cancelAtPeriodEnd: false,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
expect(subscription.isActive()).toBe(true);
|
|
});
|
|
|
|
it('should return false for CANCELED status', () => {
|
|
const subscription = Subscription.fromPersistence({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: 'FREE',
|
|
status: 'CANCELED',
|
|
stripeCustomerId: null,
|
|
stripeSubscriptionId: null,
|
|
currentPeriodStart: null,
|
|
currentPeriodEnd: null,
|
|
cancelAtPeriodEnd: false,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
expect(subscription.isActive()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('canAllocateLicenses', () => {
|
|
it('should return true when licenses are available', () => {
|
|
const subscription = createValidSubscription();
|
|
expect(subscription.canAllocateLicenses(0, 1)).toBe(true);
|
|
expect(subscription.canAllocateLicenses(1, 1)).toBe(true);
|
|
});
|
|
|
|
it('should return false when no licenses available', () => {
|
|
const subscription = createValidSubscription();
|
|
expect(subscription.canAllocateLicenses(2, 1)).toBe(false); // FREE has 2 licenses
|
|
});
|
|
|
|
it('should always return true for ENTERPRISE plan', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.enterprise(),
|
|
});
|
|
expect(subscription.canAllocateLicenses(1000, 100)).toBe(true);
|
|
});
|
|
|
|
it('should return false when subscription is not active', () => {
|
|
const subscription = Subscription.fromPersistence({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: 'FREE',
|
|
status: 'CANCELED',
|
|
stripeCustomerId: null,
|
|
stripeSubscriptionId: null,
|
|
currentPeriodStart: null,
|
|
currentPeriodEnd: null,
|
|
cancelAtPeriodEnd: false,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
expect(subscription.canAllocateLicenses(0, 1)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('canUpgradeTo', () => {
|
|
it('should allow upgrade from FREE to STARTER', () => {
|
|
const subscription = createValidSubscription();
|
|
expect(subscription.canUpgradeTo(SubscriptionPlan.starter())).toBe(true);
|
|
});
|
|
|
|
it('should allow upgrade from FREE to PRO', () => {
|
|
const subscription = createValidSubscription();
|
|
expect(subscription.canUpgradeTo(SubscriptionPlan.pro())).toBe(true);
|
|
});
|
|
|
|
it('should not allow downgrade via canUpgradeTo', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.starter(),
|
|
});
|
|
expect(subscription.canUpgradeTo(SubscriptionPlan.free())).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('canDowngradeTo', () => {
|
|
it('should allow downgrade when user count fits', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.starter(),
|
|
});
|
|
expect(subscription.canDowngradeTo(SubscriptionPlan.free(), 1)).toBe(true);
|
|
});
|
|
|
|
it('should prevent downgrade when user count exceeds new plan', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.starter(),
|
|
});
|
|
expect(subscription.canDowngradeTo(SubscriptionPlan.free(), 5)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updatePlan', () => {
|
|
it('should update to new plan when valid', () => {
|
|
const subscription = createValidSubscription();
|
|
const updated = subscription.updatePlan(SubscriptionPlan.starter(), 1);
|
|
|
|
expect(updated.plan.value).toBe('STARTER');
|
|
});
|
|
|
|
it('should throw when subscription is not active', () => {
|
|
const subscription = Subscription.fromPersistence({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: 'FREE',
|
|
status: 'CANCELED',
|
|
stripeCustomerId: null,
|
|
stripeSubscriptionId: null,
|
|
currentPeriodStart: null,
|
|
currentPeriodEnd: null,
|
|
cancelAtPeriodEnd: false,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
expect(() => subscription.updatePlan(SubscriptionPlan.starter(), 0)).toThrow(
|
|
SubscriptionNotActiveException,
|
|
);
|
|
});
|
|
|
|
it('should throw when downgrading with too many users', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.pro(),
|
|
});
|
|
|
|
expect(() => subscription.updatePlan(SubscriptionPlan.starter(), 10)).toThrow(
|
|
InvalidSubscriptionDowngradeException,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('updateStatus', () => {
|
|
it('should update subscription status', () => {
|
|
const subscription = createValidSubscription();
|
|
const updated = subscription.updateStatus(SubscriptionStatus.pastDue());
|
|
|
|
expect(updated.status.value).toBe('PAST_DUE');
|
|
});
|
|
});
|
|
|
|
describe('updateStripeCustomerId', () => {
|
|
it('should update Stripe customer ID', () => {
|
|
const subscription = createValidSubscription();
|
|
const updated = subscription.updateStripeCustomerId('cus_new_123');
|
|
|
|
expect(updated.stripeCustomerId).toBe('cus_new_123');
|
|
});
|
|
});
|
|
|
|
describe('updateStripeSubscription', () => {
|
|
it('should update Stripe subscription details', () => {
|
|
const subscription = createValidSubscription();
|
|
const periodStart = new Date('2024-02-01');
|
|
const periodEnd = new Date('2024-03-01');
|
|
|
|
const updated = subscription.updateStripeSubscription({
|
|
stripeSubscriptionId: 'sub_new_123',
|
|
currentPeriodStart: periodStart,
|
|
currentPeriodEnd: periodEnd,
|
|
cancelAtPeriodEnd: true,
|
|
});
|
|
|
|
expect(updated.stripeSubscriptionId).toBe('sub_new_123');
|
|
expect(updated.currentPeriodStart).toEqual(periodStart);
|
|
expect(updated.currentPeriodEnd).toEqual(periodEnd);
|
|
expect(updated.cancelAtPeriodEnd).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('scheduleCancellation', () => {
|
|
it('should mark subscription for cancellation', () => {
|
|
const subscription = createValidSubscription();
|
|
const updated = subscription.scheduleCancellation();
|
|
|
|
expect(updated.cancelAtPeriodEnd).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('unscheduleCancellation', () => {
|
|
it('should unmark subscription for cancellation', () => {
|
|
const subscription = Subscription.fromPersistence({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: 'STARTER',
|
|
status: 'ACTIVE',
|
|
stripeCustomerId: 'cus_123',
|
|
stripeSubscriptionId: 'sub_123',
|
|
currentPeriodStart: new Date(),
|
|
currentPeriodEnd: new Date(),
|
|
cancelAtPeriodEnd: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
const updated = subscription.unscheduleCancellation();
|
|
expect(updated.cancelAtPeriodEnd).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('cancel', () => {
|
|
it('should cancel the subscription immediately', () => {
|
|
const subscription = createValidSubscription();
|
|
const updated = subscription.cancel();
|
|
|
|
expect(updated.status.value).toBe('CANCELED');
|
|
expect(updated.cancelAtPeriodEnd).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isFree and isPaid', () => {
|
|
it('should return true for isFree when FREE plan', () => {
|
|
const subscription = createValidSubscription();
|
|
expect(subscription.isFree()).toBe(true);
|
|
expect(subscription.isPaid()).toBe(false);
|
|
});
|
|
|
|
it('should return true for isPaid when STARTER plan', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
plan: SubscriptionPlan.starter(),
|
|
});
|
|
expect(subscription.isFree()).toBe(false);
|
|
expect(subscription.isPaid()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('toObject', () => {
|
|
it('should convert to plain object for persistence', () => {
|
|
const subscription = Subscription.create({
|
|
id: 'sub-123',
|
|
organizationId: 'org-123',
|
|
stripeCustomerId: 'cus_123',
|
|
});
|
|
|
|
const obj = subscription.toObject();
|
|
|
|
expect(obj.id).toBe('sub-123');
|
|
expect(obj.organizationId).toBe('org-123');
|
|
expect(obj.plan).toBe('FREE');
|
|
expect(obj.status).toBe('ACTIVE');
|
|
expect(obj.stripeCustomerId).toBe('cus_123');
|
|
});
|
|
});
|
|
});
|