feat: complete phase2 baseline cdr quality rbac

This commit is contained in:
hectorzhao
2026-06-24 18:40:21 +08:00
parent 7057fd3c42
commit a86de6545f
63 changed files with 7853 additions and 3678 deletions
@@ -0,0 +1,217 @@
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
};
}
}