import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { randomUUID } from 'node:crypto'; const url = new URL(process.env.COMPLETION_TEST_DATABASE_URL || ''); assert(['localhost', '127.0.0.1'].includes(url.hostname) && url.pathname.startsWith('/cmpp_qa_')); process.env.NODE_ENV = 'test'; process.env.DATABASE_URL = url.toString(); const require = createRequire(new URL('../../api/package.json', import.meta.url)); require('reflect-metadata'); const { PrismaService } = require('./dist/prisma/prisma.service'); const { SendChainService } = require('./dist/send-chain/send-chain.service'); const { BillingService } = require('./dist/billing/billing.service'); const db = new PrismaService(); const chain = new SendChainService(db, new BillingService(db), {}, {}); const key = () => randomUUID(); const results = []; try { const tenant = await db.tenant.create({ data: { name: '隔离审查', code: key() } }); const app = await db.smsApplication.create({ data: { tenantId: tenant.id, name: '隔离', cmppAccount: key(), cmppEnterpriseCode: '000001', secretHash: 'disabled', interfaceEnabled: true, }, }); const channel = await db.smsChannel.create({ data: { name: '隔离不联网', code: key(), gatewayHost: '127.0.0.1', gatewayPort: 1, account: key(), passwordCipher: 'unused', srcId: '1069', carriers: ['mobile'], status: 'disabled', }, }); async function fixture(business = false) { const mid = key(), sid = key(); const m = await db.smsMessageRecord.create({ data: { messageId: mid, phoneNumber: '13800138000', content: '隔离'.repeat(90), billingUnits: 2, status: 'submitted', channelId: channel.id, submitId: sid, ...(business ? { tenantId: tenant.id, applicationId: app.id, cmppSubmitSequenceId: '42', cmppRegisteredDelivery: true } : {}), }, }); const s = await db.smsSubmitRecord.create({ data: { messageRecordId: m.id, tenantId: m.tenantId, channelId: channel.id, submitId: sid, submitStatus: 'accepted', }, }); return { m, s }; } const receipt = (m, g, status = 'delivered') => ({ messageId: m.messageId, channelId: channel.id, gatewayMessageId: g, phoneNumber: m.phoneNumber, receiptStatus: status, rawStatus: status === 'delivered' ? 'DELIVRD' : 'FAIL', deliveredAt: new Date().toISOString(), }); async function segment(m, s, index, g) { return chain.handleSubmitSegmentResult({ messageId: m.messageId, channelId: channel.id, submitId: s.submitId, gatewayMessageId: g, sequenceId: index, segmentIndex: index, segmentTotal: 2, submitStatus: 'accepted', }); } // 1. Two attempts reuse one supplier ID: only target attempt should change. { const { m, s } = await fixture(); const g = key(); await segment(m, s, 1, g); const ch2 = await db.smsChannel.create({ data: { name: '隔离通道2', code: key(), gatewayHost: '127.0.0.1', gatewayPort: 2, account: key(), passwordCipher: 'unused', srcId: '1069', carriers: ['mobile'], status: 'disabled', }, }); const other = await db.smsSubmitRecord.create({ data: { messageRecordId: m.id, channelId: ch2.id, submitId: key(), submitStatus: 'accepted' }, }); await db.smsMessageSegmentAudit.create({ data: { messageRecordId: m.id, submitRecordId: other.id, submitId: other.submitId, channelId: ch2.id, gatewayMessageId: g, segmentIndex: 1, segmentTotal: 2, }, }); const resolved = await chain.resolveReceiptMessage(receipt(m, g)); await chain.handleReceipt(receipt(m, g)); const rows = await db.smsMessageSegmentAudit.findMany({ where: { messageRecordId: m.id } }); assert.equal(rows.filter((r) => r.receiptStatus === 'delivered').length, 1); assert.equal(resolved.channelId, channel.id); assert.equal(rows.find((r) => r.submitRecordId === other.id).receiptStatus, null); results.push({ passed: 'cross_attempt_update', updatedAttempts: 1, expected: 1, matchedWrongChannel: resolved.channelId !== channel.id, }); } // 2. Final failure preserves accounting and downstream result despite contradictory success. { const { m, s } = await fixture(true); const gs = [key(), key()]; await segment(m, s, 1, gs[0]); await segment(m, s, 2, gs[1]); await db.smsMessageRecord.update({ where: { id: m.id }, data: { status: 'failed', receiptStatus: 'undelivered', amountCents: 100n }, }); await db.smsBillingRecord.create({ data: { tenantId: tenant.id, applicationId: app.id, messageId: m.messageId, phoneNumber: m.phoneNumber, contentLength: 180, billingUnits: 2, unitPrice: 50n, amountCents: 100n, billingStatus: 'refunded', }, }); await db.cmppDownstreamDelivery.create({ data: { tenantId: tenant.id, applicationId: app.id, messageRecordId: m.id, messageId: m.messageId, dedupeKey: 'receipt:' + m.id, deliveryType: 'receipt', payload: { receiptStatus: 'undelivered' }, status: 'delivered', }, }); await chain.handleReceipt(receipt(m, gs[0])); await chain.handleReceipt(receipt(m, gs[1])); const updated = await db.smsMessageRecord.findUnique({ where: { id: m.id } }); const bill = await db.smsBillingRecord.findFirst({ where: { messageId: m.messageId } }); const notice = await db.cmppDownstreamDelivery.findUnique({ where: { dedupeKey: 'receipt:' + m.id } }); assert.equal(updated.status, 'failed'); assert.equal(await db.smsReceiptAnomaly.count({ where: { messageRecordId: m.id } }), 1); assert.equal( await db.smsMessageSegmentAudit.count({ where: { messageRecordId: m.id, receiptStatus: 'delivered' } }), 0, ); assert.equal(bill.billingStatus, 'refunded'); assert.equal(notice.payload.receiptStatus, 'undelivered'); results.push({ passed: 'contradictory_final', message: updated.status, billing: bill.billingStatus, notice: notice.payload.receiptStatus, }); } // 3. Second-fragment receipt precedes its SubmitSegmentResult metadata. { const { m, s } = await fixture(); const gs = [key(), key()]; await segment(m, s, 1, gs[0]); await assert.rejects(() => chain.handleReceipt(receipt(m, gs[1])), /提交尝试关联/); await segment(m, s, 2, gs[1]); await chain.handleReceipt(receipt(m, gs[1])); await chain.handleReceipt(receipt(m, gs[0])); const rows = await db.smsMessageSegmentAudit.findMany({ where: { messageRecordId: m.id }, orderBy: { segmentIndex: 'asc' }, }); const updated = await db.smsMessageRecord.findUnique({ where: { id: m.id } }); const count = await db.smsReceiptRecord.count({ where: { messageRecordId: m.id, receiptStatus: 'delivered' } }); assert.equal(count, 2); assert.equal(updated.status, 'delivered'); results.push({ passed: 'early_fragment_defers_then_recovers', receiptFacts: count, message: updated.status, segments: rows.map((r) => ({ index: r.segmentIndex, status: r.receiptStatus })), }); } // 4. Late failed fragments must not repeat route decisions after final failure. { const { m, s } = await fixture(true); const task = await db.smsBatchTask.create({ data: { tenantId: tenant.id, applicationId: app.id, taskNo: key(), content: '隔离', phoneTotal: 1 }, }); await db.smsMessageRecord.update({ where: { id: m.id }, data: { batchTaskId: task.id } }); const gs = [key(), key()]; await segment(m, s, 1, gs[0]); await segment(m, s, 2, gs[1]); const priorFind = chain.findApplicationRoute, priorSelect = chain.selectChannelForMessage; let selections = 0; chain.findApplicationRoute = async () => ({ group: { retryEnabled: true, retryTimeLimitMinutes: 60 } }); chain.selectChannelForMessage = async () => { selections++; throw new (require('@nestjs/common').BadRequestException)('isolated no route'); }; try { await chain.handleReceipt(receipt(m, gs[0], 'undelivered')); const afterFirst = await db.smsMessageRecord.findUnique({ where: { id: m.id } }); assert.equal(afterFirst.status, 'failed'); await Promise.all(Array.from({ length: 8 }, () => chain.handleReceipt(receipt(m, gs[1], 'undelivered')))); assert.equal(selections, 1); results.push({ passed: 'repeated_terminal_routing', routeSelections: selections, statusAfterFirst: afterFirst.status, routingBoundaryIsolated: true, }); } finally { chain.findApplicationRoute = priorFind; chain.selectChannelForMessage = priorSelect; } } // 5. Same-channel collision between a primary ID and another attempt's fragment is ambiguous. { const { m, s } = await fixture(); const g = key(); await segment(m, s, 1, g); const other = await db.smsSubmitRecord.create({ data: { messageRecordId: m.id, channelId: channel.id, submitId: key(), gatewayMessageId: g, submitStatus: 'accepted', }, }); await assert.rejects(() => chain.handleReceipt(receipt(m, g)), /提交尝试关联/); assert.equal(await db.smsReceiptRecord.count({ where: { messageRecordId: m.id } }), 0); assert.equal( await db.smsMessageSegmentAudit.count({ where: { messageRecordId: m.id, receiptStatus: 'delivered' } }), 0, ); results.push({ passed: 'ambiguous_primary_fragment_rejected', otherAttempt: other.id }); } // 6. Unknown is not a final failure; all actual fragments may still complete it. { const { m, s } = await fixture(); const gs = [key(), key()]; await segment(m, s, 1, gs[0]); await segment(m, s, 2, gs[1]); await db.smsMessageRecord.update({ where: { id: m.id }, data: { status: 'unknown' } }); await Promise.all(gs.map((g) => chain.handleReceipt(receipt(m, g)))); assert.equal((await db.smsMessageRecord.findUnique({ where: { id: m.id } })).status, 'delivered'); results.push({ passed: 'unknown_and_concurrent_fragments_recover' }); } // 7. Late submit rejection cannot reopen a final failure or create a successor. { const { m, s } = await fixture(); const g = key(); await segment(m, s, 1, g); await db.smsMessageRecord.update({ where: { id: m.id }, data: { status: 'failed', receiptStatus: 'undelivered' } }); await chain.handleSubmitResult({ messageId: m.messageId, channelId: channel.id, submitId: s.submitId, gatewayMessageId: g, submitStatus: 'rejected', }); assert.equal((await db.smsMessageRecord.findUnique({ where: { id: m.id } })).status, 'failed'); assert.equal(await db.smsSubmitRecord.count({ where: { messageRecordId: m.id } }), 1); results.push({ passed: 'late_submit_rejection_preserves_final' }); } // 8. Legacy audits missing the FK must join the verified attempt before aggregation. { const { m, s } = await fixture(); const gs = [key(), key()]; await segment(m, s, 1, gs[0]); await segment(m, s, 2, gs[1]); await db.smsMessageSegmentAudit.updateMany({ where: { messageRecordId: m.id }, data: { submitRecordId: null } }); for (const g of gs) await chain.handleReceipt(receipt(m, g)); assert.equal((await db.smsMessageRecord.findUnique({ where: { id: m.id } })).status, 'delivered'); assert.equal(await db.smsMessageSegmentAudit.count({ where: { messageRecordId: m.id, submitRecordId: s.id } }), 2); results.push({ passed: 'legacy_fragment_relation_recovered' }); } console.log(JSON.stringify(results)); } finally { await db.$disconnect(); }