204 lines
5.7 KiB
TypeScript
204 lines
5.7 KiB
TypeScript
|
|
/**
|
||
|
|
* ID Mapping Domain - Validation
|
||
|
|
*
|
||
|
|
* Pure business validation functions for ID mappings.
|
||
|
|
* These functions contain no infrastructure dependencies (no DB, no HTTP, no logging).
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { z } from "zod";
|
||
|
|
import {
|
||
|
|
createMappingRequestSchema,
|
||
|
|
updateMappingRequestSchema,
|
||
|
|
userIdMappingSchema,
|
||
|
|
} from "./schema";
|
||
|
|
import type {
|
||
|
|
CreateMappingRequest,
|
||
|
|
UpdateMappingRequest,
|
||
|
|
UserIdMapping,
|
||
|
|
MappingValidationResult,
|
||
|
|
} from "./contract";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate a create mapping request format
|
||
|
|
*/
|
||
|
|
export function 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);
|
||
|
|
return { isValid: false, errors, warnings: [] };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate an update mapping request format
|
||
|
|
*/
|
||
|
|
export function 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);
|
||
|
|
return { isValid: false, errors, warnings: [] };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate an existing mapping
|
||
|
|
*/
|
||
|
|
export function 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);
|
||
|
|
return { isValid: false, errors, warnings: [] };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate bulk mappings
|
||
|
|
*/
|
||
|
|
export function validateBulkMappings(
|
||
|
|
mappings: CreateMappingRequest[]
|
||
|
|
): Array<{ index: number; validation: MappingValidationResult }> {
|
||
|
|
return mappings.map((mapping, index) => ({
|
||
|
|
index,
|
||
|
|
validation: validateCreateRequest(mapping),
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate no conflicts exist with existing mappings
|
||
|
|
* Business rule: Each userId, whmcsClientId should be unique
|
||
|
|
*/
|
||
|
|
export function validateNoConflicts(
|
||
|
|
request: CreateMappingRequest,
|
||
|
|
existingMappings: UserIdMapping[]
|
||
|
|
): MappingValidationResult {
|
||
|
|
const errors: string[] = [];
|
||
|
|
const warnings: string[] = [];
|
||
|
|
|
||
|
|
// First validate the request format
|
||
|
|
const formatValidation = 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 };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate deletion constraints
|
||
|
|
* Business rule: Warn about data access impacts
|
||
|
|
*/
|
||
|
|
export function validateDeletion(mapping: UserIdMapping | null | undefined): 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 = 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 };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sanitize and normalize a create mapping request
|
||
|
|
*
|
||
|
|
* Note: This performs basic string trimming before validation.
|
||
|
|
* The schema handles validation; this is purely for data cleanup.
|
||
|
|
*/
|
||
|
|
export function sanitizeCreateRequest(request: CreateMappingRequest): CreateMappingRequest {
|
||
|
|
return {
|
||
|
|
userId: request.userId?.trim(),
|
||
|
|
whmcsClientId: request.whmcsClientId,
|
||
|
|
sfAccountId: request.sfAccountId?.trim() || undefined,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sanitize and normalize an update mapping request
|
||
|
|
*
|
||
|
|
* Note: This performs basic string trimming before validation.
|
||
|
|
* The schema handles validation; this is purely for data cleanup.
|
||
|
|
*/
|
||
|
|
export function sanitizeUpdateRequest(request: UpdateMappingRequest): UpdateMappingRequest {
|
||
|
|
const sanitized: Partial<UpdateMappingRequest> = {};
|
||
|
|
|
||
|
|
if (request.whmcsClientId !== undefined) {
|
||
|
|
sanitized.whmcsClientId = request.whmcsClientId;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (request.sfAccountId !== undefined) {
|
||
|
|
sanitized.sfAccountId = request.sfAccountId?.trim() || undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
return sanitized;
|
||
|
|
}
|
||
|
|
|