Assist_Design/apps/bff/src/mappings/mappings.service.ts

606 lines
17 KiB
TypeScript
Raw Normal View History

import {
Injectable,
Logger,
NotFoundException,
ConflictException,
BadRequestException
} from '@nestjs/common';
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,
BulkMappingOperation,
BulkMappingResult,
} from './types/mapping.types';
@Injectable()
export class MappingsService {
private readonly logger = new Logger(MappingsService.name);
constructor(
private readonly prisma: PrismaService,
private readonly cacheService: MappingCacheService,
private readonly validator: MappingValidatorService,
) {}
/**
* Create a new user mapping
*/
async createMapping(request: CreateMappingRequest): Promise<UserIdMapping> {
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<UserIdMapping | null> {
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<UserIdMapping | null> {
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<UserIdMapping | null> {
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<UserIdMapping> {
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<void> {
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<UserIdMapping[]> {
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<MappingStats> {
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<BulkMappingResult> {
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<boolean> {
try {
// Try cache first
const hasCache = await this.cacheService.hasMapping(userId);
if (hasCache) {
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<void> {
await this.cacheService.invalidateUserMapping(userId);
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<UserIdMapping[]> {
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<UserIdMapping> {
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<UserIdMapping> {
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<UserIdMapping> {
this.logger.warn('Using legacy updateMapping method - please update to updateMapping');
return this.updateMapping(userId, updates);
}
}