# 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): 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)