feat: aggregate cmpp template mismatch reviews
This commit is contained in:
@@ -95,9 +95,16 @@ function createPrismaMock() {
|
||||
signature: { auditStatus: 'approved', reportStatus: 'approved' },
|
||||
}),
|
||||
},
|
||||
smsSignature: {
|
||||
findFirst: jest.fn().mockResolvedValue({ id: 'sig-1', name: '签名', auditStatus: 'approved', reportStatus: 'approved' }),
|
||||
},
|
||||
smsSendTask: {
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
smsBatchTask: {
|
||||
create: jest.fn().mockResolvedValue(task),
|
||||
findUnique: jest.fn().mockResolvedValue(task),
|
||||
findFirst: jest.fn().mockResolvedValue(task),
|
||||
findMany: jest.fn(),
|
||||
update: jest.fn().mockResolvedValue(task),
|
||||
},
|
||||
@@ -296,6 +303,10 @@ function createService(prisma = createPrismaMock()) {
|
||||
reason: null,
|
||||
task: { id: 'risk-task-1' },
|
||||
}),
|
||||
aggregateTemplateMismatch: jest.fn().mockResolvedValue({
|
||||
id: 'review-task-1',
|
||||
reviewReason: '企业应用已配置模板不匹配进入人工审核',
|
||||
}),
|
||||
} as unknown as RiskReviewService;
|
||||
const service = new SendChainService(prisma as never, billing, riskReview);
|
||||
service['postGatewayControl'] = jest.fn().mockResolvedValue({ delivered: true });
|
||||
@@ -372,7 +383,7 @@ describe('SendChainService', () => {
|
||||
|
||||
it('cancels scheduled tasks before dispatch', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsBatchTask.findUnique.mockResolvedValue({ id: 'task-1', status: 'scheduled' });
|
||||
prisma.smsBatchTask.findFirst.mockResolvedValue({ id: 'task-1', status: 'scheduled' });
|
||||
|
||||
await service.cancelBatchTask('task-1');
|
||||
|
||||
@@ -386,6 +397,30 @@ describe('SendChainService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('lists only client-created batch tasks for task progress', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsBatchTask.findMany.mockResolvedValue([{ id: 'task-client', tenantId: 'tenant-1', sourceType: 'client' }]);
|
||||
prisma.smsMessageRecord.groupBy.mockResolvedValue([]);
|
||||
|
||||
await service.listBatchTasks('tenant-1', 'queued');
|
||||
|
||||
expect(prisma.smsBatchTask.findMany).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { tenantId: 'tenant-1', status: 'queued', sourceType: 'client' },
|
||||
}));
|
||||
});
|
||||
|
||||
it('does not expose CMPP internal tasks through client task detail or messages', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsBatchTask.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(service.getBatchTask('task-cmpp', 'tenant-1', 'client')).rejects.toThrow('SMS batch task not found');
|
||||
await expect(service.listClientTaskMessages('task-cmpp', 'tenant-1')).rejects.toThrow('SMS batch task not found');
|
||||
expect(prisma.smsBatchTask.findFirst).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { id: 'task-cmpp', tenantId: 'tenant-1', sourceType: 'client' },
|
||||
}));
|
||||
expect(prisma.smsMessageRecord.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('terminates non-final tasks by canceling unsubmitted messages', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsBatchTask.findUnique.mockResolvedValue({ id: 'task-1', status: 'sending' });
|
||||
@@ -507,7 +542,7 @@ describe('SendChainService', () => {
|
||||
});
|
||||
|
||||
it('records an unreported CMPP message and returns success before delivering the template failure receipt', async () => {
|
||||
const { service, prisma } = createService();
|
||||
const { service, prisma, riskReview } = createService();
|
||||
prisma.smsTemplate.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(service.submitInboundMessage({
|
||||
@@ -524,6 +559,74 @@ describe('SendChainService', () => {
|
||||
'/downstream/receipt',
|
||||
expect.objectContaining({ receiptStatus: 'undelivered', rawStatus: 'REJECTD', errorCode: 'TEMPLATE' }),
|
||||
);
|
||||
expect(riskReview.aggregateTemplateMismatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('aggregates template-mismatched CMPP messages only when the application uses manual review', async () => {
|
||||
const { service, prisma, riskReview } = createService();
|
||||
prisma.smsApplication.findFirst.mockResolvedValue({
|
||||
id: 'app-1',
|
||||
tenantId: 'tenant-1',
|
||||
cmppAccount: '100001',
|
||||
status: 'active',
|
||||
interfaceEnabled: true,
|
||||
templateMismatchMode: 'manual_review',
|
||||
customerUnitPrice: 3,
|
||||
queuePriority: 'normal',
|
||||
ipAllowlist: [{ ipCidr: '127.0.0.1/32' }],
|
||||
tenant: { id: 'tenant-1', status: 'active', certificationStatus: 'approved' },
|
||||
});
|
||||
prisma.smsTemplate.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(service.submitInboundMessage({
|
||||
account: '100001',
|
||||
phoneNumber: '13800000001',
|
||||
content: '【签名】未匹配模板的内容',
|
||||
remoteIp: '127.0.0.1',
|
||||
})).resolves.toEqual(expect.objectContaining({ accepted: true, messageRecordId: 'record-1' }));
|
||||
|
||||
expect(riskReview.aggregateTemplateMismatch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
applicationId: 'app-1',
|
||||
account: '100001',
|
||||
messageRecordId: 'record-1',
|
||||
signatureId: 'sig-1',
|
||||
}));
|
||||
expect(prisma.smsBatchTask.update).toHaveBeenCalledWith({
|
||||
where: { id: 'task-1' },
|
||||
data: expect.objectContaining({ status: 'pending_review', riskTaskId: 'review-task-1', auditStatus: 'pending' }),
|
||||
});
|
||||
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fans an approved aggregated review task back into each internal CMPP batch', async () => {
|
||||
const { service, prisma } = createService();
|
||||
service.enqueueBatchTask = jest.fn().mockResolvedValue({ taskId: 'task-1', enqueued: 1 });
|
||||
prisma.smsSendTask.findUnique.mockResolvedValue({
|
||||
id: 'review-task-1',
|
||||
messageRecords: [{
|
||||
id: 'record-1',
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
batchTaskId: 'task-1',
|
||||
messageId: 'MSG-1',
|
||||
phoneNumber: '13800000001',
|
||||
amountCents: 3,
|
||||
billingUnits: 1,
|
||||
batchTask: { id: 'task-1', sourceType: 'cmpp' },
|
||||
}],
|
||||
});
|
||||
|
||||
await expect(service.handleReviewDecision('review-task-1', 'approved', '审核通过')).resolves.toEqual({
|
||||
reviewTaskId: 'review-task-1',
|
||||
decision: 'approved',
|
||||
affected: 1,
|
||||
});
|
||||
|
||||
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith({
|
||||
where: { id: 'record-1' },
|
||||
data: { status: 'queued', errorCode: null, errorMessage: null },
|
||||
});
|
||||
expect(service.enqueueBatchTask).toHaveBeenCalledWith('task-1');
|
||||
});
|
||||
|
||||
it('previews imported phone files with duplicate, invalid, blacklist, and variable errors', async () => {
|
||||
|
||||
Reference in New Issue
Block a user