import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import crypto from 'node:crypto'; import { Prisma } from '@lisglosips/database'; import { PrismaService } from '../database/prisma.service.js'; export type BusinessPrefixStatus = 'ENABLED' | 'DISABLED'; export interface BusinessPrefixSummary { id: string; prefix: string; name: string; description: string | null; priority: number; status: BusinessPrefixStatus; gatewayCount: number; createdAt: Date; updatedAt: Date; } export interface CreateBusinessPrefixInput { prefix: string; name: string; description?: string | null; priority: number; status?: BusinessPrefixStatus; actorId?: string; } export type UpdateBusinessPrefixInput = Partial> & { actorId?: string; }; export interface BusinessPrefixesRepository { list(query?: { keyword?: string; status?: BusinessPrefixStatus }): Promise; get(prefixId: string): Promise; create(input: CreateBusinessPrefixInput): Promise; update(prefixId: string, input: UpdateBusinessPrefixInput): Promise; setStatus(prefixId: string, status: BusinessPrefixStatus, actorId?: string): Promise; softDelete(prefixId: string, actorId?: string): Promise; } export const BUSINESS_PREFIXES_REPOSITORY = Symbol('BUSINESS_PREFIXES_REPOSITORY'); function businessPrefixId(): string { return `bp_${crypto.randomUUID().replaceAll('-', '').slice(0, 29)}`; } function outboxId(): string { return `out_${crypto.randomUUID().replaceAll('-', '').slice(0, 28)}`; } @Injectable() export class PrismaBusinessPrefixesRepository implements BusinessPrefixesRepository { constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {} async list(query: { keyword?: string; status?: BusinessPrefixStatus } = {}): Promise { const where: Prisma.BusinessPrefixWhereInput = { deletedAt: null, status: query.status, OR: query.keyword ? [{ prefix: { contains: query.keyword } }, { name: { contains: query.keyword } }, { description: { contains: query.keyword } }] : undefined }; const items = await this.prisma.businessPrefix.findMany({ where, orderBy: [{ priority: 'asc' }, { prefix: 'asc' }], include: this.includeSummary() }); return items.map((item) => this.toSummary(item)); } async get(prefixId: string): Promise { return this.toSummary(await this.findActiveOrThrow(prefixId)); } async create(input: CreateBusinessPrefixInput): Promise { const item = await this.prisma.$transaction(async (tx) => { const created = await tx.businessPrefix.create({ data: { id: businessPrefixId(), prefix: input.prefix, name: input.name, description: input.description, priority: input.priority, status: input.status ?? 'ENABLED', createdBy: input.actorId, updatedBy: input.actorId }, include: this.includeSummary() }); await this.enqueueConfigOutbox(tx, created.id, 'business_prefix.created'); return created; }); return this.toSummary(item); } async update(prefixId: string, input: UpdateBusinessPrefixInput): Promise { await this.findActiveOrThrow(prefixId); const item = await this.prisma.$transaction(async (tx) => { const updated = await tx.businessPrefix.update({ where: { id: prefixId }, data: { prefix: input.prefix, name: input.name, description: input.description, priority: input.priority, status: input.status, updatedBy: input.actorId, version: { increment: 1 } }, include: this.includeSummary() }); await this.enqueueConfigOutbox(tx, updated.id, 'business_prefix.updated'); return updated; }); return this.toSummary(item); } async setStatus(prefixId: string, status: BusinessPrefixStatus, actorId?: string): Promise { return this.update(prefixId, { status, actorId }); } async softDelete(prefixId: string, actorId?: string): Promise { const existing = await this.findActiveOrThrow(prefixId); const linkedGateways = await this.prisma.customerGatewayBusinessPrefix.count({ where: { businessPrefixId: prefixId } }); if (linkedGateways > 0) { throw new BadRequestException({ code: 'BUSINESS_PREFIX_IN_USE', message: 'Business prefix in use by customer gateways cannot be deleted.' }); } const item = await this.prisma.$transaction(async (tx) => { const updated = await tx.businessPrefix.update({ where: { id: existing.id }, data: { status: 'DISABLED', deletedAt: new Date(), updatedBy: actorId, version: { increment: 1 } }, include: this.includeSummary() }); await this.enqueueConfigOutbox(tx, updated.id, 'business_prefix.deleted'); return updated; }); return this.toSummary(item); } private async enqueueConfigOutbox(tx: Prisma.TransactionClient, aggregateId: string, eventType: string): Promise { await tx.outboxEvent.create({ data: { id: outboxId(), aggregateType: 'business_prefix_config', aggregateId, eventType, payload: Prisma.JsonNull } }); } private includeSummary() { return { _count: { select: { customerGateways: true } } } satisfies Prisma.BusinessPrefixInclude; } private async findActiveOrThrow(prefixId: string) { const item = await this.prisma.businessPrefix.findUnique({ where: { id: prefixId }, include: this.includeSummary() }); if (!item || item.deletedAt) { throw new NotFoundException({ code: 'BUSINESS_PREFIX_NOT_FOUND', message: 'Business prefix not found.' }); } return item; } private toSummary(item: { id: string; prefix: string; name: string; description: string | null; priority: number; status: BusinessPrefixStatus; createdAt: Date; updatedAt: Date; _count: { customerGateways: number }; }): BusinessPrefixSummary { return { id: item.id, prefix: item.prefix, name: item.name, description: item.description, priority: item.priority, status: item.status, gatewayCount: item._count.customerGateways, createdAt: item.createdAt, updatedAt: item.updatedAt }; } }