168 lines
4.9 KiB
Markdown
168 lines
4.9 KiB
Markdown
# Invoice Validation Cleanup Plan
|
|
|
|
## Problem Statement
|
|
|
|
The `InvoiceValidatorService` contains redundant validation logic that duplicates what Zod schemas already provide.
|
|
|
|
## Current Redundant Code
|
|
|
|
### ❌ In BFF: `apps/bff/src/modules/invoices/validators/invoice-validator.service.ts`
|
|
|
|
```typescript
|
|
// ALL OF THESE ARE REDUNDANT:
|
|
validateInvoiceId(invoiceId: number): void {
|
|
if (!invoiceId || invoiceId < 1) throw new BadRequestException("Invalid invoice ID");
|
|
}
|
|
|
|
validateUserId(userId: string): void {
|
|
if (!userId || typeof userId !== "string" || userId.trim().length === 0) {
|
|
throw new BadRequestException("Invalid user ID");
|
|
}
|
|
}
|
|
|
|
validatePagination(options: Partial<InvoiceListQuery>): void {
|
|
if (page < 1) throw new BadRequestException("Page must be greater than 0");
|
|
if (limit < min || limit > max) throw new BadRequestException(`Limit must be between...`);
|
|
}
|
|
|
|
validateInvoiceStatus(status: string): InvoiceStatus {
|
|
if (!isValidInvoiceStatus(status)) throw new BadRequestException(`Invalid status...`);
|
|
return status as InvoiceStatus;
|
|
}
|
|
|
|
validateWhmcsClientId(clientId: number | undefined): void {
|
|
if (!clientId || clientId < 1) throw new BadRequestException("Invalid WHMCS client ID");
|
|
}
|
|
|
|
validatePaymentGateway(gatewayName: string): void {
|
|
if (!gatewayName || typeof gatewayName !== "string" || gatewayName.trim().length === 0) {
|
|
throw new BadRequestException("Invalid payment gateway name");
|
|
}
|
|
}
|
|
|
|
validateGetInvoicesOptions(options: InvoiceListQuery): InvoiceValidationResult {
|
|
// Calls the above functions - all redundant!
|
|
}
|
|
```
|
|
|
|
### ✅ Already Exists: `packages/domain/billing/schema.ts`
|
|
|
|
```typescript
|
|
// THESE ALREADY HANDLE ALL VALIDATION:
|
|
export const invoiceSchema = z.object({
|
|
id: z.number().int().positive("Invoice id must be positive"),
|
|
// ...
|
|
});
|
|
|
|
export const invoiceListQuerySchema = z.object({
|
|
page: z.coerce.number().int().positive().optional(),
|
|
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
status: invoiceListStatusSchema.optional(),
|
|
});
|
|
```
|
|
|
|
## Solution: Use Schemas Directly
|
|
|
|
### Step 1: Add Missing Schemas to Domain
|
|
|
|
Only ONE new schema needed:
|
|
|
|
```typescript
|
|
// packages/domain/common/validation.ts (add to existing file)
|
|
export const urlSchema = z.string().url();
|
|
|
|
export function validateUrl(url: string): { isValid: boolean; errors: string[] } {
|
|
const result = urlSchema.safeParse(url);
|
|
return {
|
|
isValid: result.success,
|
|
errors: result.success ? [] : result.error.issues.map(i => i.message),
|
|
};
|
|
}
|
|
```
|
|
|
|
### Step 2: Use Schemas at Controller/Entry Point
|
|
|
|
```typescript
|
|
// apps/bff/src/modules/invoices/invoices.controller.ts
|
|
import { invoiceListQuerySchema } from "@customer-portal/domain/billing";
|
|
import { ZodValidationPipe } from "@bff/core/validation";
|
|
|
|
@Controller("invoices")
|
|
export class InvoicesController {
|
|
@Get()
|
|
async getInvoices(
|
|
@Query(new ZodValidationPipe(invoiceListQuerySchema)) query: InvoiceListQuery,
|
|
@Request() req: RequestWithUser
|
|
) {
|
|
// query is already validated by Zod pipe!
|
|
// No need for validator service
|
|
return this.invoicesService.getInvoices(req.user.id, query);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 3: Delete InvoiceValidatorService
|
|
|
|
The entire service can be removed:
|
|
- ❌ Delete `apps/bff/src/modules/invoices/validators/invoice-validator.service.ts`
|
|
- ❌ Delete `apps/bff/src/modules/invoices/types/invoice-service.types.ts` (InvoiceValidationResult)
|
|
- ✅ Use Zod schemas + `ZodValidationPipe` instead
|
|
|
|
## Migration Steps
|
|
|
|
### A. Add URL Validation to Domain (Only Useful One)
|
|
|
|
1. Add `urlSchema` and `validateUrl()` to `packages/domain/common/validation.ts`
|
|
2. Export from `packages/domain/common/index.ts`
|
|
|
|
### B. Update BFF to Use Schemas Directly
|
|
|
|
3. Update `InvoiceRetrievalService` to use schemas instead of validator
|
|
4. Update `InvoicesOrchestratorService` to use schemas
|
|
5. Update controller to use `ZodValidationPipe` with domain schemas
|
|
|
|
### C. Remove Redundant Code
|
|
|
|
6. Delete `InvoiceValidatorService`
|
|
7. Delete `InvoiceValidationResult` type
|
|
8. Remove from `InvoicesModule` providers
|
|
|
|
## Benefits
|
|
|
|
✅ Single source of truth (Zod schemas in domain)
|
|
✅ No duplicate validation logic
|
|
✅ Type-safe (schemas generate types)
|
|
✅ Consistent error messages
|
|
✅ Less code to maintain
|
|
|
|
## General Pattern
|
|
|
|
This applies to ALL modules:
|
|
|
|
```typescript
|
|
// ❌ DON'T: Create validator service for field validation
|
|
class XyzValidatorService {
|
|
validateField(value) { ... } // Redundant with schema
|
|
}
|
|
|
|
// ✅ DO: Use Zod schema + validation pipe
|
|
const xyzRequestSchema = z.object({
|
|
field: z.string().min(1),
|
|
});
|
|
|
|
@Post()
|
|
async create(@Body(new ZodValidationPipe(xyzRequestSchema)) body: XyzRequest) {
|
|
// Already validated!
|
|
}
|
|
```
|
|
|
|
**Validator Services should ONLY exist for:**
|
|
- Business logic validation (requires multiple fields or data)
|
|
- Infrastructure validation (requires DB/API calls)
|
|
|
|
**NOT for:**
|
|
- Field format validation (use Zod schemas)
|
|
- Type validation (use Zod schemas)
|
|
- Range validation (use Zod schemas)
|
|
|