fix: enforce drainage uniqueness and carrier-specific reporting
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { selectDrainageReportTask } from '../common/drainage-report-task';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { Prisma } from '@prisma/client';
|
||||
@@ -450,17 +451,25 @@ export class ChannelReportingService {
|
||||
}),
|
||||
);
|
||||
const drainageDetails = signature.drainageItems.flatMap((drainageInfo) =>
|
||||
channels
|
||||
.map((channel) => {
|
||||
const existing = signature.reportTasks.find(
|
||||
(task) =>
|
||||
task.reportType === 'drainage' &&
|
||||
task.channelId === channel.id &&
|
||||
task.drainageItemId === drainageInfo.id,
|
||||
channels.flatMap((channel) =>
|
||||
normalizeChannelCarriers(channel.carriers, channel.carrier).flatMap((carrier) => {
|
||||
const tasks = signature.reportTasks.filter(
|
||||
(task) => task.reportType === 'drainage' && task.drainageItemId === drainageInfo.id,
|
||||
);
|
||||
return existing ? { ...existing, signature } : undefined;
|
||||
})
|
||||
.filter(Boolean),
|
||||
const existing = selectDrainageReportTask(tasks, channel.id, carrier);
|
||||
return existing
|
||||
? [
|
||||
{
|
||||
...existing,
|
||||
id: existing.carrier ? existing.id : `virtual:${drainageInfo.id}:${channel.id}:${carrier}`,
|
||||
carrier,
|
||||
virtual: !existing.carrier,
|
||||
signature,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}),
|
||||
),
|
||||
);
|
||||
return [...signatureDetails, ...drainageDetails];
|
||||
})
|
||||
@@ -551,6 +560,9 @@ export class ChannelReportingService {
|
||||
throw new BadRequestException('unsupported report task source entry');
|
||||
}
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
for (const signatureId of [...new Set(data.items.map((item) => item.signatureId))].sort()) {
|
||||
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${signatureId}, 910))`;
|
||||
}
|
||||
const signatureIds = [
|
||||
...new Set(
|
||||
data.items.filter((item) => (item.reportType ?? 'signature') === 'signature').map((item) => item.signatureId),
|
||||
@@ -561,6 +573,7 @@ export class ChannelReportingService {
|
||||
reportType: 'drainage';
|
||||
drainageItemId: string;
|
||||
channelId: string;
|
||||
carrier: string | null;
|
||||
status: string;
|
||||
}> = [];
|
||||
for (const item of data.items) {
|
||||
@@ -577,7 +590,7 @@ export class ChannelReportingService {
|
||||
if (drainageInfo.auditStatus !== 'approved')
|
||||
throw new BadRequestException('引流信息审核通过后才能修改通道报备状态');
|
||||
}
|
||||
const carrier = reportType === 'signature' && item.carrier ? normalizeBusinessCarrier(item.carrier) : null;
|
||||
const carrier = item.carrier ? normalizeBusinessCarrier(item.carrier) : null;
|
||||
if (carrier && !normalizeChannelCarriers(channel.carriers, channel.carrier).includes(carrier)) {
|
||||
throw new BadRequestException('报备运营商不在通道支持范围内');
|
||||
}
|
||||
@@ -587,11 +600,23 @@ export class ChannelReportingService {
|
||||
channelId: item.channelId,
|
||||
reportType,
|
||||
drainageItemId: reportType === 'drainage' ? item.drainageItemId : null,
|
||||
carrier: reportType === 'signature' ? carrier : null,
|
||||
carrier,
|
||||
},
|
||||
});
|
||||
if (reportType === 'drainage' && !existing)
|
||||
throw new BadRequestException('引流信息通道报备任务不存在,请先完成运营审核');
|
||||
if (reportType === 'drainage' && !existing) {
|
||||
const legacy = carrier
|
||||
? await tx.channelSignatureReportTask.findFirst({
|
||||
where: {
|
||||
signatureId: item.signatureId,
|
||||
channelId: item.channelId,
|
||||
reportType,
|
||||
drainageItemId: item.drainageItemId,
|
||||
carrier: null,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
if (!legacy) throw new BadRequestException('引流信息通道报备任务不存在,请先完成运营审核');
|
||||
}
|
||||
if (reportType === 'signature' && !carrier && !existing)
|
||||
throw new BadRequestException('签名报备状态必须指定运营商');
|
||||
const approvedAt =
|
||||
@@ -603,7 +628,7 @@ export class ChannelReportingService {
|
||||
const task = existing
|
||||
? await tx.channelSignatureReportTask.update({
|
||||
where: { id: existing.id },
|
||||
data: { status: item.status, reason: data.reason, ...(reportType === 'signature' ? { approvedAt } : {}) },
|
||||
data: { status: item.status, reason: data.reason, approvedAt },
|
||||
})
|
||||
: await tx.channelSignatureReportTask.create({
|
||||
data: {
|
||||
@@ -638,6 +663,7 @@ export class ChannelReportingService {
|
||||
reportType,
|
||||
drainageItemId: item.drainageItemId!,
|
||||
channelId: item.channelId,
|
||||
carrier,
|
||||
status: item.status,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,6 +370,7 @@ describe('ChannelsService', () => {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
const tx = {
|
||||
$executeRaw: jest.fn().mockResolvedValue(1),
|
||||
channelReportField: {
|
||||
findMany: jest
|
||||
.fn()
|
||||
@@ -519,6 +520,7 @@ describe('ChannelsService', () => {
|
||||
async (sourceEntry) => {
|
||||
const prisma = createPrismaMock();
|
||||
const tx = {
|
||||
$executeRaw: jest.fn().mockResolvedValue(1),
|
||||
smsSignature: {
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1' }),
|
||||
update: jest.fn().mockResolvedValue({ id: 'sig-1', reportStatus: 'approved' }),
|
||||
@@ -601,6 +603,7 @@ describe('ChannelsService', () => {
|
||||
it('uses the enterprise-signature save time when creating an approved carrier task', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const tx = {
|
||||
$executeRaw: jest.fn().mockResolvedValue(1),
|
||||
smsSignature: {
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1' }),
|
||||
update: jest.fn().mockResolvedValue({ id: 'sig-1', reportStatus: 'approved' }),
|
||||
@@ -655,6 +658,7 @@ describe('ChannelsService', () => {
|
||||
it('changes a drainage report task without overwriting the signature report summary', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const tx = {
|
||||
$executeRaw: jest.fn().mockResolvedValue(1),
|
||||
smsSignature: {
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1' }),
|
||||
update: jest.fn(),
|
||||
@@ -699,6 +703,7 @@ describe('ChannelsService', () => {
|
||||
reportType: 'drainage',
|
||||
drainageItemId: 'drain-1',
|
||||
channelId: 'channel-1',
|
||||
carrier: null,
|
||||
status: 'approved',
|
||||
},
|
||||
]);
|
||||
@@ -1259,6 +1264,7 @@ describe('ChannelsService', () => {
|
||||
|
||||
const transactionCallback = prisma.$transaction.mock.calls[0][0];
|
||||
const tx = {
|
||||
$executeRaw: jest.fn().mockResolvedValue(1),
|
||||
smsChannelGroupItem: { deleteMany: jest.fn(), createMany: jest.fn() },
|
||||
smsChannelGroup: {
|
||||
update: jest.fn(),
|
||||
|
||||
Reference in New Issue
Block a user