fix: reconcile shared-channel receipts and protocol logs
This commit is contained in:
@@ -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([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user