import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { z } from "zod"; // Simple Zod schemas for mapping validation (matching database types) const createMappingRequestSchema = z.object({ userId: z.string().uuid(), whmcsClientId: z.number().int().positive(), sfAccountId: z.string().optional(), }); const updateMappingRequestSchema = z.object({ whmcsClientId: z.number().int().positive().optional(), sfAccountId: z.string().optional(), }); const userIdMappingSchema = z.object({ id: z.string().uuid(), userId: z.string().uuid(), whmcsClientId: z.number().int().positive(), sfAccountId: z.string().nullable(), createdAt: z.date(), updatedAt: z.date(), }); export type CreateMappingRequest = z.infer; export type UpdateMappingRequest = z.infer; export type UserIdMapping = z.infer; // Legacy interface for backward compatibility export interface MappingValidationResult { isValid: boolean; errors: string[]; warnings: string[]; } @Injectable() export class MappingValidatorService { constructor(@Inject(Logger) private readonly logger: Logger) {} validateCreateRequest(request: CreateMappingRequest): MappingValidationResult { const validationResult = createMappingRequestSchema.safeParse(request); if (validationResult.success) { const warnings: string[] = []; if (!request.sfAccountId) { warnings.push("Salesforce account ID not provided - mapping will be incomplete"); } return { isValid: true, errors: [], warnings }; } const errors = validationResult.error.issues.map(issue => issue.message); this.logger.warn({ request, errors }, "Create mapping request validation failed"); return { isValid: false, errors, warnings: [] }; } validateUpdateRequest(userId: string, request: UpdateMappingRequest): MappingValidationResult { // First validate userId const userIdValidation = z.string().uuid().safeParse(userId); if (!userIdValidation.success) { return { isValid: false, errors: ["User ID must be a valid UUID"], warnings: [], }; } // Then validate the update request const validationResult = updateMappingRequestSchema.safeParse(request); if (validationResult.success) { return { isValid: true, errors: [], warnings: [] }; } const errors = validationResult.error.issues.map(issue => issue.message); this.logger.warn({ userId, request, errors }, "Update mapping request validation failed"); return { isValid: false, errors, warnings: [] }; } validateExistingMapping(mapping: UserIdMapping): MappingValidationResult { const validationResult = userIdMappingSchema.safeParse(mapping); if (validationResult.success) { const warnings: string[] = []; if (!mapping.sfAccountId) { warnings.push("Mapping is missing Salesforce account ID"); } return { isValid: true, errors: [], warnings }; } const errors = validationResult.error.issues.map(issue => issue.message); this.logger.warn({ mapping, errors }, "Existing mapping validation failed"); return { isValid: false, errors, warnings: [] }; } validateBulkMappings( mappings: CreateMappingRequest[] ): Array<{ index: number; validation: MappingValidationResult }> { return mappings.map((mapping, index) => ({ index, validation: this.validateCreateRequest(mapping), })); } validateNoConflicts( request: CreateMappingRequest, existingMappings: UserIdMapping[] ): MappingValidationResult { const errors: string[] = []; const warnings: string[] = []; // First validate the request format const formatValidation = this.validateCreateRequest(request); if (!formatValidation.isValid) { return formatValidation; } // Check for conflicts const duplicateUser = existingMappings.find(m => m.userId === request.userId); if (duplicateUser) { errors.push(`User ${request.userId} already has a mapping`); } const duplicateWhmcs = existingMappings.find(m => m.whmcsClientId === request.whmcsClientId); if (duplicateWhmcs) { errors.push( `WHMCS client ${request.whmcsClientId} is already mapped to user ${duplicateWhmcs.userId}` ); } if (request.sfAccountId) { const duplicateSf = existingMappings.find(m => m.sfAccountId === request.sfAccountId); if (duplicateSf) { warnings.push( `Salesforce account ${request.sfAccountId} is already mapped to user ${duplicateSf.userId}` ); } } return { isValid: errors.length === 0, errors, warnings }; } validateDeletion(mapping: UserIdMapping): MappingValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!mapping) { errors.push("Cannot delete non-existent mapping"); return { isValid: false, errors, warnings }; } // Validate the mapping format const formatValidation = this.validateExistingMapping(mapping); if (!formatValidation.isValid) { return formatValidation; } warnings.push( "Deleting this mapping will prevent access to WHMCS/Salesforce data for this user" ); if (mapping.sfAccountId) { warnings.push( "This mapping includes Salesforce integration - deletion will affect case management" ); } return { isValid: true, errors, warnings }; } sanitizeCreateRequest(request: CreateMappingRequest): CreateMappingRequest { // Use Zod parsing to sanitize and validate const validationResult = createMappingRequestSchema.safeParse({ userId: request.userId?.trim(), whmcsClientId: request.whmcsClientId, sfAccountId: request.sfAccountId?.trim() || undefined, }); if (validationResult.success) { return validationResult.data; } // Fallback to original behavior if validation fails return { userId: request.userId?.trim(), whmcsClientId: request.whmcsClientId, sfAccountId: request.sfAccountId?.trim() || undefined, }; } sanitizeUpdateRequest(request: UpdateMappingRequest): UpdateMappingRequest { const sanitized: any = {}; if (request.whmcsClientId !== undefined) { sanitized.whmcsClientId = request.whmcsClientId; } if (request.sfAccountId !== undefined) { sanitized.sfAccountId = request.sfAccountId?.trim() || undefined; } // Use Zod parsing to validate the sanitized data const validationResult = updateMappingRequestSchema.safeParse(sanitized); if (validationResult.success) { return validationResult.data; } // Fallback to sanitized data if validation fails return sanitized; } }