/** * AuthenticatedUser Domain Entity * * Single source of truth for the signed-in user's identity + billing state on * the dashboard. Seeded from the /auth/login and /auth/refresh responses, * persisted in localStorage (`twwim_authenticated_user`), and exposed to * components via `useAuthenticatedUser()`. * * Immutable: every mutation returns a new instance via `with()`. Components * re-render through the storage store's `useSyncExternalStore` subscription. * * Growable: `account` starts with billing-relevant flags (currency + * completeness). Add new fields to both the backend `account.response.ts` * schema and `AuthenticatedAccount` here in lockstep. * * (c) 2026 TWWIM UG. All rights reserved. (www.twwim.com) */ import { Currency } from '@archer/domain'; export type LegalDocTypeWire = 'AGB' | 'DATENSCHUTZ' | 'AVV' | 'IMPRESSUM' | 'COOKIE_POLICY'; export interface AccountLegalStatus { /** Number of doctypes currently PENDING or OUTDATED for this company. */ pendingCount: number; /** The exact doctypes — used for badges and analytics. */ pendingDoctypes: LegalDocTypeWire[]; } export interface AuthenticatedAccount { /** Billing currency — drives price display across the CP. */ preferredCurrency: Currency; /** Server-computed; true when billing-required fields are all set. */ isProfileComplete: boolean; /** Field names still missing when `isProfileComplete=false`. */ missingProfileFields: string[]; /** Raw auth.emailVerified — independent of isProfileComplete. Paid-purchase * gate is the AND of these two predicates; banner nags on either. */ emailVerified: boolean; /** Legal-acceptance status — non-zero pendingCount drives the consent banner. */ legal: AccountLegalStatus; } export interface AuthenticatedUserProps { id: string; email: string; name: string; companyId: string; role: string; authOrigin?: string; account: AuthenticatedAccount; } export class AuthenticatedUser { private constructor(private readonly props: AuthenticatedUserProps) {} static create(props: AuthenticatedUserProps): AuthenticatedUser { return new AuthenticatedUser(props); } get id(): string { return this.props.id; } get email(): string { return this.props.email; } get name(): string { return this.props.name; } get companyId(): string { return this.props.companyId; } get role(): string { return this.props.role; } get authOrigin(): string | undefined { return this.props.authOrigin; } get account(): AuthenticatedAccount { return this.props.account; } /** Convenience readers — match the display sites that currently read these flags. */ get preferredCurrency(): Currency { return this.props.account.preferredCurrency; } get isProfileComplete(): boolean { return this.props.account.isProfileComplete; } get missingProfileFields(): string[] { return this.props.account.missingProfileFields; } get emailVerified(): boolean { return this.props.account.emailVerified; } get legal(): AccountLegalStatus { return this.props.account.legal; } /** * Return a new instance with the given identity fields patched. The nested * `account` merges field-by-field so callers don't have to respread it. */ with(patch: Partial> & { account?: Partial }): AuthenticatedUser { const nextAccount: AuthenticatedAccount = patch.account ? { ...this.props.account, ...patch.account } : this.props.account; return new AuthenticatedUser({ ...this.props, ...patch, account: nextAccount, }); } toJSON(): AuthenticatedUserProps { return { id: this.props.id, email: this.props.email, name: this.props.name, companyId: this.props.companyId, role: this.props.role, authOrigin: this.props.authOrigin, account: { ...this.props.account }, }; } }