Assist_Design/CUSTOMER-DOMAIN-REFACTOR.md

17 KiB

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:

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<typeof customerAddressSchema>;
export type PortalUser = z.infer<typeof portalUserSchema>;
export type CustomerProfile = z.infer<typeof customerProfileSchema>;
export type UserProfile = z.infer<typeof userProfileSchema>;
// ... all other types

1.2 Create Portal Provider

Directory: packages/domain/customer/providers/portal/

File: packages/domain/customer/providers/portal/types.ts

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

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

export * from "./mapper";
export * from "./types";

1.3 Update Customer Domain Index

File: packages/domain/customer/index.ts

/**
 * 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

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:

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<typeof loginRequestSchema>;
export type AuthTokens = z.infer<typeof authTokensSchema>;
export type AuthResponse = z.infer<typeof authResponseSchema>;
export type SignupResult = z.infer<typeof signupResultSchema>;
// ... 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

/**
 * 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

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:

// 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!):

import { 
  type PortalUser, 
  type CustomerProfile, 
  type UserProfile,
} from "@customer-portal/domain/customer";

async getProfile(userId: string): Promise<UserProfile> {
  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

import type { PortalUser } from "@customer-portal/domain/customer";

async validate(payload: JwtPayload): Promise<PortalUser> {
  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

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