import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { randomUUID } from 'node:crypto'; const url = new URL(process.env.HOME_TEST_DATABASE_URL || ''); assert( ['127.0.0.1', 'localhost'].includes(url.hostname) && url.pathname.startsWith('/cmpp_qa_home_'), 'isolated local database required', ); Object.assign(process.env, { DATABASE_URL: url.toString(), NODE_ENV: 'test' }); const require = createRequire(new URL('../../api/package.json', import.meta.url)); require('reflect-metadata'); const { PrismaService } = require('./dist/prisma/prisma.service'); const { HomeProjection } = require('./dist/home-dashboard/home-projection'); const { HomeService } = require('./dist/home-dashboard/home.service'); const { todayKey, addDays, startOfDay } = require('./dist/signature-analytics/analytics-date'); const db = new PrismaService(), projector = new HomeProjection(db), read = new HomeService(db); const T = todayKey(), now = new Date(), stamp = randomUUID().slice(0, 8), pass = (s) => console.log('PASS', s); const at = (day, hour = 1) => new Date(startOfDay(day).getTime() + hour * 3600000); try { assert.equal( await db.smsMessageRecord.count(), 0, 'use a fresh disposable test database; existing data is never cleared', ); const tenant = await db.tenant.create({ data: { name: '首页验收企业甲', code: stamp } }); const second = await db.tenant.create({ data: { name: '首页验收企业乙', code: stamp + 'b' } }); for (const t of [tenant, second]) await db.tenantAccount.create({ data: { tenantId: t.id, balanceCents: 185675000n } }); const channel = await db.smsChannel.create({ data: { name: '首页隔离通道', code: stamp, gatewayHost: '127.0.0.1', gatewayPort: 1, account: 'no-send', passwordCipher: 'isolated', srcId: '1069', carriers: ['mobile'], status: 'disabled', }, }); let serial = 0; async function sms(day, states, units = 3, receiveDays = [], total = units) { const id = `${stamp}-${++serial}`; const delivered = states.length === total && states.every((s) => s === 'delivered'); const m = await db.smsMessageRecord.create({ data: { tenantId: tenant.id, messageId: id, phoneNumber: '13800000000', content: '隔离数据库验收,不发送', billingUnits: units, unitPrice: 500n, amountCents: BigInt(units) * 500n, status: delivered ? 'delivered' : 'failed', queuedAt: at(day), }, }); const attempt = await db.smsSubmitRecord.create({ data: { messageRecordId: m.id, channelId: channel.id, submitId: id, submitStatus: 'accepted', costUnitPrice: 300n, gatewayMessageId: id + '-0', }, }); for (let i = 0; i < states.length; i++) { await db.smsMessageSegmentAudit.create({ data: { messageRecordId: m.id, submitRecordId: attempt.id, channelId: channel.id, submitId: id, segmentIndex: i + 1, segmentTotal: total, gatewayMessageId: id + '-' + i, receiptStatus: states[i], submitStatus: 'accepted', }, }); await receipt(m, attempt, id + '-' + i, states[i], receiveDays[i] ?? T); } return { m, attempt }; } async function receipt(m, attempt, gatewayId, status, date, receivedAt = at(date)) { return db.upstreamReceiptInbox.create({ data: { receiptKey: randomUUID(), incomingChannelId: channel.id, upstreamAccount: 'no-send', upstreamHost: '127.0.0.1', upstreamPort: 1, protocol: 'cmpp', protocolVersion: '3.0', gatewayMessageId: gatewayId, receiptStatus: status, rawStatus: status, deliveredAt: receivedAt, gatewayReceivedAt: receivedAt, receivedAt: now, status: 'matched', matchedMessageRecordId: m.id, matchedSubmitRecordId: attempt.id, matchedChannelId: channel.id, }, }); } for (let offset = 0; offset < 4; offset++) { const day = addDays(T, -offset); await sms(day, ['delivered', 'delivered', 'delivered']); await sms(day, ['failed']); await sms(day, ['delivered', 'delivered']); await sms(day, ['delivered'], 1); } await sms(addDays(T, -4), ['delivered', 'delivered', 'delivered']); const cross = await sms(addDays(T, -2), ['delivered', 'delivered', 'delivered'], 3, [ addDays(T, -1), addDays(T, -1), T, ]); await receipt(cross.m, cross.attempt, cross.attempt.submitId + '-2', 'delivered', T, at(T, 2)); // Already completed yesterday: today's duplicate must not count. const yesterday = await sms(addDays(T, -1), ['delivered'], 1, [addDays(T, -1)]); await receipt(yesterday.m, yesterday.attempt, yesterday.attempt.submitId + '-0', 'delivered', T); for (const [type, amount, day, relatedType] of [ ['refunded', 123456n, T, 'sms_message_record'], ['released', 100n, T, 'sms_message_record'], ['released', 999n, T, 'other'], ['refunded', 999n, addDays(T, -1), 'sms_message_record'], ]) await db.accountTransaction.create({ data: { tenantId: tenant.id, transactionType: type, amountCents: amount, relatedType, createdAt: at(day) }, }); await db.smsBillingRecord.create({ data: { tenantId: tenant.id, contentLength: 5, billingUnits: 10, unitPrice: 500n, amountCents: 5000n, billingStatus: 'charged', createdAt: at(T), }, }); await assert.rejects(read.summary('qa'), /初始化/); await projector.tick(); const s = await read.summary('qa'); assert.equal(s.today.sent, 4); assert.equal(s.today.delivered, 2); assert.equal(s.today.successRate, 50); assert.equal(s.today.receiptUnits, 43); assert.equal(s.today.successUnits, 19); assert.equal(s.today.revenueCents, 9500); assert.equal(s.today.profitCents, 3800); assert.equal(s.enterpriseSpendRanks[0].todayReturnedCents, 123556); assert.equal(s.enterpriseSpendRanks[0].todaySpendCents, 5000); pass('four-day cohorts, long missing/partial success, cross-midnight completion, global replay dedup, refund ledger'); const r = await read.breakdown('qa', s.snapshotToken, 'receipt'), f = await read.breakdown('qa', s.snapshotToken, 'revenue'); assert.equal(r.items.length, 4); assert.equal(f.items.length, 4); assert.equal( r.items.reduce((a, b) => a + b.total, 0), s.today.receiptUnits, ); assert.equal( f.items.reduce((a, b) => a + b.revenueCents, 0), s.today.revenueCents, ); await assert.rejects(read.breakdown('other', s.snapshotToken, 'receipt'), /不可访问/); await assert.rejects(read.breakdown('qa', 'invalid', 'receipt'), /无效/); await assert.rejects(read.breakdown('qa', s.snapshotToken, 'receipt', new Date(now.getTime() + 3600000)), /过期/); pass('snapshot total reconciliation, ownership, validation, expiry'); await sms(T, ['delivered'], 1); await projector.tick(); assert.deepEqual(await read.breakdown('qa', s.snapshotToken, 'receipt'), r); assert.equal((await read.summary('qa')).today.successUnits, 20); pass('old immutable snapshot remains consistent after new receipt publication'); // Transaction rollback must leave both source and pending work unchanged. const before = await db.homeProjectionDirty.count(); await assert.rejects( db.$transaction(async (tx) => { await tx.smsMessageRecord.update({ where: { id: cross.m.id }, data: { unitPrice: 999n } }); throw new Error('injected rollback'); }), /injected/, ); assert.equal(await db.homeProjectionDirty.count(), before); assert.equal((await db.smsMessageRecord.findUnique({ where: { id: cross.m.id } })).unitPrice, 500n); const lock = db.$transaction(async (tx) => { await tx.$executeRaw`SELECT pg_advisory_xact_lock(17100917)`; await new Promise((r) => setTimeout(r, 500)); }); await new Promise((r) => setTimeout(r, 100)); assert.equal((await projector.tick()).busy, true); await lock; pass('source/dirty atomic rollback and independent worker atomic claim'); console.log( JSON.stringify({ database: url.pathname, fixtureMessages: await db.smsMessageRecord.count(), status: 'PASS' }), ); } finally { await db.$disconnect(); }