Assist_Design/docs/_archive/INVOICE-VALIDATION-CLEANUP-PLAN.md

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 + 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

  1. Update InvoiceRetrievalService to use schemas instead of validator
  2. Update InvoicesOrchestratorService to use schemas
  3. Update controller to use ZodValidationPipe with domain schemas

C. Remove Redundant Code

  1. Delete InvoiceValidatorService
  2. Delete InvoiceValidationResult type
  3. 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:

// ❌ 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)