feat: filter SMS routes by channel sensitive words
This commit is contained in:
@@ -4,6 +4,7 @@ import { BillingService } from '../billing/billing.service';
|
||||
import { RiskReviewService } from '../risk-review/risk-review.service';
|
||||
import { SendChainService } from './send-chain.service';
|
||||
import { DrainageRejection } from './drainage-authorization';
|
||||
import { ChannelWordRejection } from './channel-sensitive-routing';
|
||||
|
||||
function createPrismaMock() {
|
||||
const task = { id: 'task-1', tenantId: 'tenant-1', status: 'ready', phoneTotal: 2 };
|
||||
@@ -77,6 +78,8 @@ function createPrismaMock() {
|
||||
},
|
||||
};
|
||||
const prisma = {
|
||||
channelSensitiveWord: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
smsChannelSensitiveDecision: { createMany: jest.fn().mockResolvedValue({ count: 1 }) },
|
||||
tenant: {
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'tenant-1', status: 'active', certificationStatus: 'approved' }),
|
||||
},
|
||||
@@ -498,6 +501,81 @@ function createService(prisma = createPrismaMock(), openApi?: { queueWebhookEven
|
||||
}
|
||||
|
||||
describe('SendChainService', () => {
|
||||
it('recovers non-CMPP channel-word finalization without pushing a receipt', async () => {
|
||||
const { service, prisma } = createService();
|
||||
const message = await prisma.smsMessageRecord.findUnique({ where: { id: 'record-1' } });
|
||||
prisma.smsMessageRecord.findMany.mockResolvedValue([
|
||||
{ ...message, channelWordFinalizationPending: true, batchTask: { sourceType: 'client' } },
|
||||
]);
|
||||
service['releaseMessageReservation'] = jest.fn();
|
||||
service['recordCmppFailureReceipt'] = jest.fn();
|
||||
service['refreshTaskProgress'] = jest.fn();
|
||||
await service.recoverDrainageFailureReceipts();
|
||||
expect(service['releaseMessageReservation']).toHaveBeenCalledTimes(1);
|
||||
expect(service['refreshTaskProgress']).toHaveBeenCalledWith('task-1');
|
||||
expect(service['recordCmppFailureReceipt']).not.toHaveBeenCalled();
|
||||
expect(prisma.smsMessageRecord.update).toHaveBeenLastCalledWith({
|
||||
where: { id: 'record-1' },
|
||||
data: { channelWordFinalizationPending: false },
|
||||
});
|
||||
});
|
||||
it.each(['cmpp', 'client', 'http'])(
|
||||
'fails an all-hit ordinary route without supplier submit (%s)',
|
||||
async (sourceType) => {
|
||||
const { service, prisma } = createService();
|
||||
const message = await prisma.smsMessageRecord.findUnique({ where: { id: 'record-1' } });
|
||||
prisma.smsMessageRecord.findUnique.mockResolvedValue({ ...message, batchTask: { id: 'task-1', sourceType } });
|
||||
service['selectChannelForMessage'] = jest.fn().mockRejectedValue(new ChannelWordRejection());
|
||||
service['recordCmppFailureReceipt'] = jest.fn();
|
||||
service['releaseMessageReservation'] = jest.fn();
|
||||
service['refreshTaskProgress'] = jest.fn();
|
||||
await service.processSendJob({ messageRecordId: 'record-1' });
|
||||
expect(service['publishGatewaySubmitCommand']).not.toHaveBeenCalled();
|
||||
expect(service['releaseMessageReservation']).toHaveBeenCalledTimes(1);
|
||||
if (sourceType === 'cmpp')
|
||||
expect(service['recordCmppFailureReceipt']).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'CSW',
|
||||
'可用通道均命中通道敏感词',
|
||||
);
|
||||
else expect(service['recordCmppFailureReceipt']).not.toHaveBeenCalled();
|
||||
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ channelWordFinalizationPending: true, drainageReceiptPending: false }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
it('recovers channel-word delivery intent from an existing receipt and keeps pending on failure', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsReceiptRecord.findFirst.mockResolvedValue({ id: 'existing-receipt' });
|
||||
const message = {
|
||||
id: 'record-1',
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
messageId: 'MSG-1',
|
||||
phoneNumber: '13800000001',
|
||||
cmppRegisteredDelivery: true,
|
||||
cmppSubmitSequenceId: '101',
|
||||
};
|
||||
service['queueAndTryDownstreamDelivery'] = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(Error('persistence failed'))
|
||||
.mockResolvedValue({ id: 'delivery' });
|
||||
await expect(service['recordCmppFailureReceipt'](message, 'CSW', '可用通道均命中通道敏感词')).rejects.toThrow(
|
||||
'persistence failed',
|
||||
);
|
||||
expect(prisma.smsMessageRecord.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: { channelWordFinalizationPending: false } }),
|
||||
);
|
||||
await service['recordCmppFailureReceipt'](message, 'CSW', '可用通道均命中通道敏感词');
|
||||
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
|
||||
expect(prisma.smsReceiptRecord.upsert).not.toHaveBeenCalled();
|
||||
expect(prisma.smsMessageRecord.update).toHaveBeenLastCalledWith({
|
||||
where: { id: 'record-1' },
|
||||
data: { channelWordFinalizationPending: false },
|
||||
});
|
||||
});
|
||||
it('recovers an existing drainage rejection receipt without duplicating it', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsReceiptRecord.findFirst.mockResolvedValue({ id: 'existing-receipt' });
|
||||
|
||||
Reference in New Issue
Block a user