Assist_Design/CUSTOMER-DOMAIN-REFACTOR.md

542 lines
17 KiB
Markdown
Raw Normal View History

# 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<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`
```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<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`
```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<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`
```typescript
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
```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