2025-08-21 15:24:40 +09:00
|
|
|
import {
|
|
|
|
|
Injectable,
|
|
|
|
|
NotFoundException,
|
|
|
|
|
ConflictException,
|
|
|
|
|
BadRequestException,
|
2025-08-22 17:02:49 +09:00
|
|
|
Inject,
|
2025-08-21 15:24:40 +09:00
|
|
|
} from "@nestjs/common";
|
2025-08-22 17:02:49 +09:00
|
|
|
import { Logger } from "nestjs-pino";
|
2025-08-21 15:24:40 +09:00
|
|
|
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";
|
2025-08-20 18:02:50 +09:00
|
|
|
import {
|
|
|
|
|
UserIdMapping,
|
|
|
|
|
CreateMappingRequest,
|
|
|
|
|
UpdateMappingRequest,
|
|
|
|
|
MappingSearchFilters,
|
|
|
|
|
MappingStats,
|
|
|
|
|
BulkMappingResult,
|
2025-08-21 15:24:40 +09:00
|
|
|
} from "./types/mapping.types";
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class MappingsService {
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly prisma: PrismaService,
|
|
|
|
|
private readonly cacheService: MappingCacheService,
|
|
|
|
|
private readonly validator: MappingValidatorService,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Inject(Logger) private readonly logger: Logger
|
2025-08-20 18:02:50 +09:00
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new user mapping
|
|
|
|
|
*/
|
|
|
|
|
async createMapping(request: CreateMappingRequest): Promise<UserIdMapping> {
|
|
|
|
|
try {
|
|
|
|
|
// Validate request
|
|
|
|
|
const validation = this.validator.validateCreateRequest(request);
|
2025-08-21 15:24:40 +09:00
|
|
|
this.validator.logValidationResult("Create mapping", validation, {
|
|
|
|
|
userId: request.userId,
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
if (!validation.isValid) {
|
2025-08-22 17:02:49 +09:00
|
|
|
throw new BadRequestException(`Invalid mapping data: ${validation.errors.join(", ")}`);
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sanitize input
|
|
|
|
|
const sanitizedRequest = this.validator.sanitizeCreateRequest(request);
|
|
|
|
|
|
|
|
|
|
// Check for conflicts
|
|
|
|
|
const existingMappings = await this.getAllMappingsFromDb();
|
2025-08-27 10:54:05 +09:00
|
|
|
const conflictValidation = this.validator.validateNoConflicts(
|
2025-08-20 18:02:50 +09:00
|
|
|
sanitizedRequest,
|
2025-08-22 17:02:49 +09:00
|
|
|
existingMappings
|
2025-08-20 18:02:50 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!conflictValidation.isValid) {
|
2025-08-22 17:02:49 +09:00
|
|
|
throw new ConflictException(`Mapping conflict: ${conflictValidation.errors.join(", ")}`);
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-08-21 15:24:40 +09:00
|
|
|
throw new BadRequestException("User ID is required");
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
*/
|
2025-08-22 17:02:49 +09:00
|
|
|
async findByWhmcsClientId(whmcsClientId: number): Promise<UserIdMapping | null> {
|
2025-08-20 18:02:50 +09:00
|
|
|
try {
|
|
|
|
|
// Validate WHMCS client ID
|
|
|
|
|
if (!whmcsClientId || whmcsClientId < 1) {
|
2025-08-21 15:24:40 +09:00
|
|
|
throw new BadRequestException("Valid WHMCS client ID is required");
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try cache first
|
|
|
|
|
const cached = await this.cacheService.getByWhmcsClientId(whmcsClientId);
|
|
|
|
|
if (cached) {
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.debug(`Cache hit for WHMCS client mapping: ${whmcsClientId}`);
|
2025-08-20 18:02:50 +09:00
|
|
|
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) {
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.error(`Failed to find mapping for WHMCS client ${whmcsClientId}`, {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find mapping by Salesforce account ID
|
|
|
|
|
*/
|
|
|
|
|
async findBySfAccountId(sfAccountId: string): Promise<UserIdMapping | null> {
|
|
|
|
|
try {
|
|
|
|
|
// Validate Salesforce account ID
|
|
|
|
|
if (!sfAccountId) {
|
2025-08-21 15:24:40 +09:00
|
|
|
throw new BadRequestException("Salesforce account ID is required");
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.error(`Failed to find mapping for SF account ${sfAccountId}`, {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update an existing mapping
|
|
|
|
|
*/
|
2025-08-22 17:02:49 +09:00
|
|
|
async updateMapping(userId: string, updates: UpdateMappingRequest): Promise<UserIdMapping> {
|
2025-08-20 18:02:50 +09:00
|
|
|
try {
|
|
|
|
|
// Validate request
|
|
|
|
|
const validation = this.validator.validateUpdateRequest(userId, updates);
|
2025-08-21 15:24:40 +09:00
|
|
|
this.validator.logValidationResult("Update mapping", validation, {
|
|
|
|
|
userId,
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
if (!validation.isValid) {
|
2025-08-22 17:02:49 +09:00
|
|
|
throw new BadRequestException(`Invalid update data: ${validation.errors.join(", ")}`);
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-08-21 15:24:40 +09:00
|
|
|
if (
|
|
|
|
|
sanitizedUpdates.whmcsClientId &&
|
|
|
|
|
sanitizedUpdates.whmcsClientId !== existing.whmcsClientId
|
|
|
|
|
) {
|
2025-08-22 17:02:49 +09:00
|
|
|
const conflictingMapping = await this.findByWhmcsClientId(sanitizedUpdates.whmcsClientId);
|
2025-08-20 18:02:50 +09:00
|
|
|
if (conflictingMapping && conflictingMapping.userId !== userId) {
|
|
|
|
|
throw new ConflictException(
|
2025-08-22 17:02:49 +09:00
|
|
|
`WHMCS client ${sanitizedUpdates.whmcsClientId} is already mapped to user ${conflictingMapping.userId}`
|
2025-08-20 18:02:50 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-08-21 15:24:40 +09:00
|
|
|
this.validator.logValidationResult("Delete mapping", validation, {
|
|
|
|
|
userId,
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
*/
|
2025-08-22 17:02:49 +09:00
|
|
|
async searchMappings(filters: MappingSearchFilters): Promise<UserIdMapping[]> {
|
2025-08-20 18:02:50 +09:00
|
|
|
try {
|
2025-08-27 10:54:05 +09:00
|
|
|
const whereClause: Record<string, unknown> = {};
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
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) {
|
2025-08-27 10:54:05 +09:00
|
|
|
whereClause.whmcsClientId = filters.hasWhmcsMapping ? { not: null } : null;
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filters.hasSfMapping !== undefined) {
|
|
|
|
|
if (filters.hasSfMapping) {
|
|
|
|
|
whereClause.sfAccountId = { not: null };
|
|
|
|
|
} else {
|
|
|
|
|
whereClause.sfAccountId = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dbMappings = await this.prisma.idMapping.findMany({
|
|
|
|
|
where: whereClause,
|
2025-08-21 15:24:40 +09:00
|
|
|
orderBy: { createdAt: "desc" },
|
2025-08-20 18:02:50 +09:00
|
|
|
});
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
const mappings: UserIdMapping[] = dbMappings.map(mapping => ({
|
2025-08-20 18:02:50 +09:00
|
|
|
userId: mapping.userId,
|
|
|
|
|
whmcsClientId: mapping.whmcsClientId,
|
|
|
|
|
sfAccountId: mapping.sfAccountId || undefined,
|
|
|
|
|
createdAt: mapping.createdAt,
|
|
|
|
|
updatedAt: mapping.updatedAt,
|
|
|
|
|
}));
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.debug(`Found ${mappings.length} mappings matching filters`, filters);
|
2025-08-20 18:02:50 +09:00
|
|
|
return mappings;
|
|
|
|
|
} catch (error) {
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.error("Failed to search mappings", {
|
2025-08-20 18:02:50 +09:00
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
filters,
|
|
|
|
|
});
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get mapping statistics
|
|
|
|
|
*/
|
|
|
|
|
async getMappingStats(): Promise<MappingStats> {
|
|
|
|
|
try {
|
2025-08-22 17:02:49 +09:00
|
|
|
const [totalCount, whmcsCount, sfCount, completeCount] = await Promise.all([
|
|
|
|
|
this.prisma.idMapping.count(),
|
2025-08-27 10:54:05 +09:00
|
|
|
// whmcsClientId is non-nullable; this count equals total mappings
|
|
|
|
|
this.prisma.idMapping.count(),
|
2025-08-22 17:02:49 +09:00
|
|
|
this.prisma.idMapping.count({
|
|
|
|
|
where: { sfAccountId: { not: null } },
|
|
|
|
|
}),
|
2025-08-27 10:54:05 +09:00
|
|
|
// Complete mappings are those with a non-null sfAccountId (whmcsClientId is always present)
|
2025-08-22 17:02:49 +09:00
|
|
|
this.prisma.idMapping.count({
|
2025-08-27 10:54:05 +09:00
|
|
|
where: { sfAccountId: { not: null } },
|
2025-08-22 17:02:49 +09:00
|
|
|
}),
|
|
|
|
|
]);
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
const stats: MappingStats = {
|
|
|
|
|
totalMappings: totalCount,
|
|
|
|
|
whmcsMappings: whmcsCount,
|
|
|
|
|
salesforceMappings: sfCount,
|
|
|
|
|
completeMappings: completeCount,
|
|
|
|
|
orphanedMappings: 0, // Would need to check against actual user records
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.debug("Generated mapping statistics", stats);
|
2025-08-20 18:02:50 +09:00
|
|
|
return stats;
|
|
|
|
|
} catch (error) {
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.error("Failed to get mapping statistics", {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk create mappings
|
|
|
|
|
*/
|
2025-08-22 17:02:49 +09:00
|
|
|
async bulkCreateMappings(mappings: CreateMappingRequest[]): Promise<BulkMappingResult> {
|
2025-08-20 18:02:50 +09:00
|
|
|
const result: BulkMappingResult = {
|
|
|
|
|
successful: 0,
|
|
|
|
|
failed: 0,
|
|
|
|
|
errors: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Validate all mappings first
|
|
|
|
|
const validations = this.validator.validateBulkMappings(mappings);
|
2025-08-21 15:24:40 +09:00
|
|
|
|
2025-08-20 18:02:50 +09:00
|
|
|
for (let i = 0; i < mappings.length; i++) {
|
|
|
|
|
const mapping = mappings[i];
|
|
|
|
|
const validation = validations[i].validation;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!validation.isValid) {
|
2025-08-21 15:24:40 +09:00
|
|
|
throw new Error(validation.errors.join(", "));
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.createMapping(mapping);
|
|
|
|
|
result.successful++;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
result.failed++;
|
|
|
|
|
result.errors.push({
|
|
|
|
|
index: i,
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
data: mapping,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.log(
|
2025-08-22 17:02:49 +09:00
|
|
|
`Bulk create completed: ${result.successful} successful, ${result.failed} failed`
|
2025-08-21 15:24:40 +09:00
|
|
|
);
|
2025-08-20 18:02:50 +09:00
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.error("Bulk create mappings failed", {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if user has mapping
|
|
|
|
|
*/
|
|
|
|
|
async hasMapping(userId: string): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
// Try cache first
|
2025-08-22 17:02:49 +09:00
|
|
|
const cached = await this.cacheService.getByUserId(userId);
|
|
|
|
|
if (cached) {
|
2025-08-20 18:02:50 +09:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check database
|
|
|
|
|
const mapping = await this.prisma.idMapping.findUnique({
|
2025-08-21 15:24:40 +09:00
|
|
|
where: { userId },
|
2025-08-20 18:02:50 +09:00
|
|
|
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> {
|
2025-08-22 17:02:49 +09:00
|
|
|
// Get the current mapping to invalidate all related cache keys
|
|
|
|
|
const mapping = await this.cacheService.getByUserId(userId);
|
|
|
|
|
if (mapping) {
|
|
|
|
|
await this.cacheService.deleteMapping(mapping);
|
|
|
|
|
}
|
2025-08-20 18:02:50 +09:00
|
|
|
this.logger.log(`Invalidated mapping cache for user ${userId}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Health check
|
|
|
|
|
*/
|
2025-08-27 10:54:05 +09:00
|
|
|
async healthCheck(): Promise<{ status: string; details: Record<string, string> }> {
|
2025-08-20 18:02:50 +09:00
|
|
|
try {
|
|
|
|
|
// Test database connectivity
|
|
|
|
|
await this.prisma.idMapping.count();
|
|
|
|
|
|
|
|
|
|
return {
|
2025-08-21 15:24:40 +09:00
|
|
|
status: "healthy",
|
2025-08-20 18:02:50 +09:00
|
|
|
details: {
|
2025-08-21 15:24:40 +09:00
|
|
|
database: "connected",
|
|
|
|
|
cache: "available",
|
2025-08-20 18:02:50 +09:00
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.error("Mapping service health check failed", {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
return {
|
2025-08-21 15:24:40 +09:00
|
|
|
status: "unhealthy",
|
2025-08-20 18:02:50 +09:00
|
|
|
details: {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Private helper methods
|
|
|
|
|
|
|
|
|
|
private async getAllMappingsFromDb(): Promise<UserIdMapping[]> {
|
|
|
|
|
const dbMappings = await this.prisma.idMapping.findMany();
|
2025-08-22 17:02:49 +09:00
|
|
|
return dbMappings.map(mapping => ({
|
2025-08-20 18:02:50 +09:00
|
|
|
userId: mapping.userId,
|
|
|
|
|
whmcsClientId: mapping.whmcsClientId,
|
|
|
|
|
sfAccountId: mapping.sfAccountId || undefined,
|
|
|
|
|
createdAt: mapping.createdAt,
|
|
|
|
|
updatedAt: mapping.updatedAt,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 10:54:05 +09:00
|
|
|
private sanitizeForLog(data: unknown): Record<string, unknown> {
|
|
|
|
|
try {
|
2025-08-28 16:57:57 +09:00
|
|
|
const plain: unknown = JSON.parse(JSON.stringify(data ?? {}));
|
2025-08-27 10:54:05 +09:00
|
|
|
if (plain && typeof plain === "object" && !Array.isArray(plain)) {
|
|
|
|
|
return plain as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
return { value: plain } as Record<string, unknown>;
|
|
|
|
|
} catch {
|
|
|
|
|
return { value: String(data) } as Record<string, unknown>;
|
|
|
|
|
}
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy method support (for backward compatibility)
|
|
|
|
|
*/
|
|
|
|
|
async create(data: CreateMappingRequest): Promise<UserIdMapping> {
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.warn("Using legacy create method - please update to createMapping");
|
2025-08-20 18:02:50 +09:00
|
|
|
return this.createMapping(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy method support (for backward compatibility)
|
|
|
|
|
*/
|
2025-08-22 17:02:49 +09:00
|
|
|
async createMappingLegacy(data: CreateMappingRequest): Promise<UserIdMapping> {
|
|
|
|
|
this.logger.warn("Using legacy createMapping method - please update to createMapping");
|
2025-08-20 18:02:50 +09:00
|
|
|
return this.createMapping(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy method support (for backward compatibility)
|
|
|
|
|
*/
|
2025-08-27 10:54:05 +09:00
|
|
|
async updateMappingLegacy(userId: string, updates: UpdateMappingRequest): Promise<UserIdMapping> {
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.warn("Using legacy updateMapping method - please update to updateMapping");
|
2025-08-20 18:02:50 +09:00
|
|
|
return this.updateMapping(userId, updates);
|
|
|
|
|
}
|
|
|
|
|
}
|