feat: complete phase2 baseline cdr quality rbac
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user