feat: complete reporting and filing workflows

This commit is contained in:
hectorzhao
2026-07-28 20:28:47 +08:00
parent 352a6293b4
commit 99c8c7c68b
52 changed files with 3490 additions and 376 deletions
@@ -151,6 +151,120 @@ describe('ReportMaterialsService', () => {
expect(prisma.reportMaterialBatch.create).not.toHaveBeenCalled();
});
it('stages imported signatures for review without changing or approving business data', async () => {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('签名资料');
sheet.addRow(['短信签名', '用途说明']);
sheet.addRow(['待审签名', '验证码']);
const content = Buffer.from(await workbook.xlsx.writeBuffer());
const stagedRows: Array<Record<string, unknown>> = [];
const prisma = {
reportMaterialImportBatch: {
findUnique: jest.fn().mockResolvedValue({
id: 'import-review-1',
tenantId: 'tenant-1',
applicationId: 'app-1',
fileObjectId: 'source-1',
fileName: '签名资料.xlsx',
reportType: 'signature',
status: 'analyzed',
sheetName: '签名资料',
dataStartRow: 2,
}),
update: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) => Promise.resolve({ id: 'import-review-1', ...data, items: stagedRows })),
},
reportMaterialImportItem: {
createMany: jest.fn().mockImplementation(({ data }: { data: Array<Record<string, unknown>> }) => {
stagedRows.push(...data);
return Promise.resolve({ count: data.length });
}),
},
smsSignature: { findFirst: jest.fn().mockResolvedValue(null) },
operationLog: { create: jest.fn().mockResolvedValue({ id: 'log-import-review' }) },
$transaction: jest.fn().mockImplementation((callback: (tx: unknown) => unknown) => callback(prisma)),
};
const files = { getDownload: jest.fn().mockResolvedValue({ content }) };
const smsConfig = {
createSignature: jest.fn(),
updateSignature: jest.fn(),
approveSignature: jest.fn(),
};
const service = new ReportMaterialsService(prisma as never, files as never, smsConfig as never);
const result = await service.commitImport('import-review-1', {
operatorId: 'operator-1',
mappings: [
{ sourceHeader: '短信签名', sourceColumnIndex: 1, targetFieldCode: 'signature_name', targetKind: 'signatureName', fieldType: 'string', required: true },
{ sourceHeader: '用途说明', sourceColumnIndex: 2, targetFieldCode: 'purpose', targetKind: 'purpose', fieldType: 'string' },
],
});
expect(result).toMatchObject({ status: 'pending_review', successCount: 1, failedCount: 0 });
expect(stagedRows).toEqual([expect.objectContaining({
rowNumber: 2,
reportType: 'signature',
operation: 'create',
status: 'pending_review',
payload: expect.objectContaining({ name: '待审签名', purpose: '验证码' }),
})]);
expect(smsConfig.createSignature).not.toHaveBeenCalled();
expect(smsConfig.updateSignature).not.toHaveBeenCalled();
expect(smsConfig.approveSignature).not.toHaveBeenCalled();
});
it('allows rejecting selected imported rows without requiring a reason', async () => {
const prisma = {
reportMaterialImportBatch: {
findUnique: jest.fn().mockResolvedValue({
id: 'import-review-2',
items: [{ id: 'item-1', rowNumber: 2, status: 'pending_review' }],
}),
update: jest.fn().mockResolvedValue({}),
},
reportMaterialImportItem: {
update: jest.fn().mockResolvedValue({}),
groupBy: jest.fn().mockResolvedValue([{ status: 'rejected', _count: { _all: 1 } }]),
},
};
const service = new ReportMaterialsService(prisma as never, {} as never, {} as never);
await expect(service.reviewImportItems('import-review-2', {
decision: 'reject',
itemIds: ['item-1'],
reviewerId: 'reviewer-1',
})).resolves.toMatchObject({ status: 'rejected', rejectedCount: 1, failedCount: 0 });
expect(prisma.reportMaterialImportItem.update).toHaveBeenCalledWith({
where: { id: 'item-1' },
data: expect.objectContaining({ status: 'rejected', reviewReason: undefined, reviewedById: 'reviewer-1' }),
});
});
it('calculates generated batch totals and success rate from per-channel report tasks', async () => {
const prisma = {
reportMaterialBatch: {
findMany: jest.fn().mockResolvedValue([{
id: 'batch-stats-1',
batchNo: 'RB-STATS-1',
exportFiles: [
{ items: [{ task: { id: 'task-1', status: 'approved' } }, { task: { id: 'task-2', status: 'rejected' } }] },
{ items: [{ task: { id: 'task-3', status: 'approved' } }] },
],
items: [],
}]),
count: jest.fn().mockResolvedValue(1),
},
};
const service = new ReportMaterialsService(prisma as never, {} as never, {} as never);
await expect(service.listBatches({ keyword: 'RB-STATS', page: 2, pageSize: 10 })).resolves.toMatchObject({
items: [{ id: 'batch-stats-1', reportTotal: 3, successCount: 2, successRate: 2 / 3 }],
total: 1,
page: 2,
pageSize: 10,
});
expect(prisma.reportMaterialBatch.findMany).toHaveBeenCalledWith(expect.objectContaining({ skip: 10, take: 10 }));
});
it('rejects malformed preflight items as a readable 400 before Prisma is called', async () => {
const prisma = { smsSignature: { findUnique: jest.fn() } };
const service = new ReportMaterialsService(prisma as never, {} as never, {} as never);