fix: approve imported signature identity fields
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
mappedCoreValue,
|
||||
mappedCorePatchValue,
|
||||
dynamicValues,
|
||||
signatureIdentityReportValues,
|
||||
jsonRecord,
|
||||
hasValue,
|
||||
normalizeImageExtension,
|
||||
@@ -390,8 +391,24 @@ export class ReportImportReviewService {
|
||||
const applicationId = typeof payload.applicationId === 'string' ? payload.applicationId : undefined;
|
||||
const importedDrainage = jsonRecord(payload.drainageInfo);
|
||||
const importedReportValues = jsonRecord(importedDrainage.signatureReportValues);
|
||||
const buildBody = (current?: { drainageInfo: Prisma.JsonValue | null }) => {
|
||||
const [reportFields, tenant, application] = await Promise.all([
|
||||
this.smsConfig.getApplicationReportFields(applicationId, 'signature'),
|
||||
this.prisma.tenant.findUnique({ where: { id: batch.tenantId }, select: { name: true } }),
|
||||
applicationId
|
||||
? this.prisma.smsApplication.findUnique({ where: { id: applicationId }, select: { name: true } })
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
const buildBody = (current?: { drainageInfo: Prisma.JsonValue | null; purpose?: string | null }) => {
|
||||
const currentDrainage = jsonRecord(current?.drainageInfo);
|
||||
const purpose = Object.prototype.hasOwnProperty.call(payload, 'purpose')
|
||||
? String(payload.purpose ?? '')
|
||||
: current?.purpose;
|
||||
const identityValues = signatureIdentityReportValues(reportFields, {
|
||||
signatureName: name,
|
||||
purpose,
|
||||
enterpriseName: tenant?.name,
|
||||
applicationName: application?.name,
|
||||
});
|
||||
return {
|
||||
applicationId,
|
||||
name,
|
||||
@@ -402,6 +419,7 @@ export class ReportImportReviewService {
|
||||
...currentDrainage,
|
||||
signatureReportValues: {
|
||||
...jsonRecord(currentDrainage.signatureReportValues),
|
||||
...identityValues,
|
||||
...importedReportValues,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -237,6 +237,33 @@ export function dynamicValues(mappings: ImportMapping[], values: Record<string,
|
||||
);
|
||||
}
|
||||
|
||||
export function signatureIdentityReportValues(
|
||||
fields: Array<{ code: string; name: string; fieldType?: string }>,
|
||||
identity: {
|
||||
signatureName?: string;
|
||||
purpose?: string | null;
|
||||
enterpriseName?: string;
|
||||
applicationName?: string;
|
||||
},
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
fields.flatMap((field) => {
|
||||
if (field.fieldType && field.fieldType !== 'string') return [];
|
||||
const semantic = normalizeHeader(`${field.code}/${field.name}`);
|
||||
const value = /短信签名|签名名称|signaturename|sms(?:signature|sign)|^sign$/.test(semantic)
|
||||
? identity.signatureName
|
||||
: /签名用途|签名依据|purpose/.test(semantic)
|
||||
? identity.purpose
|
||||
: /企业名称|公司名称|enterprisename|companyname/.test(semantic)
|
||||
? identity.enterpriseName
|
||||
: /应用名称|applicationname|appname/.test(semantic)
|
||||
? identity.applicationName
|
||||
: undefined;
|
||||
return hasValue(value) ? [[field.code, value]] : [];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function jsonRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ExcelJS from 'exceljs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { ReportMaterialsService } from './report-materials.service';
|
||||
import { mappedCorePatchValue } from './report-materials.helpers';
|
||||
import { mappedCorePatchValue, signatureIdentityReportValues } from './report-materials.helpers';
|
||||
|
||||
describe('ReportMaterialsService', () => {
|
||||
it('requires an enterprise application before parsing an import workbook', async () => {
|
||||
@@ -43,6 +43,27 @@ describe('ReportMaterialsService', () => {
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('materializes system identity fields without inventing a missing signature purpose', () => {
|
||||
const fields = [
|
||||
{ code: 'qaSignatureName', name: '短信签名', fieldType: 'string' },
|
||||
{ code: 'qaSignaturePurpose', name: '签名用途', fieldType: 'string' },
|
||||
{ code: 'qaEnterpriseName', name: '企业名称', fieldType: 'string' },
|
||||
{ code: 'qaApplicationName', name: '应用名称', fieldType: 'string' },
|
||||
{ code: 'qaLicenseImage', name: '营业执照图片', fieldType: 'image' },
|
||||
];
|
||||
expect(
|
||||
signatureIdentityReportValues(fields, {
|
||||
signatureName: '【导入测试】',
|
||||
enterpriseName: '测试企业',
|
||||
applicationName: '测试应用',
|
||||
}),
|
||||
).toEqual({
|
||||
qaSignatureName: '【导入测试】',
|
||||
qaEnterpriseName: '测试企业',
|
||||
qaApplicationName: '测试应用',
|
||||
});
|
||||
});
|
||||
it('builds an official XLSX import template with documented signature columns', async () => {
|
||||
const operationLog = { create: jest.fn().mockResolvedValue({ id: 'log-template' }) };
|
||||
const service = new ReportMaterialsService({ operationLog } as never, {} as never, {} as never);
|
||||
@@ -602,6 +623,79 @@ describe('ReportMaterialsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('approves an imported signature by supplying configured identity fields from real entities', async () => {
|
||||
const item = {
|
||||
id: 'item-identity-1',
|
||||
rowNumber: 2,
|
||||
reportType: 'signature',
|
||||
targetId: null,
|
||||
status: 'pending_review',
|
||||
payload: {
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
name: '【导入测试】',
|
||||
purpose: '验证码通知',
|
||||
drainageInfo: {
|
||||
signatureReportValues: {
|
||||
qaLicenseImage: { fileObjectId: 'image-1', fileName: 'license.png', contentType: 'image/png' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const prisma = {
|
||||
reportMaterialImportBatch: {
|
||||
findUnique: jest.fn().mockResolvedValue({
|
||||
id: 'batch-identity-1',
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
reportType: 'signature',
|
||||
items: [item],
|
||||
}),
|
||||
update: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
reportMaterialImportItem: {
|
||||
update: jest.fn().mockResolvedValue({}),
|
||||
groupBy: jest.fn().mockResolvedValue([{ status: 'approved', _count: { _all: 1 } }]),
|
||||
},
|
||||
smsSignature: {
|
||||
findFirst: jest.fn().mockResolvedValue(null),
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
tenant: { findUnique: jest.fn().mockResolvedValue({ name: '测试企业' }) },
|
||||
smsApplication: { findUnique: jest.fn().mockResolvedValue({ name: '测试应用' }) },
|
||||
};
|
||||
const smsConfig = {
|
||||
getApplicationReportFields: jest.fn().mockResolvedValue([
|
||||
{ code: 'qaSignatureName', name: '短信签名', fieldType: 'string' },
|
||||
{ code: 'qaSignaturePurpose', name: '签名用途', fieldType: 'string' },
|
||||
{ code: 'qaEnterpriseName', name: '企业名称', fieldType: 'string' },
|
||||
{ code: 'qaApplicationName', name: '应用名称', fieldType: 'string' },
|
||||
{ code: 'qaLicenseImage', name: '营业执照图片', fieldType: 'image' },
|
||||
]),
|
||||
createSignature: jest.fn().mockImplementation(({ drainageInfo }: { drainageInfo: { signatureReportValues: Record<string, unknown> } }) => {
|
||||
expect(drainageInfo.signatureReportValues).toEqual(expect.objectContaining({
|
||||
qaSignatureName: '【导入测试】',
|
||||
qaSignaturePurpose: '验证码通知',
|
||||
qaEnterpriseName: '测试企业',
|
||||
qaApplicationName: '测试应用',
|
||||
qaLicenseImage: expect.objectContaining({ fileObjectId: 'image-1' }),
|
||||
}));
|
||||
return Promise.resolve({ id: 'signature-created-1' });
|
||||
}),
|
||||
approveSignature: jest.fn().mockResolvedValue({ id: 'signature-created-1', auditStatus: 'approved' }),
|
||||
};
|
||||
const service = new ReportMaterialsService(prisma as never, {} as never, smsConfig as never);
|
||||
|
||||
await expect(service.reviewImportItems('batch-identity-1', {
|
||||
decision: 'approve',
|
||||
itemIds: [item.id],
|
||||
reviewerId: 'reviewer-1',
|
||||
})).resolves.toMatchObject({ status: 'approved', approvedCount: 1, failedCount: 0 });
|
||||
expect(smsConfig.approveSignature).toHaveBeenCalledWith('signature-created-1', expect.objectContaining({
|
||||
reviewerId: 'reviewer-1',
|
||||
}));
|
||||
});
|
||||
|
||||
it('calculates generated batch totals and success rate from per-channel report tasks', async () => {
|
||||
const prisma = {
|
||||
reportMaterialBatch: {
|
||||
|
||||
Reference in New Issue
Block a user