import { Injectable, NotFoundException, ConflictException, BadRequestException, Inject, } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { PrismaService } from "../common/prisma/prisma.service"; import { getErrorMessage } from "../common/utils/error.util"; import { MappingCacheService } from "./cache/mapping-cache.service"; import { MappingValidatorService } from "./validation/mapping-validator.service"; import { UserIdMapping, CreateMappingRequest, UpdateMappingRequest, MappingSearchFilters, MappingStats, BulkMappingResult, } from "./types/mapping.types"; @Injectable() export class MappingsService { constructor( private readonly prisma: PrismaService, private readonly cacheService: MappingCacheService, private readonly validator: MappingValidatorService, @Inject(Logger) private readonly logger: Logger ) {} /** * Create a new user mapping */ async createMapping(request: CreateMappingRequest): Promise { try { // Validate request const validation = this.validator.validateCreateRequest(request); this.validator.logValidationResult("Create mapping", validation, { userId: request.userId, }); if (!validation.isValid) { throw new BadRequestException(`Invalid mapping data: ${validation.errors.join(", ")}`); } // Sanitize input const sanitizedRequest = this.validator.sanitizeCreateRequest(request); // Check for conflicts const existingMappings = await this.getAllMappingsFromDb(); const conflictValidation = await this.validator.validateNoConflicts( sanitizedRequest, existingMappings ); if (!conflictValidation.isValid) { throw new ConflictException(`Mapping conflict: ${conflictValidation.errors.join(", ")}`); } // Create in database const created = await this.prisma.idMapping.create({ data: sanitizedRequest, }); const mapping: UserIdMapping = { userId: created.userId, whmcsClientId: created.whmcsClientId, sfAccountId: created.sfAccountId || undefined, createdAt: created.createdAt, updatedAt: created.updatedAt, }; // Cache the new mapping await this.cacheService.setMapping(mapping); this.logger.log(`Created mapping for user ${mapping.userId}`, { whmcsClientId: mapping.whmcsClientId, sfAccountId: mapping.sfAccountId, warnings: validation.warnings, }); return mapping; } catch (error) { this.logger.error(`Failed to create mapping for user ${request.userId}`, { error: getErrorMessage(error), request: this.sanitizeForLog(request), }); throw error; } } /** * Find mapping by user ID */ async findByUserId(userId: string): Promise { try { // Validate user ID if (!userId) { throw new BadRequestException("User ID is required"); } // Try cache first const cached = await this.cacheService.getByUserId(userId); if (cached) { this.logger.debug(`Cache hit for user mapping: ${userId}`); return cached; } // Fetch from database const dbMapping = await this.prisma.idMapping.findUnique({ where: { userId }, }); if (!dbMapping) { this.logger.debug(`No mapping found for user ${userId}`); return null; } const mapping: UserIdMapping = { userId: dbMapping.userId, whmcsClientId: dbMapping.whmcsClientId, sfAccountId: dbMapping.sfAccountId || undefined, createdAt: dbMapping.createdAt, updatedAt: dbMapping.updatedAt, }; // Cache the result await this.cacheService.setMapping(mapping); this.logger.debug(`Found mapping for user ${userId}`, { whmcsClientId: mapping.whmcsClientId, sfAccountId: mapping.sfAccountId, }); return mapping; } catch (error) { this.logger.error(`Failed to find mapping for user ${userId}`, { error: getErrorMessage(error), }); throw error; } } /** * Find mapping by WHMCS client ID */ async findByWhmcsClientId(whmcsClientId: number): Promise { try { // Validate WHMCS client ID if (!whmcsClientId || whmcsClientId < 1) { throw new BadRequestException("Valid WHMCS client ID is required"); } // Try cache first const cached = await this.cacheService.getByWhmcsClientId(whmcsClientId); if (cached) { this.logger.debug(`Cache hit for WHMCS client mapping: ${whmcsClientId}`); return cached; } // Fetch from database const dbMapping = await this.prisma.idMapping.findUnique({ where: { whmcsClientId }, }); if (!dbMapping) { this.logger.debug(`No mapping found for WHMCS client ${whmcsClientId}`); return null; } const mapping: UserIdMapping = { userId: dbMapping.userId, whmcsClientId: dbMapping.whmcsClientId, sfAccountId: dbMapping.sfAccountId || undefined, createdAt: dbMapping.createdAt, updatedAt: dbMapping.updatedAt, }; // Cache the result await this.cacheService.setMapping(mapping); this.logger.debug(`Found mapping for WHMCS client ${whmcsClientId}`, { userId: mapping.userId, sfAccountId: mapping.sfAccountId, }); return mapping; } catch (error) { this.logger.error(`Failed to find mapping for WHMCS client ${whmcsClientId}`, { error: getErrorMessage(error), }); throw error; } } /** * Find mapping by Salesforce account ID */ async findBySfAccountId(sfAccountId: string): Promise { try { // Validate Salesforce account ID if (!sfAccountId) { throw new BadRequestException("Salesforce account ID is required"); } // Try cache first const cached = await this.cacheService.getBySfAccountId(sfAccountId); if (cached) { this.logger.debug(`Cache hit for SF account mapping: ${sfAccountId}`); return cached; } // Fetch from database const dbMapping = await this.prisma.idMapping.findFirst({ where: { sfAccountId }, }); if (!dbMapping) { this.logger.debug(`No mapping found for SF account ${sfAccountId}`); return null; } const mapping: UserIdMapping = { userId: dbMapping.userId, whmcsClientId: dbMapping.whmcsClientId, sfAccountId: dbMapping.sfAccountId || undefined, createdAt: dbMapping.createdAt, updatedAt: dbMapping.updatedAt, }; // Cache the result await this.cacheService.setMapping(mapping); this.logger.debug(`Found mapping for SF account ${sfAccountId}`, { userId: mapping.userId, whmcsClientId: mapping.whmcsClientId, }); return mapping; } catch (error) { this.logger.error(`Failed to find mapping for SF account ${sfAccountId}`, { error: getErrorMessage(error), }); throw error; } } /** * Update an existing mapping */ async updateMapping(userId: string, updates: UpdateMappingRequest): Promise { try { // Validate request const validation = this.validator.validateUpdateRequest(userId, updates); this.validator.logValidationResult("Update mapping", validation, { userId, }); if (!validation.isValid) { throw new BadRequestException(`Invalid update data: ${validation.errors.join(", ")}`); } // Get existing mapping const existing = await this.findByUserId(userId); if (!existing) { throw new NotFoundException(`Mapping not found for user ${userId}`); } // Sanitize input const sanitizedUpdates = this.validator.sanitizeUpdateRequest(updates); // Check for conflicts if WHMCS client ID is being changed if ( sanitizedUpdates.whmcsClientId && sanitizedUpdates.whmcsClientId !== existing.whmcsClientId ) { const conflictingMapping = await this.findByWhmcsClientId(sanitizedUpdates.whmcsClientId); if (conflictingMapping && conflictingMapping.userId !== userId) { throw new ConflictException( `WHMCS client ${sanitizedUpdates.whmcsClientId} is already mapped to user ${conflictingMapping.userId}` ); } } // Update in database const updated = await this.prisma.idMapping.update({ where: { userId }, data: sanitizedUpdates, }); const newMapping: UserIdMapping = { userId: updated.userId, whmcsClientId: updated.whmcsClientId, sfAccountId: updated.sfAccountId || undefined, createdAt: updated.createdAt, updatedAt: updated.updatedAt, }; // Update cache await this.cacheService.updateMapping(existing, newMapping); this.logger.log(`Updated mapping for user ${userId}`, { changes: sanitizedUpdates, warnings: validation.warnings, }); return newMapping; } catch (error) { this.logger.error(`Failed to update mapping for user ${userId}`, { error: getErrorMessage(error), updates: this.sanitizeForLog(updates), }); throw error; } } /** * Delete a mapping */ async deleteMapping(userId: string): Promise { try { // Get existing mapping const existing = await this.findByUserId(userId); if (!existing) { throw new NotFoundException(`Mapping not found for user ${userId}`); } // Validate deletion const validation = this.validator.validateDeletion(existing); this.validator.logValidationResult("Delete mapping", validation, { userId, }); // Delete from database await this.prisma.idMapping.delete({ where: { userId }, }); // Remove from cache await this.cacheService.deleteMapping(existing); this.logger.log(`Deleted mapping for user ${userId}`, { whmcsClientId: existing.whmcsClientId, sfAccountId: existing.sfAccountId, warnings: validation.warnings, }); } catch (error) { this.logger.error(`Failed to delete mapping for user ${userId}`, { error: getErrorMessage(error), }); throw error; } } /** * Search mappings with filters */ async searchMappings(filters: MappingSearchFilters): Promise { try { const whereClause: any = {}; if (filters.userId) { whereClause.userId = filters.userId; } if (filters.whmcsClientId) { whereClause.whmcsClientId = filters.whmcsClientId; } if (filters.sfAccountId) { whereClause.sfAccountId = filters.sfAccountId; } if (filters.hasWhmcsMapping !== undefined) { if (filters.hasWhmcsMapping) { whereClause.whmcsClientId = { not: null }; } else { whereClause.whmcsClientId = null; } } if (filters.hasSfMapping !== undefined) { if (filters.hasSfMapping) { whereClause.sfAccountId = { not: null }; } else { whereClause.sfAccountId = null; } } const dbMappings = await this.prisma.idMapping.findMany({ where: whereClause, orderBy: { createdAt: "desc" }, }); const mappings: UserIdMapping[] = dbMappings.map(mapping => ({ userId: mapping.userId, whmcsClientId: mapping.whmcsClientId, sfAccountId: mapping.sfAccountId || undefined, createdAt: mapping.createdAt, updatedAt: mapping.updatedAt, })); this.logger.debug(`Found ${mappings.length} mappings matching filters`, filters); return mappings; } catch (error) { this.logger.error("Failed to search mappings", { error: getErrorMessage(error), filters, }); throw error; } } /** * Get mapping statistics */ async getMappingStats(): Promise { try { const [totalCount, whmcsCount, sfCount, completeCount] = await Promise.all([ this.prisma.idMapping.count(), this.prisma.idMapping.count({ where: { whmcsClientId: { not: null as any } }, }), this.prisma.idMapping.count({ where: { sfAccountId: { not: null } }, }), this.prisma.idMapping.count({ where: { AND: [{ whmcsClientId: { not: null as any } }, { sfAccountId: { not: null } }], }, }), ]); const stats: MappingStats = { totalMappings: totalCount, whmcsMappings: whmcsCount, salesforceMappings: sfCount, completeMappings: completeCount, orphanedMappings: 0, // Would need to check against actual user records }; this.logger.debug("Generated mapping statistics", stats); return stats; } catch (error) { this.logger.error("Failed to get mapping statistics", { error: getErrorMessage(error), }); throw error; } } /** * Bulk create mappings */ async bulkCreateMappings(mappings: CreateMappingRequest[]): Promise { const result: BulkMappingResult = { successful: 0, failed: 0, errors: [], }; try { // Validate all mappings first const validations = this.validator.validateBulkMappings(mappings); for (let i = 0; i < mappings.length; i++) { const mapping = mappings[i]; const validation = validations[i].validation; try { if (!validation.isValid) { throw new Error(validation.errors.join(", ")); } await this.createMapping(mapping); result.successful++; } catch (error) { result.failed++; result.errors.push({ index: i, error: getErrorMessage(error), data: mapping, }); } } this.logger.log( `Bulk create completed: ${result.successful} successful, ${result.failed} failed` ); return result; } catch (error) { this.logger.error("Bulk create mappings failed", { error: getErrorMessage(error), }); throw error; } } /** * Check if user has mapping */ async hasMapping(userId: string): Promise { try { // Try cache first const cached = await this.cacheService.getByUserId(userId); if (cached) { return true; } // Check database const mapping = await this.prisma.idMapping.findUnique({ where: { userId }, select: { userId: true }, }); return mapping !== null; } catch (error) { this.logger.error(`Failed to check mapping for user ${userId}`, { error: getErrorMessage(error), }); return false; } } /** * Invalidate cache for a user */ async invalidateCache(userId: string): Promise { // Get the current mapping to invalidate all related cache keys const mapping = await this.cacheService.getByUserId(userId); if (mapping) { await this.cacheService.deleteMapping(mapping); } this.logger.log(`Invalidated mapping cache for user ${userId}`); } /** * Health check */ async healthCheck(): Promise<{ status: string; details: any }> { try { // Test database connectivity await this.prisma.idMapping.count(); return { status: "healthy", details: { database: "connected", cache: "available", timestamp: new Date().toISOString(), }, }; } catch (error) { this.logger.error("Mapping service health check failed", { error: getErrorMessage(error), }); return { status: "unhealthy", details: { error: getErrorMessage(error), timestamp: new Date().toISOString(), }, }; } } // Private helper methods private async getAllMappingsFromDb(): Promise { const dbMappings = await this.prisma.idMapping.findMany(); return dbMappings.map(mapping => ({ userId: mapping.userId, whmcsClientId: mapping.whmcsClientId, sfAccountId: mapping.sfAccountId || undefined, createdAt: mapping.createdAt, updatedAt: mapping.updatedAt, })); } private sanitizeForLog(data: any): any { // Remove sensitive data from logs if needed const sanitized = { ...data }; // Add any sanitization logic here return sanitized; } /** * Legacy method support (for backward compatibility) */ async create(data: CreateMappingRequest): Promise { this.logger.warn("Using legacy create method - please update to createMapping"); return this.createMapping(data); } /** * Legacy method support (for backward compatibility) */ async createMappingLegacy(data: CreateMappingRequest): Promise { this.logger.warn("Using legacy createMapping method - please update to createMapping"); return this.createMapping(data); } /** * Legacy method support (for backward compatibility) */ async updateMappingLegacy(userId: string, updates: any): Promise { this.logger.warn("Using legacy updateMapping method - please update to updateMapping"); return this.updateMapping(userId, updates); } }