feat: add report material workflows and gateway safeguards
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import ExcelJS from 'exceljs';
|
||||
import { ReportMaterialsService } from './report-materials.service';
|
||||
|
||||
describe('ReportMaterialsService', () => {
|
||||
it('detects WPS-compatible embedded images and source columns during XLSX analysis', async () => {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const sheet = workbook.addWorksheet('签名资料');
|
||||
sheet.addRow(['短信签名', '营业执照']);
|
||||
sheet.addRow(['测试签名', '']);
|
||||
const imageId = workbook.addImage({ base64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZsmAAAAAASUVORK5CYII=', extension: 'png' });
|
||||
sheet.addImage(imageId, { tl: { col: 1, row: 1 }, ext: { width: 80, height: 60 } });
|
||||
const buffer = Buffer.from(await workbook.xlsx.writeBuffer());
|
||||
const prisma = {
|
||||
reportMaterialImportProfile: { findUnique: jest.fn().mockResolvedValue({ sheetName: '签名资料', columns: [{ sourceHeader: '短信签名', sourceHeaderPath: '短信签名', sourceColumnIndex: 9, targetFieldCode: 'signature_name', targetKind: 'signatureName', fieldType: 'string', required: true, sortOrder: 10 }] }) },
|
||||
reportMaterialImportBatch: { create: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) => Promise.resolve({ id: 'import-1', ...data })) },
|
||||
};
|
||||
const files = { upload: jest.fn().mockResolvedValue({ id: 'source-1', fileName: '签名资料.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) };
|
||||
const service = new ReportMaterialsService(prisma as never, files as never, {} as never);
|
||||
|
||||
const result = await service.analyzeImport({ originalname: '签名资料.xlsx', mimetype: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: buffer.length, buffer }, { tenantId: 'tenant-1', applicationId: 'app-1', reportType: 'signature', headerRowCount: 1, dataStartRow: 2, profileId: 'profile-1' });
|
||||
|
||||
expect(result.imageCount).toBe(1);
|
||||
expect(result.columns).toEqual(expect.arrayContaining([expect.objectContaining({ sourceHeader: '营业执照', imageCount: 1 })]));
|
||||
expect(result.rows).toEqual([expect.objectContaining({ rowNumber: 2, imageColumns: [2] })]);
|
||||
expect(result.suggestedMappings).toEqual([expect.objectContaining({ sourceColumnIndex: 1, targetKind: 'signatureName' })]);
|
||||
});
|
||||
|
||||
it('expands one selected signature to every routed channel and embeds images in each XLSX', async () => {
|
||||
const uploadedWorkbooks: Buffer[] = [];
|
||||
let batchItemSequence = 0;
|
||||
let exportSequence = 0;
|
||||
const channels = [{ id: 'channel-a', name: '通道A', status: 'active' }, { id: 'channel-b', name: '通道B', status: 'active' }];
|
||||
const prisma = {
|
||||
reportMaterialBatch: {
|
||||
create: jest.fn().mockResolvedValue({ id: 'batch-1', batchNo: 'RB001' }),
|
||||
update: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) => Promise.resolve({ id: 'batch-1', batchNo: 'RB001', ...data })),
|
||||
},
|
||||
smsSignature: {
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'signature-1', tenantId: 'tenant-1', applicationId: 'app-1', name: '测试签名', purpose: '验证码', auditStatus: 'approved', materialVersion: 3, drainageInfo: { signatureReportValues: { license: { fileObjectId: 'image-1', fileName: 'license.png', contentType: 'image/png' } } }, tenant: { name: '测试企业' }, application: { name: '测试应用' } }),
|
||||
update: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
smsDrainageInfo: { findUnique: jest.fn(), update: jest.fn() },
|
||||
channelRouteRule: { findMany: jest.fn().mockResolvedValue([{ group: { items: channels.map((channel, index) => ({ priority: index, channel })) } }]) },
|
||||
reportMaterialBatchItem: { create: jest.fn().mockImplementation(() => Promise.resolve({ id: `batch-item-${++batchItemSequence}` })) },
|
||||
smsChannel: { findUnique: jest.fn().mockImplementation(({ where }: { where: { id: string } }) => Promise.resolve(channels.find((channel) => channel.id === where.id))) },
|
||||
channelReportField: { findMany: jest.fn().mockResolvedValue([
|
||||
{ code: 'sign', name: '短信签名', exportName: '通道签名', required: true, columnWidth: 18, imageWidth: 120, imageHeight: 80, transform: null, defaultValue: null },
|
||||
{ code: 'license', name: '营业执照', exportName: '营业执照图片', required: true, columnWidth: 24, imageWidth: 120, imageHeight: 80, transform: null, defaultValue: null },
|
||||
]) },
|
||||
channelSignatureReportTask: { findFirst: jest.fn().mockResolvedValue(null), create: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) => Promise.resolve({ id: `task-${String(data.channelId)}`, ...data })), update: jest.fn() },
|
||||
channelSignatureReportRecord: { create: jest.fn().mockResolvedValue({}) },
|
||||
reportExportFile: { create: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) => Promise.resolve({ id: `export-${++exportSequence}`, ...data })) },
|
||||
reportExportFileItem: { createMany: jest.fn().mockResolvedValue({ count: 1 }) },
|
||||
};
|
||||
const files = {
|
||||
getDownload: jest.fn().mockResolvedValue({ fileObject: { fileName: 'license.png', contentType: 'image/png' }, content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZsmAAAAAASUVORK5CYII=', 'base64') }),
|
||||
upload: jest.fn().mockImplementation((_options: unknown, file: { originalname: string; mimetype: string; buffer: Buffer }) => {
|
||||
uploadedWorkbooks.push(file.buffer);
|
||||
return Promise.resolve({ id: `file-${uploadedWorkbooks.length}`, fileName: file.originalname, contentType: file.mimetype });
|
||||
}),
|
||||
};
|
||||
const service = new ReportMaterialsService(prisma as never, files as never, {} as never);
|
||||
|
||||
const result = await service.createBatch({ items: [{ reportType: 'signature', signatureId: 'signature-1' }] });
|
||||
|
||||
expect(result).toMatchObject({ status: 'completed', channelCount: 2, fileCount: 2 });
|
||||
expect(prisma.channelSignatureReportTask.create).toHaveBeenCalledTimes(2);
|
||||
expect(prisma.smsSignature.update).toHaveBeenCalledWith({ where: { id: 'signature-1' }, data: { pendingReport: false } });
|
||||
expect(uploadedWorkbooks).toHaveLength(2);
|
||||
for (const buffer of uploadedWorkbooks) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(buffer as never);
|
||||
const sheet = workbook.getWorksheet('签名报备');
|
||||
expect(sheet?.getCell('A1').text).toBe('通道签名');
|
||||
expect(sheet?.getCell('A2').text).toBe('测试签名');
|
||||
expect(sheet?.getImages()).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps material pending and marks the task waiting when the channel has no field configuration', async () => {
|
||||
const prisma = {
|
||||
reportMaterialBatch: { create: jest.fn().mockResolvedValue({ id: 'batch-2' }), update: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) => Promise.resolve(data)) },
|
||||
smsSignature: { findUnique: jest.fn().mockResolvedValue({ id: 'signature-2', tenantId: 'tenant-1', applicationId: 'app-1', name: '测试签名', auditStatus: 'approved', materialVersion: 1, drainageInfo: {}, tenant: { name: '企业' }, application: { name: '应用' } }), update: jest.fn() },
|
||||
smsDrainageInfo: { findUnique: jest.fn(), update: jest.fn() },
|
||||
channelRouteRule: { findMany: jest.fn().mockResolvedValue([{ group: { items: [{ channel: { id: 'channel-a', name: '通道A', status: 'active' } }] } }]) },
|
||||
reportMaterialBatchItem: { create: jest.fn().mockResolvedValue({ id: 'batch-item-2' }) },
|
||||
smsChannel: { findUnique: jest.fn().mockResolvedValue({ id: 'channel-a', name: '通道A' }) },
|
||||
channelReportField: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
channelSignatureReportTask: { findFirst: jest.fn().mockResolvedValue(null), create: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) => Promise.resolve({ id: 'task-2', ...data })) },
|
||||
channelSignatureReportRecord: { create: jest.fn().mockResolvedValue({}) },
|
||||
reportExportFile: { create: jest.fn().mockResolvedValue({ id: 'export-2' }) },
|
||||
reportExportFileItem: { createMany: jest.fn() },
|
||||
};
|
||||
const files = { upload: jest.fn().mockResolvedValue({ id: 'file-2', fileName: 'empty.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) };
|
||||
const service = new ReportMaterialsService(prisma as never, files as never, {} as never);
|
||||
|
||||
const result = await service.createBatch({ items: [{ reportType: 'signature', signatureId: 'signature-2' }] });
|
||||
|
||||
expect(result).toMatchObject({ status: 'partial_failed' });
|
||||
expect(prisma.channelSignatureReportTask.create).toHaveBeenCalledWith({ data: expect.objectContaining({ status: 'waiting_material', reason: '通道未配置当前资料类型的报备字段' }) });
|
||||
expect(prisma.smsSignature.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user