256 lines
7.0 KiB
Markdown
256 lines
7.0 KiB
Markdown
# Customer Domain - Type Naming Clarity
|
|
|
|
## The Problem
|
|
|
|
Current naming is confusing:
|
|
- `PortalUser` - What is this?
|
|
- `CustomerProfile` - Is this different from a user?
|
|
- `UserProfile` - How is this different from the above?
|
|
- `AuthenticatedUser` - Another alias?
|
|
|
|
## Proposed Clear Naming
|
|
|
|
### Core Principle: Same Entity, Different Data Sources
|
|
|
|
A customer/user is **ONE business entity** with data from **TWO sources**:
|
|
|
|
1. **Portal Database** → Authentication & account state
|
|
2. **WHMCS** → Profile & billing information
|
|
|
|
## Recommended Schema Structure
|
|
|
|
```typescript
|
|
// packages/domain/customer/types.ts
|
|
|
|
// ============================================================================
|
|
// User Auth State (from Portal Database)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* User authentication and account state stored in portal database
|
|
* Source: Portal DB (Prisma)
|
|
* Contains: Auth-related fields only
|
|
*/
|
|
export const userAuthSchema = z.object({
|
|
id: z.string().uuid(), // Portal user ID (primary key)
|
|
email: z.string().email(), // Email (for login)
|
|
role: z.enum(["USER", "ADMIN"]), // User role
|
|
emailVerified: z.boolean(), // Email verification status
|
|
mfaEnabled: z.boolean(), // MFA enabled flag
|
|
lastLoginAt: z.string().optional(), // Last login timestamp
|
|
createdAt: z.string(), // Account created date
|
|
updatedAt: z.string(), // Account updated date
|
|
});
|
|
|
|
export type UserAuth = z.infer<typeof userAuthSchema>;
|
|
|
|
|
|
// ============================================================================
|
|
// Customer Profile Data (from WHMCS)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Customer profile and billing information from WHMCS
|
|
* Source: WHMCS API
|
|
* Contains: Personal info, address, preferences
|
|
*/
|
|
export const customerDataSchema = z.object({
|
|
whmcsClientId: z.number().int(), // WHMCS client ID
|
|
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(),
|
|
// ... other WHMCS fields
|
|
});
|
|
|
|
export type CustomerData = z.infer<typeof customerDataSchema>;
|
|
|
|
|
|
// ============================================================================
|
|
// Complete User (Auth + Profile Data)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Complete user profile combining auth state and customer data
|
|
* Composition: UserAuth (portal) + CustomerData (WHMCS)
|
|
* This is what gets returned in API responses
|
|
*/
|
|
export const userSchema = userAuthSchema.extend({
|
|
// Add customer data 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(),
|
|
});
|
|
|
|
/**
|
|
* Complete user - exported as 'User' for simplicity
|
|
* Use this in API responses and business logic
|
|
*/
|
|
export type User = z.infer<typeof userSchema>;
|
|
```
|
|
|
|
## Naming Comparison
|
|
|
|
### Option A: Auth/Data (Recommended)
|
|
```typescript
|
|
UserAuth // Auth state from portal DB
|
|
CustomerData // Profile data from WHMCS
|
|
User // Complete entity
|
|
```
|
|
|
|
**Pros:**
|
|
- Clear distinction: "Auth" = authentication, "Data" = profile information
|
|
- Simple exports: `User` is the main type
|
|
- Intuitive: "I need UserAuth for JWT" vs "I need User for profile API"
|
|
|
|
**Cons:**
|
|
- "CustomerData" might seem weird (but it's accurate - it's WHMCS customer data)
|
|
|
|
### Option B: Portal/Customer (Current-ish)
|
|
```typescript
|
|
PortalUser // Auth state from portal DB
|
|
CustomerProfile // Profile data from WHMCS
|
|
User // Complete entity
|
|
```
|
|
|
|
**Pros:**
|
|
- "Portal" clearly indicates source
|
|
- "CustomerProfile" is business-domain language
|
|
|
|
**Cons:**
|
|
- "PortalUser" vs "User" - what's the difference?
|
|
- Implies there are different "user" entities (there's only ONE user)
|
|
|
|
### Option C: Auth/Profile (Simple)
|
|
```typescript
|
|
AuthUser // Auth state
|
|
ProfileData // Profile info
|
|
User // Complete entity
|
|
```
|
|
|
|
**Pros:**
|
|
- Very simple and clear
|
|
- Purpose-driven naming
|
|
|
|
**Cons:**
|
|
- Less obvious where data comes from
|
|
|
|
### Option D: Keep Descriptive (Verbose but Clear)
|
|
```typescript
|
|
UserAuthState // Auth state from portal DB
|
|
CustomerProfileData // Profile data from WHMCS
|
|
UserWithProfile // Complete entity
|
|
```
|
|
|
|
**Pros:**
|
|
- Extremely explicit
|
|
- No ambiguity
|
|
|
|
**Cons:**
|
|
- Verbose
|
|
- "UserWithProfile" is awkward
|
|
|
|
## Recommended: Option A
|
|
|
|
```typescript
|
|
// Clear and intuitive
|
|
import type { UserAuth, CustomerData, User } from "@customer-portal/domain/customer";
|
|
|
|
// In JWT strategy - only need auth state
|
|
async validate(payload: JwtPayload): Promise<UserAuth> {
|
|
return this.getAuthState(payload.sub);
|
|
}
|
|
|
|
// In profile API - need complete user
|
|
async getProfile(userId: string): Promise<User> {
|
|
const auth = await this.getAuthState(userId);
|
|
const data = await this.getCustomerData(userId);
|
|
|
|
return {
|
|
...auth,
|
|
...data,
|
|
};
|
|
}
|
|
```
|
|
|
|
## Provider Structure
|
|
|
|
```
|
|
packages/domain/customer/providers/
|
|
├── portal/
|
|
│ ├── types.ts # PrismaUserRaw interface
|
|
│ └── mapper.ts # Prisma → UserAuth
|
|
│
|
|
└── whmcs/
|
|
├── types.ts # WhmcsClientRaw interface
|
|
└── mapper.ts # WHMCS → CustomerData
|
|
```
|
|
|
|
## What Gets Exported
|
|
|
|
```typescript
|
|
// packages/domain/customer/index.ts
|
|
|
|
export type {
|
|
// Core types (use these in your code)
|
|
User, // Complete user (UserAuth + CustomerData)
|
|
UserAuth, // Auth state only (for JWT, auth checks)
|
|
CustomerData, // Profile data only (rarely used directly)
|
|
|
|
// Supporting types
|
|
CustomerAddress,
|
|
// ... etc
|
|
} from "./types";
|
|
|
|
export {
|
|
// Schemas for validation
|
|
userSchema,
|
|
userAuthSchema,
|
|
customerDataSchema,
|
|
// ... etc
|
|
} from "./types";
|
|
|
|
export * as Providers from "./providers";
|
|
```
|
|
|
|
## Auth Domain References User
|
|
|
|
```typescript
|
|
// packages/domain/auth/types.ts
|
|
|
|
import { userSchema } from "../customer/types";
|
|
|
|
export const authResponseSchema = z.object({
|
|
user: userSchema, // Reference the User schema from customer domain
|
|
tokens: authTokensSchema,
|
|
});
|
|
|
|
export type AuthResponse = z.infer<typeof authResponseSchema>;
|
|
```
|
|
|
|
## Summary
|
|
|
|
**Entity**: ONE user/customer entity
|
|
|
|
**Data Sources**:
|
|
- `UserAuth` - From portal database (auth state)
|
|
- `CustomerData` - From WHMCS (profile data)
|
|
|
|
**Combined**:
|
|
- `User` - Complete entity (auth + profile)
|
|
|
|
**Usage**:
|
|
- JWT validation → `UserAuth`
|
|
- API responses → `User`
|
|
- Internal mapping → `CustomerData` (via providers)
|
|
|