/** * Company Domain Entity * * Represents a company (top-level entity in multi-tenant hierarchy). * Encapsulates company business logic and tenant relationships. * * Domain invariants enforced: * - Name must be non-empty * - Type must be valid * * @layer Domain */ /** * Company Type Enum */ export enum CompanyType { INDIVIDUAL = 'INDIVIDUAL', CORPORATE = 'CORPORATE', } interface Address { street?: string; supplement?: string; zip?: string; city?: string; country?: string; } interface CompanyProps { id: string; name: string; type: CompanyType; address?: Address; vatId?: string; website?: string; industry?: string; size?: string; billingEmail?: string; /** Legal form (LegalForm enum value) — required for non-PERSONAL accounts. */ legalForm?: string; /** Steuernummer (DE) or international tax-ID. Distinct from vatId. */ taxNumber?: string; /** Registergericht — required iff legalForm has a register entry. */ registerCourt?: string; /** Registernummer (e.g. "HRB 25861"). */ registerNumber?: string; /** Geschäftsführer / Director — required iff legalForm requires one. */ directorName?: string; /** Billing currency (EUR or USD). Null when not yet resolved. */ preferredCurrency?: string | null; /** Timestamp when currency was locked by first paid payment. Null = still changeable. */ currencyLockedAt?: Date | null; /** Recomputed server-side on every read/write — mirrored into AuthenticatedUser. */ isProfileComplete: boolean; missingProfileFields: string[]; createdAt: Date; updatedAt: Date; } /** * Company Entity * * Encapsulates company business logic. * Use factory methods (create, fromPersistence) to construct instances. */ export class Company { private constructor(private readonly props: CompanyProps) { this.validate(); } /** * Creates a new Company instance * * @param data - Company creation data * @returns Company domain entity * @throws Error if validation fails */ static create(data: { id: string; name: string; type: CompanyType; address?: Address; vatId?: string; website?: string; industry?: string; size?: string; billingEmail?: string; }): Company { const now = new Date(); return new Company({ id: data.id, name: data.name, type: data.type, address: data.address, vatId: data.vatId, website: data.website, industry: data.industry, size: data.size, billingEmail: data.billingEmail, isProfileComplete: false, missingProfileFields: [], createdAt: now, updatedAt: now, }); } /** * Reconstructs Company from persistence layer * * @param data - Persisted company data * @returns Company domain entity */ static fromPersistence(data: { id: string; name: string; type: CompanyType; address?: Address; vatId?: string; website?: string; industry?: string; size?: string; billingEmail?: string; legalForm?: string; taxNumber?: string; registerCourt?: string; registerNumber?: string; directorName?: string; preferredCurrency?: string | null; currencyLockedAt?: string | Date | null; isProfileComplete?: boolean; missingProfileFields?: string[]; createdAt: string | Date; updatedAt: string | Date; }): Company { return new Company({ id: data.id, name: data.name, type: data.type, address: data.address, vatId: data.vatId, website: data.website, industry: data.industry, size: data.size, billingEmail: data.billingEmail, legalForm: data.legalForm, taxNumber: data.taxNumber, registerCourt: data.registerCourt, registerNumber: data.registerNumber, directorName: data.directorName, preferredCurrency: data.preferredCurrency ?? null, currencyLockedAt: data.currencyLockedAt ? (typeof data.currencyLockedAt === 'string' ? new Date(data.currencyLockedAt) : data.currencyLockedAt) : null, isProfileComplete: data.isProfileComplete ?? false, missingProfileFields: data.missingProfileFields ?? [], createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt) : data.createdAt, updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt) : data.updatedAt, }); } /** * Validates domain invariants * * @throws Error if validation fails */ private validate(): void { if (!this.props.name || this.props.name.trim().length === 0) { throw new Error('Company name cannot be empty'); } } // Getters get id(): string { return this.props.id; } get name(): string { return this.props.name; } get type(): CompanyType { return this.props.type; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } get address(): Address | undefined { return this.props.address; } get vatId(): string | undefined { return this.props.vatId; } get website(): string | undefined { return this.props.website; } get industry(): string | undefined { return this.props.industry; } get size(): string | undefined { return this.props.size; } get billingEmail(): string | undefined { return this.props.billingEmail; } get legalForm(): string | undefined { return this.props.legalForm; } get taxNumber(): string | undefined { return this.props.taxNumber; } get registerCourt(): string | undefined { return this.props.registerCourt; } get registerNumber(): string | undefined { return this.props.registerNumber; } get directorName(): string | undefined { return this.props.directorName; } get preferredCurrency(): string | null | undefined { return this.props.preferredCurrency; } get currencyLockedAt(): Date | null | undefined { return this.props.currencyLockedAt; } get isProfileComplete(): boolean { return this.props.isProfileComplete; } get missingProfileFields(): string[] { return this.props.missingProfileFields; } /** True when billing currency is locked by an active paid subscription. */ isCurrencyLocked(): boolean { return !!this.props.currencyLockedAt; } /** * Checks if company is individual type * * @returns true if company type is INDIVIDUAL */ isIndividual(): boolean { return this.props.type === CompanyType.INDIVIDUAL; } /** * Checks if company is corporate type * * @returns true if company type is CORPORATE */ isCorporate(): boolean { return this.props.type === CompanyType.CORPORATE; } /** * Updates company properties * * @param updates - Properties to update * @returns New Company instance with updates */ update(updates: { name?: string; type?: CompanyType; address?: Address; vatId?: string; website?: string; industry?: string; size?: string; billingEmail?: string; }): Company { return new Company({ ...this.props, ...updates, updatedAt: new Date(), }); } /** * Converts entity to persistence format * * @returns Plain object for storage */ toPersistence(): { id: string; name: string; type: CompanyType; createdAt: string; updatedAt: string; } { return { id: this.props.id, name: this.props.name, type: this.props.type, createdAt: this.props.createdAt.toISOString(), updatedAt: this.props.updatedAt.toISOString(), }; } }