# Customer Domain Refactor Plan ## Moving User Entity Types from Auth to Customer Domain ## Overview **Problem**: Auth domain currently contains both authentication mechanisms AND user entity types, which mixes concerns. **Solution**: - Move all user entity types (`PortalUser`, `UserProfile`) from `auth/` to `customer/` domain - Add Portal (Prisma) as a provider alongside WHMCS - Keep auth domain focused ONLY on authentication mechanisms - Consolidate schemas and contracts into single files where appropriate ## Architecture Vision ``` packages/domain/customer/ ├── index.ts # Main exports ├── types.ts # ALL types in one file (schemas + inferred types) │ ├── Schemas (Zod) │ │ ├── customerAddressSchema │ │ ├── portalUserSchema (NEW - from auth) │ │ ├── customerProfileSchema │ │ └── userProfileSchema (NEW - from auth) │ │ │ └── Types (inferred from schemas) │ ├── CustomerAddress │ ├── PortalUser (NEW - auth state from portal DB) │ ├── CustomerProfile (WHMCS profile data) │ └── UserProfile (NEW - PortalUser + CustomerProfile) │ ├── providers/ │ ├── index.ts │ │ │ ├── portal/ (NEW - Portal DB provider) │ │ ├── index.ts │ │ ├── types.ts # PrismaUserRaw │ │ └── mapper.ts # Prisma → PortalUser │ │ │ ├── whmcs/ (EXISTING) │ │ ├── index.ts │ │ ├── types.ts # WHMCS raw types │ │ └── mapper.ts # WHMCS → CustomerProfile │ │ │ └── salesforce/ (EXISTING if needed) │ packages/domain/auth/ ├── index.ts ├── types.ts # Authentication mechanism types ONLY │ ├── Schemas (Zod) │ │ ├── loginRequestSchema │ │ ├── signupRequestSchema │ │ ├── passwordResetSchema │ │ ├── authTokensSchema │ │ ├── authResponseSchema (user: UserProfile reference, tokens) │ │ └── mfaSchemas │ │ │ └── Types (inferred) │ ├── LoginRequest │ ├── SignupRequest │ ├── AuthTokens │ └── AuthResponse │ └── NO user entity types, NO PortalUser, NO UserProfile ``` ## Detailed Implementation Plan ### Phase 1: Create New Customer Domain Structure #### 1.1 Create consolidated types file **File:** `packages/domain/customer/types.ts` Move from multiple files into one: - From `customer/schema.ts`: All existing customer schemas - From `customer/contract.ts`: Interface definitions (convert to schema-first) - From `auth/schema.ts`: `portalUserSchema`, `userProfileSchema` Structure: ```typescript import { z } from "zod"; // ============================================================================ // Common Schemas // ============================================================================ export const customerAddressSchema = z.object({...}); export const customerEmailPreferencesSchema = z.object({...}); // ... other existing schemas // ============================================================================ // Portal User Schema (Auth State from Portal DB) // ============================================================================ /** * PortalUser represents ONLY auth state stored in portal database * Provider: Prisma (portal DB) */ export const portalUserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(["USER", "ADMIN"]), emailVerified: z.boolean(), mfaEnabled: z.boolean(), lastLoginAt: z.string().optional(), createdAt: z.string(), updatedAt: z.string(), }); // ============================================================================ // Customer Profile Schema (WHMCS Profile Data) // ============================================================================ /** * CustomerProfile represents profile data from WHMCS * Provider: WHMCS */ export const customerProfileSchema = z.object({ id: z.string(), email: z.string(), firstname: z.string().nullable().optional(), lastname: z.string().nullable().optional(), fullname: z.string().nullable().optional(), companyname: z.string().nullable().optional(), phonenumber: z.string().nullable().optional(), address: customerAddressSchema.optional(), language: z.string().nullable().optional(), currencyCode: z.string().nullable().optional(), createdAt: z.string().optional(), updatedAt: z.string().optional(), }); // ============================================================================ // User Profile Schema (Complete User = PortalUser + CustomerProfile) // ============================================================================ /** * UserProfile is the complete authenticated user * Composition: PortalUser (auth state) + CustomerProfile (WHMCS data) */ export const userProfileSchema = portalUserSchema.extend({ // Add CustomerProfile fields firstname: z.string().nullable().optional(), lastname: z.string().nullable().optional(), fullname: z.string().nullable().optional(), companyname: z.string().nullable().optional(), phonenumber: z.string().nullable().optional(), address: customerAddressSchema.optional(), language: z.string().nullable().optional(), currencyCode: z.string().nullable().optional(), }); // ============================================================================ // Inferred Types (Schema-First) // ============================================================================ export type CustomerAddress = z.infer; export type PortalUser = z.infer; export type CustomerProfile = z.infer; export type UserProfile = z.infer; // ... all other types ``` #### 1.2 Create Portal Provider **Directory:** `packages/domain/customer/providers/portal/` **File:** `packages/domain/customer/providers/portal/types.ts` ```typescript import type { UserRole } from "../../types"; /** * Raw Prisma user data * This interface matches Prisma schema but doesn't depend on @prisma/client */ export interface PrismaUserRaw { id: string; email: string; passwordHash: string | null; role: UserRole; mfaSecret: string | null; emailVerified: boolean; failedLoginAttempts: number; lockedUntil: Date | null; lastLoginAt: Date | null; createdAt: Date; updatedAt: Date; } ``` **File:** `packages/domain/customer/providers/portal/mapper.ts` ```typescript import type { PrismaUserRaw } from "./types"; import type { PortalUser } from "../../types"; /** * Maps raw Prisma user data to PortalUser domain type */ export function mapPrismaUserToPortalUser(raw: PrismaUserRaw): PortalUser { return { id: raw.id, email: raw.email, role: raw.role, mfaEnabled: !!raw.mfaSecret, emailVerified: raw.emailVerified, lastLoginAt: raw.lastLoginAt?.toISOString(), createdAt: raw.createdAt.toISOString(), updatedAt: raw.updatedAt.toISOString(), }; } ``` **File:** `packages/domain/customer/providers/portal/index.ts` ```typescript export * from "./mapper"; export * from "./types"; ``` #### 1.3 Update Customer Domain Index **File:** `packages/domain/customer/index.ts` ```typescript /** * Customer Domain * * Contains all user/customer entity types and their providers: * - PortalUser: Auth state from portal DB (via Prisma) * - CustomerProfile: Profile data from WHMCS * - UserProfile: Complete user (PortalUser + CustomerProfile) */ // Export all types export type { // Core types PortalUser, CustomerProfile, UserProfile, CustomerAddress, // ... all other types } from "./types"; // Export schemas for validation export { portalUserSchema, customerProfileSchema, userProfileSchema, customerAddressSchema, // ... all other schemas } from "./types"; // Export providers export * as Providers from "./providers"; ``` #### 1.4 Update Providers Index **File:** `packages/domain/customer/providers/index.ts` ```typescript import * as PortalModule from "./portal"; import * as WhmcsModule from "./whmcs"; export const Portal = PortalModule; export const Whmcs = WhmcsModule; export { PortalModule, WhmcsModule }; ``` ### Phase 2: Clean Up Auth Domain #### 2.1 Remove user entity types from auth **File:** `packages/domain/auth/types.ts` (create new, consolidate schema.ts + contract.ts) Remove: - `portalUserSchema` → moved to customer domain - `userProfileSchema` → moved to customer domain - `PortalUser` type → moved to customer domain - `UserProfile` / `AuthenticatedUser` type → moved to customer domain Keep ONLY: - Authentication mechanism schemas (login, signup, password reset, MFA) - Token schemas (authTokensSchema, refreshTokenSchema) - Auth response schemas (authResponseSchema references UserProfile from customer domain) Structure: ```typescript import { z } from "zod"; import { userProfileSchema } from "../customer/types"; // ============================================================================ // Request Schemas (Authentication Mechanisms) // ============================================================================ export const loginRequestSchema = z.object({...}); export const signupRequestSchema = z.object({...}); export const passwordResetRequestSchema = z.object({...}); // ... all auth request schemas // ============================================================================ // Token Schemas // ============================================================================ export const authTokensSchema = z.object({ accessToken: z.string(), refreshToken: z.string(), expiresAt: z.string(), refreshExpiresAt: z.string(), tokenType: z.literal("Bearer"), }); // ============================================================================ // Response Schemas (Reference UserProfile from Customer Domain) // ============================================================================ export const authResponseSchema = z.object({ user: userProfileSchema, // from customer domain tokens: authTokensSchema, }); export const signupResultSchema = z.object({ user: userProfileSchema, tokens: authTokensSchema, }); // ============================================================================ // Inferred Types // ============================================================================ export type LoginRequest = z.infer; export type AuthTokens = z.infer; export type AuthResponse = z.infer; export type SignupResult = z.infer; // ... all other auth types ``` #### 2.2 Delete auth/schema.ts and auth/contract.ts After consolidating into `auth/types.ts`, remove: - `packages/domain/auth/schema.ts` - `packages/domain/auth/contract.ts` #### 2.3 Delete auth/providers/ Remove `packages/domain/auth/providers/` entirely (Portal provider moved to customer domain) #### 2.4 Update Auth Domain Index **File:** `packages/domain/auth/index.ts` ```typescript /** * Auth Domain * * Contains ONLY authentication mechanisms: * - Login, Signup, Password Management * - Token Management (JWT) * - MFA, SSO * * User entity types are in customer domain. */ export type { LoginRequest, SignupRequest, AuthTokens, AuthResponse, SignupResult, PasswordChangeResult, // ... all auth mechanism types } from "./types"; export { loginRequestSchema, signupRequestSchema, authTokensSchema, authResponseSchema, // ... all auth schemas } from "./types"; ``` ### Phase 3: Update BFF Layer #### 3.1 Update BFF User Mapper **File:** `apps/bff/src/infra/mappers/user.mapper.ts` ```typescript import type { User as PrismaUser } from "@prisma/client"; import { Providers as CustomerProviders } from "@customer-portal/domain/customer"; import type { PortalUser } from "@customer-portal/domain/customer"; /** * Adapter: Converts Prisma User to domain PortalUser */ export function mapPrismaUserToDomain(user: PrismaUser): PortalUser { const prismaUserRaw: CustomerProviders.Portal.PrismaUserRaw = { id: user.id, email: user.email, passwordHash: user.passwordHash, role: user.role, mfaSecret: user.mfaSecret, emailVerified: user.emailVerified, failedLoginAttempts: user.failedLoginAttempts, lockedUntil: user.lockedUntil, lastLoginAt: user.lastLoginAt, createdAt: user.createdAt, updatedAt: user.updatedAt, }; return CustomerProviders.Portal.mapPrismaUserToPortalUser(prismaUserRaw); } ``` #### 3.2 Update Auth Workflow Services Update imports in: - `apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts` - `apps/bff/src/modules/auth/infra/workflows/workflows/password-workflow.service.ts` - `apps/bff/src/modules/auth/application/auth.facade.ts` Change: ```typescript // OLD import type { UserProfile, SignupResult } from "@customer-portal/domain/auth"; // NEW import type { UserProfile } from "@customer-portal/domain/customer"; import type { SignupResult } from "@customer-portal/domain/auth"; ``` #### 3.3 Update Users Service **File:** `apps/bff/src/modules/users/users.service.ts` Construct UserProfile directly (already doing this!): ```typescript import { type PortalUser, type CustomerProfile, type UserProfile, } from "@customer-portal/domain/customer"; async getProfile(userId: string): Promise { const user = await this.prisma.user.findUnique({ where: { id: userId } }); const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId); // Construct UserProfile directly const profile: UserProfile = { // Auth state from portal id: user.id, email: client.email, role: user.role, emailVerified: user.emailVerified, mfaEnabled: user.mfaSecret !== null, lastLoginAt: user.lastLoginAt?.toISOString(), createdAt: user.createdAt.toISOString(), updatedAt: user.updatedAt.toISOString(), // Profile from WHMCS firstname: client.firstname || null, lastname: client.lastname || null, fullname: client.fullname || null, companyname: client.companyName || null, phonenumber: client.phoneNumber || null, address: client.address || undefined, language: client.language || null, currencyCode: client.currencyCode || null, }; return profile; } ``` #### 3.4 Update JWT Strategy **File:** `apps/bff/src/modules/auth/presentation/strategies/jwt.strategy.ts` ```typescript import type { PortalUser } from "@customer-portal/domain/customer"; async validate(payload: JwtPayload): Promise { const user = await this.prisma.user.findUnique({...}); return this.userMapper.mapPrismaUserToDomain(user); // Returns PortalUser } ``` ### Phase 4: Delete Old Files #### 4.1 Delete from customer domain - `packages/domain/customer/schema.ts` (consolidated into types.ts) - `packages/domain/customer/contract.ts` (consolidated into types.ts) #### 4.2 Delete from auth domain - `packages/domain/auth/schema.ts` (consolidated into types.ts) - `packages/domain/auth/contract.ts` (consolidated into types.ts) - `packages/domain/auth/providers/` (moved to customer domain) #### 4.3 Delete from BFF (already done in previous cleanup) - Redundant type files already removed ### Phase 5: Verification #### 5.1 TypeScript Compilation ```bash cd packages/domain && npx tsc --noEmit cd apps/bff && npx tsc --noEmit ``` #### 5.2 Import Verification Verify: - ✅ No imports of `PortalUser` or `UserProfile` from `@customer-portal/domain/auth` - ✅ All user entity imports come from `@customer-portal/domain/customer` - ✅ Auth domain only exports authentication mechanism types - ✅ BFF constructs UserProfile directly via object creation #### 5.3 Provider Pattern Consistency Verify all domains follow provider pattern: - ✅ `customer/providers/portal/` - Prisma → PortalUser - ✅ `customer/providers/whmcs/` - WHMCS → CustomerProfile - ✅ Other domains (billing, subscriptions) have consistent provider structure ## Summary of Changes ### What's Moving - `PortalUser` type: `auth/` → `customer/` - `UserProfile` type: `auth/` → `customer/` - Portal (Prisma) provider: `auth/providers/prisma/` → `customer/providers/portal/` - All schemas: `*/schema.ts` + `*/contract.ts` → `*/types.ts` (consolidated) ### What's Staying - Auth domain: Only authentication mechanisms (login, signup, tokens, password, MFA) - Customer domain: All user/customer entity types + providers ### New Components - `customer/types.ts` - Single file with all schemas and types (including PortalUser, UserProfile) - `customer/providers/portal/` - Portal DB provider for PortalUser - `auth/types.ts` - Single file with all auth mechanism types (no user entities) ## Benefits 1. **Clear Separation of Concerns** - Auth = "How do I authenticate?" (mechanisms) - Customer = "Who am I?" (entities) 2. **Provider Pattern Consistency** - Portal provider (Prisma) alongside WHMCS provider - Same entity, different data sources 3. **Simplified File Structure** - One `types.ts` instead of `schema.ts` + `contract.ts` - Less file navigation 4. **Simple & Direct** - No unnecessary abstraction layers - Direct object construction with type safety 5. **Standard DDD** - Customer is the aggregate root - Auth is a mechanism that references customer