218 lines
6.7 KiB
TypeScript
218 lines
6.7 KiB
TypeScript
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<Omit<CreateBusinessPrefixInput, 'actorId'>> & {
|
|
actorId?: string;
|
|
};
|
|
|
|
export interface BusinessPrefixesRepository {
|
|
list(query?: { keyword?: string; status?: BusinessPrefixStatus }): Promise<BusinessPrefixSummary[]>;
|
|
get(prefixId: string): Promise<BusinessPrefixSummary>;
|
|
create(input: CreateBusinessPrefixInput): Promise<BusinessPrefixSummary>;
|
|
update(prefixId: string, input: UpdateBusinessPrefixInput): Promise<BusinessPrefixSummary>;
|
|
setStatus(prefixId: string, status: BusinessPrefixStatus, actorId?: string): Promise<BusinessPrefixSummary>;
|
|
softDelete(prefixId: string, actorId?: string): Promise<BusinessPrefixSummary>;
|
|
}
|
|
|
|
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<BusinessPrefixSummary[]> {
|
|
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<BusinessPrefixSummary> {
|
|
return this.toSummary(await this.findActiveOrThrow(prefixId));
|
|
}
|
|
|
|
async create(input: CreateBusinessPrefixInput): Promise<BusinessPrefixSummary> {
|
|
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<BusinessPrefixSummary> {
|
|
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<BusinessPrefixSummary> {
|
|
return this.update(prefixId, { status, actorId });
|
|
}
|
|
|
|
async softDelete(prefixId: string, actorId?: string): Promise<BusinessPrefixSummary> {
|
|
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<void> {
|
|
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
|
|
};
|
|
}
|
|
}
|