T. Narantuya a22b84f128 Refactor and clean up BFF and portal components for improved maintainability
- Removed deprecated files and components from the BFF application, including various auth and catalog services, enhancing code clarity.
- Updated package.json scripts for better organization and streamlined development processes.
- Refactored portal components to improve structure and maintainability, including the removal of unused files and components.
- Enhanced type definitions and imports across the application for consistency and clarity.
2025-09-18 14:52:26 +09:00

380 lines
13 KiB
TypeScript

import {
Injectable,
NotFoundException,
ConflictException,
BadRequestException,
Inject,
} from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { PrismaService } from "@bff/infra/database/prisma.service";
import { getErrorMessage } from "@bff/core/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
) {}
async createMapping(request: CreateMappingRequest): Promise<UserIdMapping> {
try {
const validation = this.validator.validateCreateRequest(request);
this.logger.debug("Validation result: Create mapping", validation);
if (!validation.isValid) {
throw new BadRequestException(`Invalid mapping data: ${validation.errors.join(", ")}`);
}
const sanitizedRequest = this.validator.sanitizeCreateRequest(request);
const [byUser, byWhmcs, bySf] = await Promise.all([
this.prisma.idMapping.findUnique({ where: { userId: sanitizedRequest.userId } }),
this.prisma.idMapping.findUnique({ where: { whmcsClientId: sanitizedRequest.whmcsClientId } }),
sanitizedRequest.sfAccountId
? this.prisma.idMapping.findFirst({ where: { sfAccountId: sanitizedRequest.sfAccountId } })
: Promise.resolve(null),
]);
if (byUser) {
throw new ConflictException(`User ${sanitizedRequest.userId} already has a mapping`);
}
if (byWhmcs) {
throw new ConflictException(
`WHMCS client ${sanitizedRequest.whmcsClientId} is already mapped to user ${byWhmcs.userId}`
);
}
if (bySf) {
this.logger.warn(
`Salesforce account ${sanitizedRequest.sfAccountId} is already mapped to user ${bySf.userId}`
);
}
let created;
try {
created = await this.prisma.idMapping.create({ data: sanitizedRequest });
} catch (e) {
const msg = getErrorMessage(e);
if (msg.includes("P2002") || /unique/i.test(msg)) {
throw new ConflictException("Mapping violates uniqueness constraints");
}
throw e;
}
const mapping: UserIdMapping = {
userId: created.userId,
whmcsClientId: created.whmcsClientId,
sfAccountId: created.sfAccountId || undefined,
createdAt: created.createdAt,
updatedAt: created.updatedAt,
};
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;
}
}
async findBySfAccountId(sfAccountId: string): Promise<UserIdMapping | null> {
try {
if (!sfAccountId) {
throw new BadRequestException("Salesforce Account ID is required");
}
const cached = await this.cacheService.getBySfAccountId(sfAccountId);
if (cached) {
this.logger.debug(`Cache hit for SF account mapping: ${sfAccountId}`);
return cached;
}
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,
};
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;
}
}
async findByUserId(userId: string): Promise<UserIdMapping | null> {
try {
if (!userId) {
throw new BadRequestException("User ID is required");
}
const cached = await this.cacheService.getByUserId(userId);
if (cached) {
this.logger.debug(`Cache hit for user mapping: ${userId}`);
return cached;
}
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,
};
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;
}
}
async findByWhmcsClientId(whmcsClientId: number): Promise<UserIdMapping | null> {
try {
if (!whmcsClientId || whmcsClientId < 1) {
throw new BadRequestException("Valid WHMCS client ID is required");
}
const cached = await this.cacheService.getByWhmcsClientId(whmcsClientId);
if (cached) {
this.logger.debug(`Cache hit for WHMCS client mapping: ${whmcsClientId}`);
return cached;
}
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,
};
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;
}
}
async updateMapping(userId: string, updates: UpdateMappingRequest): Promise<UserIdMapping> {
try {
const validation = this.validator.validateUpdateRequest(userId, updates);
this.logger.debug("Validation result: Update mapping", validation);
if (!validation.isValid) {
throw new BadRequestException(`Invalid update data: ${validation.errors.join(", ")}`);
}
const existing = await this.findByUserId(userId);
if (!existing) {
throw new NotFoundException(`Mapping not found for user ${userId}`);
}
const sanitizedUpdates = this.validator.sanitizeUpdateRequest(updates);
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}`
);
}
}
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,
};
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;
}
}
async deleteMapping(userId: string): Promise<void> {
try {
const existing = await this.findByUserId(userId);
if (!existing) {
throw new NotFoundException(`Mapping not found for user ${userId}`);
}
const validation = this.validator.validateDeletion(existing);
this.logger.debug("Validation result: Delete mapping", validation);
await this.prisma.idMapping.delete({ where: { userId } });
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;
}
}
async searchMappings(filters: MappingSearchFilters): Promise<UserIdMapping[]> {
try {
const whereClause: Record<string, unknown> = {};
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) {
whereClause.whmcsClientId = filters.hasWhmcsMapping ? { not: null } : null;
}
if (filters.hasSfMapping !== undefined) {
whereClause.sfAccountId = filters.hasSfMapping ? { not: null } : 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;
}
}
async getMappingStats(): Promise<MappingStats> {
try {
const [totalCount, whmcsCount, sfCount, completeCount] = await Promise.all([
this.prisma.idMapping.count(),
this.prisma.idMapping.count(),
this.prisma.idMapping.count({ where: { sfAccountId: { not: null } } }),
this.prisma.idMapping.count({ where: { sfAccountId: { not: null } } }),
]);
const stats: MappingStats = {
totalMappings: totalCount,
whmcsMappings: whmcsCount,
salesforceMappings: sfCount,
completeMappings: completeCount,
orphanedMappings: 0,
};
this.logger.debug("Generated mapping statistics", stats);
return stats;
} catch (error) {
this.logger.error("Failed to get mapping statistics", { error: getErrorMessage(error) });
throw error;
}
}
async hasMapping(userId: string): Promise<boolean> {
try {
const cached = await this.cacheService.getByUserId(userId);
if (cached) return true;
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;
}
}
async invalidateCache(userId: string): Promise<void> {
const mapping = await this.cacheService.getByUserId(userId);
if (mapping) {
await this.cacheService.deleteMapping(mapping);
}
this.logger.log(`Invalidated mapping cache for user ${userId}`);
}
private sanitizeForLog(data: unknown): Record<string, unknown> {
try {
const plain: unknown = JSON.parse(JSON.stringify(data ?? {}));
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>;
}
}
}