feat: refine client status and review views

This commit is contained in:
hectorzhao
2026-08-27 17:32:05 +08:00
parent d6edb88b76
commit 01cffa4758
31 changed files with 528 additions and 117 deletions
@@ -45,6 +45,7 @@ function createPrismaMock(overrides: Record<string, unknown> = {}) {
},
smsBatchTask: {
count: jest.fn().mockResolvedValue(0),
findMany: jest.fn().mockResolvedValue([]),
},
riskHitRecord: {
createMany: jest.fn(),
@@ -152,6 +153,21 @@ describe('RiskReviewService', () => {
}));
});
it('adds the associated batch task number to SMS review rows', async () => {
const prisma = createPrismaMock();
prisma.smsSendTask.findMany.mockResolvedValue([{ id: 'review-task-1', taskNo: 'REVIEW-001' }]);
prisma.smsBatchTask.findMany.mockResolvedValue([{ id: 'batch-1', taskNo: 'BATCH-001', riskTaskId: 'review-task-1' }]);
const service = new RiskReviewService(prisma as never);
await expect(service.listTasks(undefined, 'pending_review')).resolves.toEqual([
expect.objectContaining({ batchTask: { id: 'batch-1', taskNo: 'BATCH-001' } }),
]);
expect(prisma.smsBatchTask.findMany).toHaveBeenCalledWith({
where: { riskTaskId: { in: ['review-task-1'] } },
select: { id: true, taskNo: true, riskTaskId: true },
});
});
it('filters SMS review tasks by their submission time', async () => {
const prisma = createPrismaMock();
prisma.smsSendTask.findMany.mockResolvedValue([]);
+8 -2
View File
@@ -202,8 +202,8 @@ export class RiskReviewService {
});
}
listTasks(tenantId?: string, status?: string, submittedAtFrom?: string, submittedAtTo?: string) {
return this.prisma.smsSendTask.findMany({
async listTasks(tenantId?: string, status?: string, submittedAtFrom?: string, submittedAtTo?: string) {
const tasks = await this.prisma.smsSendTask.findMany({
where: {
tenantId,
status,
@@ -231,6 +231,12 @@ export class RiskReviewService {
},
orderBy: { createdAt: 'desc' },
});
const batchTasks = tasks.length ? await this.prisma.smsBatchTask.findMany({
where: { riskTaskId: { in: tasks.map((task) => task.id) } },
select: { id: true, taskNo: true, riskTaskId: true },
}) : [];
const batchByRiskTaskId = new Map(batchTasks.map((task) => [task.riskTaskId, { id: task.id, taskNo: task.taskNo }]));
return tasks.map((task) => ({ ...task, batchTask: batchByRiskTaskId.get(task.id) ?? null }));
}
listPendingTasks() {
+35
View File
@@ -223,14 +223,47 @@ export class SmsSignatureService {
updatedAt: true,
},
},
reportTasks: {
select: { channelId: true, carrier: true, status: true, approvalScope: true, reportType: true, drainageItemId: true },
},
_count: { select: { reportMaterials: true } },
},
orderBy: { updatedAt: 'desc' },
skip: query.page && query.pageSize ? (query.page - 1) * query.pageSize : undefined,
take: query.pageSize,
});
const applicationIds = signatures.map((signature) => signature.applicationId).filter((id): id is string => Boolean(id));
const routes = applicationIds.length ? await this.prisma.channelRouteRule.findMany({
where: { applicationId: { in: applicationIds }, status: 'active' },
include: { group: { include: { items: { include: { channel: { include: { reportFields: true } } } } } } },
}) : [];
const hasCommonDrainageFields = await this.prisma.commonReportField.count({
where: { status: 'active', reportType: 'drainage', drainageField: { status: 'active' } },
}).then((count) => count > 0);
return signatures.map((signature) => {
const stored = isRecord(signature.drainageInfo) ? signature.drainageInfo : {};
const applicationChannels = [...new Map(routes
.filter((route) => route.applicationId === signature.applicationId && route.group)
.flatMap((route) => route.group!.items.map((item) => item.channel))
.filter((channel) => channel.status !== 'deleted')
.map((channel) => [channel.id, channel])).values()];
const signatureTasks = signature.reportTasks.filter((task) => task.reportType === 'signature');
const carrierReportSummary = Object.fromEntries(['mobile', 'unicom', 'telecom'].map((carrier) => {
const targets = applicationChannels.filter((channel) => normalizeChannelCarriers(channel.carriers, channel.carrier).includes(carrier));
const statuses = targets.map((channel) => signatureTasks.find((task) => task.channelId === channel.id && task.carrier === carrier)?.status
?? signatureTasks.find((task) => task.channelId === channel.id && task.carrier === null && task.approvalScope === 'legacy_channel')?.status
?? 'pending');
return [carrier, summarizeReportStatuses(statuses)];
}));
const drainageCarrierReportSummary = Object.fromEntries(signature.drainageItems.map((item) => {
const targets = applicationChannels.filter((channel) => hasCommonDrainageFields || channel.reportFields.some((field) => field.status === 'active' && ['drainage', 'both'].includes(field.reportType)));
const tasks = signature.reportTasks.filter((task) => task.reportType === 'drainage' && task.drainageItemId === item.id);
return [item.id, Object.fromEntries(['mobile', 'unicom', 'telecom'].map((carrier) => {
const carrierTargets = targets.filter((channel) => normalizeChannelCarriers(channel.carriers, channel.carrier).includes(carrier));
const statuses = carrierTargets.map((channel) => tasks.find((task) => task.channelId === channel.id)?.status ?? 'pending');
return [carrier, summarizeReportStatuses(statuses)];
}))];
}));
return {
id: signature.id,
tenantId: signature.tenantId,
@@ -247,6 +280,8 @@ export class SmsSignatureService {
application: signature.application,
materials: signature.materials,
submittedMaterialCount: signature.materials.length + signature._count.reportMaterials,
carrierReportSummary,
drainageCarrierReportSummary,
reportValues: isRecord(stored.signatureReportValues) ? stored.signatureReportValues : {},
drainageInfo: {
links: signature.drainageItems.map((item) => ({