import assert from 'node:assert/strict'; import test from 'node:test'; import { buildInsufficientBalancePayload, createRechargeService, isAllowedRechargeAmount, loadRechargeConfig, } from './billing-recharge.mjs'; test('loadRechargeConfig parses tier list', () => { const previous = process.env.H5_RECHARGE_TIERS_CENTS; process.env.H5_RECHARGE_TIERS_CENTS = '500,1000,3000'; try { const config = loadRechargeConfig(); assert.deepEqual(config.tiersCents, [500, 1000, 3000]); assert.equal(config.minRechargeCents, 500); } finally { if (previous === undefined) delete process.env.H5_RECHARGE_TIERS_CENTS; else process.env.H5_RECHARGE_TIERS_CENTS = previous; } }); test('isAllowedRechargeAmount only accepts configured tiers', () => { const config = { tiersCents: [500, 1000, 3000], minRechargeCents: 500 }; assert.equal(isAllowedRechargeAmount(1000, config), true); assert.equal(isAllowedRechargeAmount(2000, config), false); }); test('buildInsufficientBalancePayload includes recharge hints', () => { const config = { tiersCents: [500, 1000], minRechargeCents: 500 }; const payload = buildInsufficientBalancePayload(0, config); assert.equal(payload.code, 'INSUFFICIENT_BALANCE'); assert.equal(payload.balanceCents, 0); assert.deepEqual(payload.suggestedTiers, [500, 1000]); }); test('fulfillOrder routes template_purchase to template catalog service', async () => { const orderRow = { id: 'order-tpl-1', user_id: 'user-1', amount_cents: 990, channel: 'wechat', status: 'pending', pay_mode: 'native', out_trade_no: 'TPTEST001', provider_txn: null, paid_at: null, expire_at: Date.now() + 60_000, created_at: Date.now(), code_url: null, h5_url: null, order_purpose: 'template_purchase', order_purpose_ref: 'page-template-travel', }; const pool = { async getConnection() { const conn = { async beginTransaction() {}, async commit() {}, async rollback() {}, async release() {}, async query(sql, params = []) { const text = String(sql); if (text.includes('FROM h5_payment_orders WHERE id = ? FOR UPDATE')) { return [[orderRow], []]; } if (text.includes('UPDATE h5_payment_orders') && text.includes("status = 'paid'")) { orderRow.status = 'paid'; return [{ affectedRows: 1 }, []]; } throw new Error(`Unhandled SQL: ${text.slice(0, 100)}`); }, }; return conn; }, }; let templateFulfillArgs = null; const templateCatalogService = { async fulfillWechatPurchaseLocked(args) { templateFulfillArgs = args; return { ok: true, skillName: 'page-template-travel', grantedSkills: ['page-template-travel'], balanceCents: 1200, }; }, }; const userAuth = { async getUserById(userId) { return { id: userId, balance_cents: 1200 }; }, async recharge() { throw new Error('recharge should not run for template_purchase'); }, }; const service = createRechargeService(pool, { userAuth, templateCatalogService }); const result = await service.fulfillOrder( { id: orderRow.id, userId: orderRow.user_id, amountCents: orderRow.amount_cents, status: 'pending', orderPurpose: 'template_purchase', orderPurposeRef: 'page-template-travel', }, { trade_state: 'SUCCESS', amount: { total: 990 }, transaction_id: 'wx-txn-1' }, ); assert.equal(result.ok, true); assert.equal(result.skillName, 'page-template-travel'); assert.deepEqual(result.grantedSkills, ['page-template-travel']); assert.equal(result.balanceCents, 1200); assert.ok(templateFulfillArgs); assert.equal(orderRow.status, 'paid'); });