fix: align signature review and admin operations
This commit is contained in:
@@ -4,7 +4,16 @@ import { ReviewGovernanceService } from './review-governance.service';
|
||||
function signature(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', name: '签名A', purpose: null,
|
||||
drainageInfo: { signatureProfile: { companyName: '企业A', creditCode: '9133', legalPersonName: '法人', responsibleName: '责任人', responsiblePhone: '13800000000', credentialFile: { fileObjectId: 'file-1' } } },
|
||||
drainageInfo: {
|
||||
signatureReportValues: { companyName: '企业A', credentialFile: { fileObjectId: 'file-1' } },
|
||||
reportRequirementSnapshot: {
|
||||
fields: [
|
||||
{ code: 'companyName', name: '公司名称', required: true, reportTypes: ['signature'] },
|
||||
{ code: 'credentialFile', name: '资质文件', required: true, reportTypes: ['signature'] },
|
||||
{ code: 'siteOwner', name: '网站主体', required: true, reportTypes: ['drainage'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
auditStatus: 'pending', reportStatus: 'waiting_material', rejectReason: null, materialVersion: 1,
|
||||
pendingReport: true, reportChangedAt: new Date(), createdAt: new Date(), updatedAt: new Date('2026-07-21T08:00:00.000Z'),
|
||||
tenant: { id: 'tenant-1', name: '企业A' }, application: { id: 'app-1', name: '应用A' }, materials: [],
|
||||
@@ -32,13 +41,46 @@ function prismaMock() {
|
||||
describe('ReviewGovernanceService', () => {
|
||||
it('blocks approval when required signature qualification is incomplete', async () => {
|
||||
const prisma = prismaMock();
|
||||
prisma.smsSignature.findUnique.mockResolvedValue(signature({ applicationId: null, application: null, drainageInfo: {}, materials: [] }));
|
||||
prisma.smsSignature.findUnique.mockResolvedValue(signature({
|
||||
applicationId: null,
|
||||
application: null,
|
||||
drainageInfo: {
|
||||
signatureReportValues: {},
|
||||
reportRequirementSnapshot: {
|
||||
fields: [
|
||||
{ code: 'companyName', name: '公司名称', required: true, reportTypes: ['signature'] },
|
||||
{ code: 'credentialFile', name: '资质文件', required: true, reportTypes: ['both'] },
|
||||
{ code: 'siteOwner', name: '网站主体', required: true, reportTypes: ['drainage'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
materials: [],
|
||||
}));
|
||||
const service = new ReviewGovernanceService(prisma as never);
|
||||
|
||||
const result = await service.preflight('signature', 'sig-1');
|
||||
|
||||
expect(result.allowedActions).toEqual(['reject']);
|
||||
expect(result.blockedReasons).toEqual(expect.arrayContaining(['未绑定短信应用', '缺少公司名称', '缺少资质文件']));
|
||||
expect(result.blockedReasons).not.toContain('缺少网站主体');
|
||||
});
|
||||
|
||||
it('does not invent legacy qualification requirements when the configured channel has no signature fields', async () => {
|
||||
const prisma = prismaMock();
|
||||
prisma.smsSignature.findUnique.mockResolvedValue(signature({
|
||||
drainageInfo: {
|
||||
signatureReportValues: {},
|
||||
reportRequirementSnapshot: {
|
||||
fields: [{ code: 'icpNo', name: 'ICP备案号', required: true, reportTypes: ['drainage'] }],
|
||||
},
|
||||
},
|
||||
}));
|
||||
const service = new ReviewGovernanceService(prisma as never);
|
||||
|
||||
const result = await service.preflight('signature', 'sig-1');
|
||||
|
||||
expect(result.allowedActions).toEqual(['approve', 'reject']);
|
||||
expect(result.blockedReasons).toEqual([]);
|
||||
});
|
||||
|
||||
it('atomically approves the expected version and returns an audit operation id', async () => {
|
||||
|
||||
@@ -90,21 +90,24 @@ export class ReviewGovernanceService {
|
||||
});
|
||||
if (!item) throw new NotFoundException('Signature not found');
|
||||
const payload = asRecord(item.drainageInfo);
|
||||
const profile = asRecord(payload.signatureProfile);
|
||||
const missing: string[] = [];
|
||||
if (!item.applicationId) missing.push('未绑定短信应用');
|
||||
if (!String(profile.companyName ?? '').trim()) missing.push('缺少公司名称');
|
||||
if (!String(profile.creditCode ?? '').trim()) missing.push('缺少统一社会信用代码');
|
||||
if (!String(profile.legalPersonName ?? '').trim()) missing.push('缺少法人姓名');
|
||||
if (!String(profile.responsibleName ?? '').trim()) missing.push('缺少责任人姓名');
|
||||
if (!String(profile.responsiblePhone ?? '').trim()) missing.push('缺少责任人手机号');
|
||||
const profileHasFile = Object.values(profile).some((value) => Boolean(asRecord(value).fileObjectId));
|
||||
if (!profileHasFile && item.materials.length === 0) missing.push('缺少资质文件');
|
||||
const signatureValues = asRecord(payload.signatureReportValues);
|
||||
const requiredFields = reportRequirementFields(payload)
|
||||
.filter((field) => field.required && field.reportTypes.some((type) => type === 'signature' || type === 'both'));
|
||||
for (const field of requiredFields) {
|
||||
if (!hasReportValue(signatureValues[field.code])) missing.push(`缺少${field.name}`);
|
||||
}
|
||||
return reviewPreflight('signature', item, {
|
||||
identity: { name: item.name, id: item.id, tenant: item.tenant.name, application: item.application?.name ?? '未绑定应用' },
|
||||
blockedReasons: missing,
|
||||
impacts: ['通过后签名将进入报备资格链路', '已绑定模板和后续发送资格可能受此决定影响'],
|
||||
materialSummary: { qualificationFiles: item.materials.length + (profileHasFile ? 1 : 0), missingCount: missing.length },
|
||||
materialSummary: {
|
||||
configuredRequiredFields: requiredFields.length,
|
||||
submittedFields: Object.values(signatureValues).filter(hasReportValue).length,
|
||||
qualificationFiles: item.materials.length + Object.values(signatureValues).filter((value) => Boolean(asRecord(value).fileObjectId)).length,
|
||||
missingCount: missing.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,3 +159,30 @@ function normalizeIdempotencyKey(value: string) {
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function reportRequirementFields(payload: Record<string, unknown>) {
|
||||
const snapshot = asRecord(payload.reportRequirementSnapshot);
|
||||
if (!Array.isArray(snapshot.fields)) return [];
|
||||
return snapshot.fields.flatMap((value) => {
|
||||
const field = asRecord(value);
|
||||
const code = typeof field.code === 'string' ? field.code.trim() : '';
|
||||
if (!code) return [];
|
||||
const reportTypes = Array.isArray(field.reportTypes)
|
||||
? field.reportTypes.filter((type): type is string => typeof type === 'string')
|
||||
: [];
|
||||
return [{
|
||||
code,
|
||||
name: typeof field.name === 'string' && field.name.trim() ? field.name.trim() : code,
|
||||
required: field.required === true,
|
||||
reportTypes,
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function hasReportValue(value: unknown) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const record = asRecord(value);
|
||||
return Boolean(record.fileObjectId || record.fieldValue || record.value);
|
||||
}
|
||||
return value !== undefined && value !== null && String(value).trim().length > 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user