fix: reconcile shared-channel receipts and protocol logs

This commit is contained in:
hectorzhao
2026-07-24 12:52:06 +08:00
parent 3997349c21
commit ca12f14b00
14 changed files with 1110 additions and 43 deletions
@@ -160,6 +160,7 @@ function createPrismaMock() {
upsert: jest.fn().mockResolvedValue({ id: 'segment-1' }),
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
findFirst: jest.fn().mockResolvedValue(null),
findMany: jest.fn().mockResolvedValue([]),
},
cmppInboundLongMessage: {
create: jest.fn(),
@@ -1917,6 +1918,7 @@ describe('SendChainService', () => {
const { service, prisma } = createService();
prisma.smsMessageRecord.findUnique.mockResolvedValue(null);
prisma.smsSubmitRecord.findMany
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
@@ -2020,6 +2022,183 @@ describe('SendChainService', () => {
});
});
it('matches a receipt from another connection only when it is the unique channel of the same supplier', async () => {
const { service, prisma } = createService();
prisma.smsMessageRecord.findUnique.mockResolvedValue(null);
prisma.smsChannel.findUnique.mockResolvedValue({
id: 'channel-copy',
account: 'C59748',
gatewayHost: 'supplier.example.com',
gatewayPort: 7890,
protocol: 'CMPP',
cmppVersion: '2.0',
});
prisma.smsSubmitRecord.findMany
.mockResolvedValueOnce([]);
prisma.smsMessageSegmentAudit.findMany
.mockResolvedValueOnce([
{
id: 'segment-2',
submitId: 'SUB-LONG-1',
submitRecordId: 'submit-original',
channelId: 'channel-original',
gatewayMessageId: '736070230367350788',
submitRecord: { id: 'submit-original', submitId: 'SUB-LONG-1' },
channel: {
id: 'channel-original',
account: 'C59748',
gatewayHost: 'supplier.example.com',
gatewayPort: 7890,
protocol: 'CMPP',
cmppVersion: '2.0',
},
messageRecord: {
id: 'record-long',
tenantId: 'tenant-1',
batchTaskId: 'task-1',
applicationId: 'app-1',
messageId: 'MSG-LONG-1',
submitId: 'SUB-LONG-1',
phoneNumber: '13127620092',
channelId: 'channel-original',
gatewayMessageId: '736070227905294338',
status: 'submitted',
billingUnits: 2,
},
},
])
.mockResolvedValueOnce([]);
await service.handleReceipt({
messageId: 'receipt-736070230367350788',
channelId: 'channel-copy',
gatewayMessageId: '736070230367350788',
phoneNumber: '13127620092',
receiptStatus: 'delivered',
rawStatus: 'DELIVRD',
});
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledWith({
data: expect.objectContaining({
channelId: 'channel-original',
messageRecordId: 'record-long',
messageId: 'MSG-LONG-1',
}),
});
});
it('does not match the same Msg_Id across channels belonging to different suppliers', async () => {
const { service, prisma } = createService();
prisma.smsMessageRecord.findUnique.mockResolvedValue(null);
prisma.smsChannel.findUnique.mockResolvedValue({
id: 'channel-other',
account: 'OTHER',
gatewayHost: 'other.example.com',
gatewayPort: 7890,
protocol: 'CMPP',
cmppVersion: '2.0',
});
prisma.smsSubmitRecord.findMany
.mockResolvedValueOnce([])
.mockResolvedValueOnce([]);
prisma.smsMessageSegmentAudit.findMany.mockResolvedValueOnce([
{
id: 'segment-original',
submitRecordId: 'submit-original',
submitId: 'SUB-ORIGINAL',
channelId: 'channel-original',
gatewayMessageId: 'SHARED-ID',
submitRecord: { id: 'submit-original', submitId: 'SUB-ORIGINAL' },
channel: {
id: 'channel-original',
account: 'C59748',
gatewayHost: 'supplier.example.com',
gatewayPort: 7890,
protocol: 'CMPP',
cmppVersion: '2.0',
},
messageRecord: {
id: 'record-original',
messageId: 'MSG-ORIGINAL',
phoneNumber: '13127620092',
},
},
]);
await expect(service.handleReceipt({
messageId: 'receipt-SHARED-ID',
channelId: 'channel-other',
gatewayMessageId: 'SHARED-ID',
phoneNumber: '13127620092',
receiptStatus: 'delivered',
rawStatus: 'DELIVRD',
})).rejects.toThrow('SMS message record not found');
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
});
it('waits for every long-message segment before marking the main message delivered', async () => {
const { service, prisma } = createService();
prisma.smsMessageRecord.findUnique.mockResolvedValue({
id: 'record-long',
tenantId: 'tenant-1',
batchTaskId: 'task-1',
applicationId: 'app-1',
messageId: 'MSG-LONG-1',
submitId: 'SUB-LONG-1',
phoneNumber: '13127620092',
channelId: 'channel-1',
gatewayMessageId: 'GW-SEG-1',
status: 'submitted',
billingUnits: 2,
});
prisma.smsSubmitRecord.findFirst.mockResolvedValue({
id: 'submit-long',
submitId: 'SUB-LONG-1',
channelId: 'channel-1',
gatewayMessageId: 'GW-SEG-1',
});
prisma.smsMessageSegmentAudit.findMany
.mockResolvedValueOnce([
{ segmentIndex: 1, segmentTotal: 2, receiptStatus: 'delivered', rawStatus: 'DELIVRD', deliveredAt: new Date() },
{ segmentIndex: 2, segmentTotal: 2, receiptStatus: null, rawStatus: null, deliveredAt: null },
])
.mockResolvedValueOnce([
{ segmentIndex: 1, segmentTotal: 2, receiptStatus: 'delivered', rawStatus: 'DELIVRD', deliveredAt: new Date() },
{ segmentIndex: 2, segmentTotal: 2, receiptStatus: 'delivered', rawStatus: 'DELIVRD', deliveredAt: new Date() },
]);
await service.handleReceipt({
messageId: 'MSG-LONG-1',
channelId: 'channel-1',
gatewayMessageId: 'GW-SEG-1',
phoneNumber: '13127620092',
receiptStatus: 'delivered',
rawStatus: 'DELIVRD',
});
expect(prisma.smsMessageRecord.update).not.toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ status: 'delivered' }),
}));
expect(prisma.cmppDownstreamDelivery.create).not.toHaveBeenCalled();
prisma.smsReceiptRecord.findUnique.mockResolvedValue(null);
await service.handleReceipt({
messageId: 'MSG-LONG-1',
channelId: 'channel-1',
gatewayMessageId: 'GW-SEG-2',
phoneNumber: '13127620092',
receiptStatus: 'delivered',
rawStatus: 'DELIVRD',
});
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith(expect.objectContaining({
where: { id: 'record-long' },
data: expect.objectContaining({ status: 'delivered', receiptStatus: 'delivered' }),
}));
expect(prisma.cmppDownstreamDelivery.create).toHaveBeenCalledTimes(1);
});
it('treats a repeated DELIVRD event as idempotent and does not redeliver it downstream', async () => {
const { service, prisma } = createService();
prisma.smsReceiptRecord.findUnique
@@ -2050,6 +2229,7 @@ describe('SendChainService', () => {
const { service, prisma } = createService();
prisma.smsMessageRecord.findUnique.mockResolvedValue(null);
prisma.smsSubmitRecord.findMany
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{