4.9 KiB
4.9 KiB
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
// 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
// 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:
// 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
// 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 +
ZodValidationPipeinstead
Migration Steps
A. Add URL Validation to Domain (Only Useful One)
- Add
urlSchemaandvalidateUrl()topackages/domain/common/validation.ts - Export from
packages/domain/common/index.ts
B. Update BFF to Use Schemas Directly
- Update
InvoiceRetrievalServiceto use schemas instead of validator - Update
InvoicesOrchestratorServiceto use schemas - Update controller to use
ZodValidationPipewith domain schemas
C. Remove Redundant Code
- Delete
InvoiceValidatorService - Delete
InvoiceValidationResulttype - Remove from
InvoicesModuleproviders
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:
// ❌ 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)