feat: complete drainage review and admin search workflows
This commit is contained in:
@@ -146,8 +146,8 @@ export class ChannelsController {
|
||||
}
|
||||
|
||||
@Get('report-tasks')
|
||||
listReportTasks(@Query('tenantId') tenantId?: string, @Query('status') status?: string, @Query('channelId') channelId?: string) {
|
||||
return this.channels.listReportTasks(tenantId, status, channelId);
|
||||
listReportTasks(@Query('tenantId') tenantId?: string, @Query('status') status?: string, @Query('channelId') channelId?: string, @Query('reportType') reportType?: string) {
|
||||
return this.channels.listReportTasks(tenantId, status, channelId, reportType);
|
||||
}
|
||||
|
||||
@Post('report-tasks/generate')
|
||||
|
||||
@@ -108,6 +108,9 @@ function createPrismaMock() {
|
||||
findUnique: jest.fn().mockResolvedValue(reportTask),
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ ...reportTask, ...data })),
|
||||
},
|
||||
smsDrainageInfo: {
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'drain-1', signatureId: 'sig-1', auditStatus: 'approved' }),
|
||||
},
|
||||
channelSignatureReportRecord: {
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'record-1', ...data })),
|
||||
findMany: jest.fn(),
|
||||
@@ -183,8 +186,8 @@ describe('ChannelsService', () => {
|
||||
await service.listReportTasks(undefined, undefined, 'channel-1');
|
||||
|
||||
expect(prisma.channelSignatureReportTask.findMany).toHaveBeenCalledWith({
|
||||
where: { tenantId: undefined, status: undefined, channelId: 'channel-1' },
|
||||
include: { signature: true, channel: true },
|
||||
where: { tenantId: undefined, status: undefined, channelId: 'channel-1', reportType: undefined },
|
||||
include: { signature: true, channel: true, drainageInfo: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
});
|
||||
@@ -197,6 +200,7 @@ describe('ChannelsService', () => {
|
||||
update: jest.fn().mockResolvedValue({ id: 'sig-1', reportStatus: 'approved' }),
|
||||
},
|
||||
smsChannel: { findUnique: jest.fn().mockResolvedValue({ id: 'channel-1', carrier: 'mobile', status: 'active' }) },
|
||||
smsDrainageInfo: { findUnique: jest.fn().mockResolvedValue({ id: 'drain-1', signatureId: 'sig-1', auditStatus: 'approved' }) },
|
||||
channelSignatureReportTask: {
|
||||
findFirst: jest.fn().mockResolvedValue({ id: 'task-1', signatureId: 'sig-1', channelId: 'channel-1', status: 'reporting' }),
|
||||
update: jest.fn().mockResolvedValue({ id: 'task-1', signatureId: 'sig-1', channelId: 'channel-1', status: 'approved' }),
|
||||
@@ -209,10 +213,10 @@ describe('ChannelsService', () => {
|
||||
prisma.$transaction.mockImplementation((callback) => callback(tx));
|
||||
const service = new ChannelsService(prisma as never);
|
||||
|
||||
await expect(service.changeReportTaskStatuses({ items: [{ signatureId: 'sig-1', channelId: 'channel-1', status: 'approved' }], reason: '运营商确认' })).resolves.toEqual([
|
||||
await expect(service.changeReportTaskStatuses({ items: [{ signatureId: 'sig-1', channelId: 'channel-1', status: 'approved' }], reason: '运营商确认', sourceEntry: 'enterprise_signature' })).resolves.toEqual([
|
||||
expect.objectContaining({ signatureId: 'sig-1', reportStatus: 'approved' }),
|
||||
]);
|
||||
expect(tx.channelSignatureReportRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ action: 'manual_status_change', statusBefore: 'reporting', statusAfter: 'approved' }) });
|
||||
expect(tx.channelSignatureReportRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ action: 'manual_status_change', statusBefore: 'reporting', statusAfter: 'approved', sourceEntry: 'enterprise_signature' }) });
|
||||
expect(tx.smsSignature.update).toHaveBeenCalledWith({ where: { id: 'sig-1' }, data: { reportStatus: 'approved' } });
|
||||
});
|
||||
|
||||
@@ -224,6 +228,7 @@ describe('ChannelsService', () => {
|
||||
update: jest.fn(),
|
||||
},
|
||||
smsChannel: { findUnique: jest.fn().mockResolvedValue({ id: 'channel-1', carrier: 'mobile', status: 'active' }) },
|
||||
smsDrainageInfo: { findUnique: jest.fn().mockResolvedValue({ id: 'drain-1', signatureId: 'sig-1', auditStatus: 'approved' }) },
|
||||
channelSignatureReportTask: {
|
||||
findFirst: jest.fn().mockResolvedValue({ id: 'drainage-task-1', signatureId: 'sig-1', channelId: 'channel-1', reportType: 'drainage', drainageItemId: 'drain-1', status: 'reporting' }),
|
||||
update: jest.fn().mockResolvedValue({ id: 'drainage-task-1', status: 'approved' }),
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface ChangeReportTaskStatusesDto {
|
||||
items: Array<{ signatureId: string; channelId: string; status: string; reportType?: 'signature' | 'drainage'; drainageItemId?: string }>;
|
||||
reason?: string;
|
||||
operatorId?: string;
|
||||
sourceEntry?: 'enterprise_signature' | 'report_task' | 'channel_report';
|
||||
}
|
||||
|
||||
export interface CreateReportExportDto {
|
||||
@@ -912,6 +913,7 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
async createReportField(data: CreateReportFieldDto) {
|
||||
if (!data.drainageFieldId) throw new BadRequestException('drainageFieldId is required');
|
||||
const field = await this.prisma.drainageField.findUnique({ where: { id: data.drainageFieldId } });
|
||||
if (!field || field.status !== 'active') {
|
||||
throw new BadRequestException('报备字段库字段不存在或已停用');
|
||||
@@ -966,10 +968,10 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
listReportTasks(tenantId?: string, status?: string, channelId?: string) {
|
||||
listReportTasks(tenantId?: string, status?: string, channelId?: string, reportType?: string) {
|
||||
return this.prisma.channelSignatureReportTask.findMany({
|
||||
where: { tenantId, status, channelId },
|
||||
include: { signature: true, channel: true },
|
||||
where: { tenantId, status, channelId, reportType },
|
||||
include: { signature: true, channel: true, drainageInfo: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
@@ -977,13 +979,19 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
||||
async createReportTask(data: CreateReportTaskDto) {
|
||||
const reportType = data.reportType ?? 'signature';
|
||||
if (reportType === 'drainage' && !data.drainageItemId) throw new BadRequestException('drainageItemId is required');
|
||||
if (reportType === 'drainage') {
|
||||
const drainageInfo = await this.prisma.smsDrainageInfo.findUnique({ where: { id: data.drainageItemId! } });
|
||||
if (!drainageInfo || drainageInfo.signatureId !== data.signatureId) throw new NotFoundException('Drainage info not found');
|
||||
if (drainageInfo.auditStatus !== 'approved') throw new BadRequestException('引流信息审核通过后才能进入通道报备');
|
||||
throw new BadRequestException('引流信息通道报备任务由运营审核通过后按应用路由自动生成');
|
||||
}
|
||||
const task = await this.prisma.channelSignatureReportTask.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
signatureId: data.signatureId,
|
||||
channelId: data.channelId,
|
||||
reportType,
|
||||
drainageItemId: reportType === 'drainage' ? data.drainageItemId : undefined,
|
||||
drainageItemId: undefined,
|
||||
createdById: data.createdById,
|
||||
status: 'pending',
|
||||
},
|
||||
@@ -998,6 +1006,10 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
||||
for (const item of data.items) {
|
||||
if (!allowed.has(item.status)) throw new BadRequestException('unsupported report task status');
|
||||
}
|
||||
const sourceEntry = data.sourceEntry ?? 'report_task';
|
||||
if (!['enterprise_signature', 'report_task', 'channel_report'].includes(sourceEntry)) {
|
||||
throw new BadRequestException('unsupported report task source entry');
|
||||
}
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const signatureIds = [...new Set(data.items.filter((item) => (item.reportType ?? 'signature') === 'signature').map((item) => item.signatureId))];
|
||||
const drainageResults: Array<{ signatureId: string; reportType: 'drainage'; drainageItemId: string; channelId: string; status: string }> = [];
|
||||
@@ -1007,11 +1019,17 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
||||
const signature = await tx.smsSignature.findUnique({ where: { id: item.signatureId } });
|
||||
const channel = await tx.smsChannel.findUnique({ where: { id: item.channelId } });
|
||||
if (!signature || !channel) throw new NotFoundException('Signature or channel not found');
|
||||
if (reportType === 'drainage') {
|
||||
const drainageInfo = await tx.smsDrainageInfo.findUnique({ where: { id: item.drainageItemId! } });
|
||||
if (!drainageInfo || drainageInfo.signatureId !== item.signatureId) throw new NotFoundException('Drainage info not found');
|
||||
if (drainageInfo.auditStatus !== 'approved') throw new BadRequestException('引流信息审核通过后才能修改通道报备状态');
|
||||
}
|
||||
const existing = await tx.channelSignatureReportTask.findFirst({ where: { signatureId: item.signatureId, channelId: item.channelId, reportType, drainageItemId: reportType === 'drainage' ? item.drainageItemId : null } });
|
||||
if (reportType === 'drainage' && !existing) throw new BadRequestException('引流信息通道报备任务不存在,请先完成运营审核');
|
||||
const task = existing
|
||||
? await tx.channelSignatureReportTask.update({ where: { id: existing.id }, data: { status: item.status, reason: data.reason } })
|
||||
: await tx.channelSignatureReportTask.create({ data: { tenantId: signature.tenantId, signatureId: item.signatureId, channelId: item.channelId, reportType, drainageItemId: reportType === 'drainage' ? item.drainageItemId : undefined, status: item.status, reason: data.reason, createdById: data.operatorId } });
|
||||
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: item.channelId, action: 'manual_status_change', statusBefore: existing?.status, statusAfter: item.status, reason: data.reason, operatorId: data.operatorId } });
|
||||
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: item.channelId, action: 'manual_status_change', statusBefore: existing?.status, statusAfter: item.status, reason: data.reason, operatorId: data.operatorId, sourceEntry } });
|
||||
if (reportType === 'drainage') drainageResults.push({ signatureId: item.signatureId, reportType, drainageItemId: item.drainageItemId!, channelId: item.channelId, status: item.status });
|
||||
}
|
||||
const summaries = [];
|
||||
@@ -1086,15 +1104,19 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
|
||||
listReportRecords(taskId?: string, channelId?: string) {
|
||||
return this.prisma.channelSignatureReportRecord.findMany({
|
||||
where: { taskId, channelId },
|
||||
include: { channel: true, task: { include: { signature: true, drainageInfo: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
private async getReportTaskOrThrow(taskId: string) {
|
||||
const task = await this.prisma.channelSignatureReportTask.findUnique({ where: { id: taskId } });
|
||||
const task = await this.prisma.channelSignatureReportTask.findUnique({ where: { id: taskId }, include: { drainageInfo: true } });
|
||||
if (!task) {
|
||||
throw new NotFoundException('Report task not found');
|
||||
}
|
||||
if (task.reportType === 'drainage' && task.drainageInfo?.auditStatus !== 'approved') {
|
||||
throw new BadRequestException('引流信息审核通过后才能处理通道报备任务');
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user