feat: restore credit limits and harden SMS sending

This commit is contained in:
hectorzhao
2026-07-14 18:46:35 +08:00
parent 3e3b7a9d1a
commit c1a17699db
23 changed files with 796 additions and 56 deletions
+36 -5
View File
@@ -1654,18 +1654,49 @@ describe('SendChainService', () => {
await expect(service.batchRequeueDownstreamDeliveries([])).rejects.toThrow('请选择至少一条下游投递记录');
});
it('marks 72 hour unknown receipts as timeout', async () => {
const { service, prisma } = createService();
it('marks submitted or unknown messages without a final receipt for 72 hours as timeout and refunds them', async () => {
const { service, prisma, billing } = createService();
prisma.smsMessageRecord.findMany.mockResolvedValue([
{ id: 'record-1', batchTaskId: 'task-1' },
{ id: 'record-2', batchTaskId: 'task-1' },
{ id: 'record-1', tenantId: 'tenant-1', batchTaskId: 'task-1', messageId: 'MSG-1', amountCents: 3, billingUnits: 1 },
{ id: 'record-2', tenantId: 'tenant-1', batchTaskId: 'task-1', messageId: 'MSG-2', amountCents: 3, billingUnits: 1 },
]);
prisma.smsBillingRecord.findFirst
.mockResolvedValueOnce(null).mockResolvedValueOnce({ id: 'bill-1', billingStatus: 'charged' })
.mockResolvedValueOnce(null).mockResolvedValueOnce({ id: 'bill-2', billingStatus: 'charged' });
await expect(service.markUnknownTimeout({ olderThanHours: 72 })).resolves.toEqual({ timeout: 2 });
expect(prisma.smsMessageRecord.findMany).toHaveBeenCalledWith({
where: {
tenantId: { not: null },
status: { in: ['submitted', 'unknown'] },
submittedAt: { lte: expect.any(Date) },
},
select: { id: true, tenantId: true, batchTaskId: true, messageId: true, amountCents: true, billingUnits: true },
take: 10000,
});
expect(prisma.smsMessageRecord.updateMany).toHaveBeenCalledWith({
where: { id: { in: ['record-1', 'record-2'] } },
where: { id: 'record-1', status: { in: ['submitted', 'unknown'] } },
data: expect.objectContaining({ status: 'timeout', errorMessage: '72小时未收到明确回执,自动转超时' }),
});
expect(billing.refund).toHaveBeenCalledTimes(2);
expect(prisma.smsBatchTask.update).toHaveBeenCalled();
});
it('starts the automatic receipt-timeout scan after application startup', async () => {
jest.useFakeTimers();
const previousEnabled = process.env.SMS_RECEIPT_TIMEOUT_SCAN_ENABLED;
const { service } = createService();
const scan = jest.spyOn(service, 'markUnknownTimeout').mockResolvedValue({ timeout: 0 });
try {
process.env.SMS_RECEIPT_TIMEOUT_SCAN_ENABLED = 'true';
service.onModuleInit();
await jest.advanceTimersByTimeAsync(60_000);
expect(scan).toHaveBeenCalledWith({});
await service.onModuleDestroy();
} finally {
if (previousEnabled === undefined) delete process.env.SMS_RECEIPT_TIMEOUT_SCAN_ENABLED;
else process.env.SMS_RECEIPT_TIMEOUT_SCAN_ENABLED = previousEnabled;
jest.useRealTimers();
}
});
});