feat: complete drainage review and admin search workflows

This commit is contained in:
hectorzhao
2026-07-14 09:58:18 +08:00
parent dec2430b7e
commit 6421671259
33 changed files with 1107 additions and 255 deletions
+67 -20
View File
@@ -64,6 +64,8 @@ function createPrismaMock() {
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
application: { id: 'app-1', name: '应用A' },
materials: [],
drainageItems: [],
reportTasks: [],
}]),
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', auditStatus: 'pending' }),
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'sig-new', tenantId: 'tenant-1', ...data })),
@@ -74,11 +76,20 @@ function createPrismaMock() {
},
drainageReportMaterial: {
upsert: jest.fn().mockResolvedValue({ id: 'drainage-report-value-1' }),
create: jest.fn().mockResolvedValue({ id: 'drainage-report-value-1' }),
deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
},
smsDrainageInfo: {
findMany: jest.fn().mockResolvedValue([]),
findUnique: jest.fn().mockResolvedValue(null),
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'drainage-1', createdAt: new Date(), updatedAt: new Date(), submittedAt: new Date(), ...data })),
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'drainage-1', tenantId: 'tenant-1', signatureId: 'sig-1', applicationId: 'app-1', siteName: '官网', url: 'https://example.com', createdAt: new Date(), updatedAt: new Date(), submittedAt: new Date(), ...data })),
},
channelSignatureReportTask: {
findFirst: jest.fn().mockResolvedValue(null),
findMany: jest.fn().mockResolvedValue([]),
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'drainage-task-1', ...data })),
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'drainage-task-1', ...data })),
deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
},
channelSignatureReportRecord: {
@@ -188,7 +199,7 @@ describe('SmsConfigService', () => {
const prisma = createPrismaMock();
const service = new SmsConfigService(prisma as never);
await expect(service.listApplications({ includeConnections: true })).resolves.toEqual([
await expect(service.listApplications({ includeConnections: true, enterpriseKeyword: '租户', applicationKeyword: '应用', status: 'active' })).resolves.toEqual([
expect.objectContaining({
id: 'app-1',
cmppStatus: 'connected',
@@ -199,6 +210,7 @@ describe('SmsConfigService', () => {
}),
]);
expect(prisma.smsApplication.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({ status: 'active', tenant: { name: { contains: '租户' } }, name: { contains: '应用' } }),
include: { tenant: true, ipAllowlist: true },
}));
expect(prisma.smsApplication.findMany.mock.calls[0][0]).not.toHaveProperty('take');
@@ -427,7 +439,7 @@ describe('SmsConfigService', () => {
const prisma = createPrismaMock();
const service = new SmsConfigService(prisma as never);
await expect(service.listSignatures({ keyword: '签名A' })).resolves.toEqual([
await expect(service.listSignatures({ enterpriseKeyword: '租户', applicationKeyword: '应用', signatureKeyword: '签名', drainageKeyword: '官网' })).resolves.toEqual([
expect.objectContaining({
id: 'sig-1',
tenant: expect.objectContaining({ name: '租户A' }),
@@ -437,9 +449,18 @@ describe('SmsConfigService', () => {
expect(prisma.smsSignature.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
auditStatus: { not: 'deleted' },
OR: expect.any(Array),
tenant: { name: { contains: '租户' } },
application: { name: { contains: '应用' } },
name: { contains: '签名' },
drainageItems: expect.objectContaining({ some: expect.objectContaining({ OR: expect.any(Array) }) }),
}),
include: { materials: true, tenant: true, application: true, reportTasks: { include: { channel: true } } },
include: {
materials: true,
tenant: true,
application: true,
drainageItems: { where: { auditStatus: { not: 'deleted' } }, orderBy: { updatedAt: 'desc' } },
reportTasks: { include: { channel: true, drainageInfo: true } },
},
}));
});
@@ -473,7 +494,7 @@ describe('SmsConfigService', () => {
]);
});
it('validates and persists dynamic signature and drainage report values by channel', async () => {
it('validates and persists dynamic signature report values by channel without bypassing drainage audit', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.findUnique.mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', auditStatus: 'draft' });
prisma.smsSignature.update.mockImplementation(({ data }) => Promise.resolve({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', ...data }));
@@ -498,25 +519,48 @@ describe('SmsConfigService', () => {
applicationId: 'app-1',
drainageInfo: {
signatureReportValues: { license: { fileObjectId: 'file-1', fileName: 'license.pdf' } },
links: [{ id: 'drain-1', reportValues: { site_owner: '企业A' } }],
links: [{ id: 'legacy-drain-1', reportValues: { site_owner: '企业A' } }],
},
});
expect(prisma.signatureReportMaterial.upsert).toHaveBeenCalledWith(expect.objectContaining({
create: expect.objectContaining({ signatureId: 'sig-1', channelId: 'channel-1', fieldCode: 'license', fileObjectId: 'file-1' }),
}));
expect(prisma.drainageReportMaterial.upsert).toHaveBeenCalledWith(expect.objectContaining({
create: expect.objectContaining({ signatureId: 'sig-1', drainageItemId: 'drain-1', channelId: 'channel-1', fieldCode: 'site_owner', fieldValue: '企业A' }),
}));
expect(prisma.drainageReportMaterial.deleteMany).toHaveBeenCalledWith({
where: { signatureId: 'sig-1', drainageItemId: { notIn: ['drain-1'] } },
});
expect(prisma.channelSignatureReportTask.create).toHaveBeenCalledWith({
data: expect.objectContaining({ signatureId: 'sig-1', channelId: 'channel-1', reportType: 'drainage', drainageItemId: 'drain-1', status: 'pending' }),
});
expect(prisma.channelSignatureReportRecord.create).toHaveBeenCalledWith({
data: expect.objectContaining({ taskId: 'drainage-task-1', action: 'create', statusAfter: 'pending' }),
});
expect(prisma.drainageReportMaterial.create).not.toHaveBeenCalled();
expect(prisma.channelSignatureReportTask.create).not.toHaveBeenCalled();
});
it('creates client drainage info as pending without generating channel report tasks', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.findUnique.mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', auditStatus: 'approved' });
prisma.channelRouteRule.findMany.mockResolvedValue([]);
const service = new SmsConfigService(prisma as never);
await expect(service.createDrainageInfo('sig-1', { siteName: '官网', url: 'https://example.com', reportValues: {} }, {}, 'tenant-1'))
.resolves.toEqual(expect.objectContaining({ id: 'drainage-1', auditStatus: 'pending' }));
expect(prisma.auditRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ targetType: 'sms_drainage_info', action: 'submit', statusAfter: 'pending' }) });
expect(prisma.channelSignatureReportTask.create).not.toHaveBeenCalled();
});
it('creates real drainage materials and channel tasks after operations approval', async () => {
const prisma = createPrismaMock();
const pendingItem = { id: 'drainage-1', tenantId: 'tenant-1', signatureId: 'sig-1', applicationId: 'app-1', siteName: '官网', url: 'https://example.com', reportValues: { site_owner: '企业A' }, auditStatus: 'pending' };
const approvedItem = { ...pendingItem, auditStatus: 'approved', signature: { id: 'sig-1', applicationId: 'app-1' } };
prisma.smsDrainageInfo.findUnique.mockResolvedValueOnce(pendingItem).mockResolvedValueOnce(approvedItem);
prisma.smsDrainageInfo.update.mockResolvedValue({ ...approvedItem, tenant: {}, application: {} });
prisma.channelRouteRule.findMany.mockResolvedValue([{
id: 'route-1', priority: 10,
group: { id: 'group-1', name: '默认通道组', items: [{ channel: { id: 'channel-1', code: 'CH-1', name: '通道一', reportFields: [{ status: 'active', required: true, reportType: 'drainage', drainageField: { id: 'field-2', code: 'site_owner', name: '网站主体', fieldType: 'string', description: null, status: 'active' } }] } }] },
}] as never);
prisma.$transaction.mockImplementation((callback) => callback(prisma));
const service = new SmsConfigService(prisma as never);
await service.approveDrainageInfo('drainage-1', {});
expect(prisma.drainageReportMaterial.create).toHaveBeenCalledWith({ data: expect.objectContaining({ drainageItemId: 'drainage-1', channelId: 'channel-1', fieldCode: 'site_owner', fieldValue: '企业A' }) });
expect(prisma.channelSignatureReportTask.create).toHaveBeenCalledWith({ data: expect.objectContaining({ reportType: 'drainage', drainageItemId: 'drainage-1', status: 'pending' }) });
expect(prisma.channelSignatureReportRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ action: 'audit_approved_create', statusAfter: 'pending' }) });
});
it('creates admin signatures with an approved initial audit status', async () => {
@@ -565,7 +609,7 @@ describe('SmsConfigService', () => {
const prisma = createPrismaMock();
const service = new SmsConfigService(prisma as never);
await expect(service.listTemplates({ keyword: '模板A' })).resolves.toEqual([
await expect(service.listTemplates({ enterpriseKeyword: '租户', applicationKeyword: '应用', nameKeyword: '模板', contentKeyword: '验证码' })).resolves.toEqual([
expect.objectContaining({
id: 'tpl-1',
tenant: expect.objectContaining({ name: '租户A' }),
@@ -576,7 +620,10 @@ describe('SmsConfigService', () => {
expect(prisma.smsTemplate.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
auditStatus: { not: 'deleted' },
OR: expect.any(Array),
tenant: { name: { contains: '租户' } },
application: { name: { contains: '应用' } },
name: { contains: '模板' },
content: { contains: '验证码' },
}),
include: { variables: true, application: true, tenant: true, signature: true },
}));