/** * Tenant Domain Entity * * Represents a customer deployment (website, mobile app, API client, etc.) * that integrates with the Archer voice platform. * * Domain invariants enforced: * - Domain must be lowercase and non-empty * - Name must be non-empty * - Status transitions follow business rules * * @layer Domain */ export { TenantStatus } from '@archer/domain'; import { TenantStatus } from '@archer/domain'; interface TenantProps { id: string; companyId: string; name: string; domain: string; status: TenantStatus; integrationKeysCount: number; createdAt: Date; updatedAt: Date; deletedAt?: Date; } /** * Tenant Entity * * Encapsulates tenant business logic and domain invariants. * Use factory methods (create, fromPersistence) to construct instances. */ export class Tenant { private constructor(private readonly props: TenantProps) { this.validate(); } /** * Creates a new Tenant instance * * @param data - Tenant creation data * @returns Tenant domain entity * @throws Error if validation fails */ static create(data: { id: string; companyId: string; name: string; domain: string; status: TenantStatus; }): Tenant { const now = new Date(); return new Tenant({ id: data.id, companyId: data.companyId, name: data.name, domain: data.domain.toLowerCase(), // Enforce lowercase status: data.status, integrationKeysCount: 0, createdAt: now, updatedAt: now, }); } /** * Reconstructs Tenant from persistence layer * * @param data - Persisted tenant data * @returns Tenant domain entity */ static fromPersistence(data: { id: string; companyId: string; name: string; domain: string; status: TenantStatus; integrationKeysCount: number; createdAt: string | Date; updatedAt: string | Date; deletedAt?: string | Date; }): Tenant { return new Tenant({ id: data.id, companyId: data.companyId, name: data.name, domain: data.domain, status: data.status, integrationKeysCount: data.integrationKeysCount, createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt) : data.createdAt, updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt) : data.updatedAt, deletedAt: data.deletedAt ? typeof data.deletedAt === 'string' ? new Date(data.deletedAt) : data.deletedAt : undefined, }); } /** * Validates domain invariants * * @throws Error if validation fails */ private validate(): void { if (!this.props.name || this.props.name.trim().length === 0) { throw new Error('Tenant name cannot be empty'); } if (!this.props.domain || this.props.domain.trim().length === 0) { throw new Error('Tenant domain cannot be empty'); } if (this.props.domain !== this.props.domain.toLowerCase()) { throw new Error('Tenant domain must be lowercase'); } if (!this.props.companyId) { throw new Error('Tenant must belong to a company'); } } // Getters get id(): string { return this.props.id; } get companyId(): string { return this.props.companyId; } get name(): string { return this.props.name; } get domain(): string { return this.props.domain; } get status(): TenantStatus { return this.props.status; } get integrationKeysCount(): number { return this.props.integrationKeysCount; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } get deletedAt(): Date | undefined { return this.props.deletedAt; } /** * Checks if tenant is active * * @returns true if tenant status is ACTIVE */ isActive(): boolean { return this.props.status === TenantStatus.ACTIVE; } /** * Checks if tenant is suspended * * @returns true if tenant status is SUSPENDED */ isSuspended(): boolean { return this.props.status === TenantStatus.SUSPENDED; } /** * Checks if tenant is archived * * @returns true if tenant status is ARCHIVED */ isArchived(): boolean { return this.props.status === TenantStatus.ARCHIVED; } /** * Checks if tenant can make API requests * * @returns true if tenant is active and not deleted */ canMakeApiRequests(): boolean { return this.isActive() && !this.props.deletedAt; } /** * Gets display name for UI * * @returns Formatted display name (e.g., "Main Website (example.com)") */ getDisplayName(): string { return `${this.props.name} (${this.props.domain})`; } /** * Updates tenant properties * * @param updates - Properties to update * @returns New Tenant instance with updates */ update(updates: { name?: string; status?: TenantStatus; integrationKeysCount?: number; }): Tenant { return new Tenant({ ...this.props, ...updates, updatedAt: new Date(), }); } /** * Marks tenant as deleted * * @returns New Tenant instance marked as archived and deleted */ delete(): Tenant { return new Tenant({ ...this.props, status: TenantStatus.ARCHIVED, deletedAt: new Date(), updatedAt: new Date(), }); } /** * Converts entity to persistence format * * @returns Plain object for storage */ toPersistence(): { id: string; companyId: string; name: string; domain: string; status: TenantStatus; integrationKeysCount: number; createdAt: string; updatedAt: string; deletedAt?: string; } { return { id: this.props.id, companyId: this.props.companyId, name: this.props.name, domain: this.props.domain, status: this.props.status, integrationKeysCount: this.props.integrationKeysCount, createdAt: this.props.createdAt.toISOString(), updatedAt: this.props.updatedAt.toISOString(), deletedAt: this.props.deletedAt?.toISOString(), }; } }