import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { CreateMappingRequest, UpdateMappingRequest, MappingValidationResult, UserIdMapping, } from "../types/mapping.types"; @Injectable() export class MappingValidatorService { constructor(@Inject(Logger) private readonly logger: Logger) {} /** * Validate create mapping request */ validateCreateRequest(request: CreateMappingRequest): MappingValidationResult { const errors: string[] = []; const warnings: string[] = []; // Validate user ID if (!request.userId) { errors.push("User ID is required"); } else if (!this.isValidUuid(request.userId)) { errors.push("User ID must be a valid UUID"); } // Validate WHMCS client ID if (!request.whmcsClientId) { errors.push("WHMCS client ID is required"); } else if (!Number.isInteger(request.whmcsClientId) || request.whmcsClientId < 1) { errors.push("WHMCS client ID must be a positive integer"); } // Validate Salesforce account ID (optional) if (request.sfAccountId) { if (!this.isValidSalesforceId(request.sfAccountId)) { errors.push("Salesforce account ID must be a valid 15 or 18 character ID"); } } else { warnings.push("Salesforce account ID not provided - mapping will be incomplete"); } return { isValid: errors.length === 0, errors, warnings, }; } /** * Validate update mapping request */ validateUpdateRequest(userId: string, request: UpdateMappingRequest): MappingValidationResult { const errors: string[] = []; const warnings: string[] = []; // Validate user ID if (!userId) { errors.push("User ID is required"); } else if (!this.isValidUuid(userId)) { errors.push("User ID must be a valid UUID"); } // Check if there's something to update if (!request.whmcsClientId && !request.sfAccountId) { errors.push("At least one field must be provided for update"); } // Validate WHMCS client ID (if provided) if (request.whmcsClientId !== undefined) { if (!Number.isInteger(request.whmcsClientId) || request.whmcsClientId < 1) { errors.push("WHMCS client ID must be a positive integer"); } } // Validate Salesforce account ID (if provided) if (request.sfAccountId !== undefined) { if (request.sfAccountId && !this.isValidSalesforceId(request.sfAccountId)) { errors.push("Salesforce account ID must be a valid 15 or 18 character ID"); } } return { isValid: errors.length === 0, errors, warnings, }; } /** * Validate existing mapping for consistency */ validateExistingMapping(mapping: UserIdMapping): MappingValidationResult { const errors: string[] = []; const warnings: string[] = []; // Validate user ID if (!mapping.userId || !this.isValidUuid(mapping.userId)) { errors.push("Invalid user ID in existing mapping"); } // Validate WHMCS client ID if ( !mapping.whmcsClientId || !Number.isInteger(mapping.whmcsClientId) || mapping.whmcsClientId < 1 ) { errors.push("Invalid WHMCS client ID in existing mapping"); } // Validate Salesforce account ID (if present) if (mapping.sfAccountId && !this.isValidSalesforceId(mapping.sfAccountId)) { errors.push("Invalid Salesforce account ID in existing mapping"); } // Check completeness if (!mapping.sfAccountId) { warnings.push("Mapping is missing Salesforce account ID"); } return { isValid: errors.length === 0, errors, warnings, }; } /** * Validate array of mappings for bulk operations */ validateBulkMappings(mappings: CreateMappingRequest[]): Array<{ index: number; validation: MappingValidationResult; }> { return mappings.map((mapping, index) => ({ index, validation: this.validateCreateRequest(mapping), })); } /** * Check for potential conflicts */ async validateNoConflicts( request: CreateMappingRequest, existingMappings: UserIdMapping[] ): Promise { const errors: string[] = []; const warnings: string[] = []; // Check for duplicate user ID const duplicateUser = existingMappings.find(m => m.userId === request.userId); if (duplicateUser) { errors.push(`User ${request.userId} already has a mapping`); } // Check for duplicate WHMCS client ID const duplicateWhmcs = existingMappings.find(m => m.whmcsClientId === request.whmcsClientId); if (duplicateWhmcs) { errors.push( `WHMCS client ${request.whmcsClientId} is already mapped to user ${duplicateWhmcs.userId}` ); } // Check for duplicate Salesforce account ID 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, }; } /** * Validate mapping before deletion */ validateDeletion(mapping: UserIdMapping): MappingValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!mapping) { errors.push("Cannot delete non-existent mapping"); return { isValid: false, errors, warnings }; } // Warning about data impact 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, }; } /** * Sanitize mapping data for safe storage */ sanitizeCreateRequest(request: CreateMappingRequest): CreateMappingRequest { return { userId: request.userId?.trim(), whmcsClientId: Number(request.whmcsClientId), sfAccountId: request.sfAccountId?.trim() || undefined, }; } /** * Sanitize update request */ sanitizeUpdateRequest(request: UpdateMappingRequest): UpdateMappingRequest { const sanitized: UpdateMappingRequest = {}; if (request.whmcsClientId !== undefined) { sanitized.whmcsClientId = Number(request.whmcsClientId); } if (request.sfAccountId !== undefined) { sanitized.sfAccountId = request.sfAccountId?.trim() || undefined; } return sanitized; } // Private validation helpers private isValidUuid(uuid: string): boolean { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(uuid); } private isValidSalesforceId(sfId: string): boolean { // Salesforce IDs are 15 or 18 characters long // 15-character: case-sensitive // 18-character: case-insensitive (includes checksum) if (!sfId) return false; const sfIdRegex = /^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/; return sfIdRegex.test(sfId); } /** * Get validation summary for logging */ getValidationSummary(validation: MappingValidationResult): string { const parts: string[] = []; if (validation.isValid) { parts.push("✓ Valid"); } else { parts.push("✗ Invalid"); } if (validation.errors.length > 0) { parts.push(`${validation.errors.length} error(s)`); } if (validation.warnings.length > 0) { parts.push(`${validation.warnings.length} warning(s)`); } return parts.join(", "); } /** * Log validation result */ logValidationResult(operation: string, validation: MappingValidationResult, context?: any): void { const summary = this.getValidationSummary(validation); if (validation.isValid) { this.logger.debug(`${operation} validation: ${summary}`, context); } else { this.logger.warn(`${operation} validation failed: ${summary}`, { ...context, errors: validation.errors, warnings: validation.warnings, }); } } }