2026-06-15 15:04:43 -07:00
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import test from 'node:test';
|
|
|
|
|
import {
|
|
|
|
|
buildInsufficientBalancePayload,
|
2026-08-10 08:06:12 +08:00
|
|
|
createRechargeService,
|
2026-06-15 15:04:43 -07:00
|
|
|
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]);
|
|
|
|
|
});
|
2026-08-10 08:06:12 +08:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|