Files
qipai/backend/tests/recharge-service.test.mjs
T
2026-06-25 12:40:21 +08:00

370 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import { RechargeError, RechargeService } from '../dist/wallets/recharge-service.js';
const future = new Date(Date.now() + 86_400_000);
const past = new Date(Date.now() - 86_400_000);
function createHarness(overrides = {}) {
const state = {
plan: {
id: '31',
tenantId: '7',
storeId: '11',
name: '充 100 送 20',
payAmountCents: 10000,
giftAmountCents: 2000,
scopeType: 'STORE',
startsAt: past,
endsAt: future,
purchaseLimitPerUser: 2,
status: 'ACTIVE',
...overrides.plan
},
orders: [],
callbacks: [],
nextOrderId: 900,
purchaseCount: overrides.purchaseCount ?? 0,
walletCredits: []
};
const connection = {
async beginTransaction() {},
async commit() {},
async rollback() {},
release() {},
async execute(sql, params) {
if (sql.includes('FROM qipai_recharge_orders') && sql.includes('client_request_id')) {
const duplicate = state.orders.find((order) =>
order.tenantId === String(params[0])
&& order.userId === String(params[1])
&& order.clientRequestId === params[2]
);
return [duplicate ? [toOrderRow(duplicate)] : [], []];
}
if (sql.includes('FROM qipai_recharge_plans')) {
if (
state.plan
&& state.plan.tenantId === String(params[0])
&& state.plan.id === String(params[1])
) {
return [[state.plan], []];
}
return [[], []];
}
if (sql.includes('COUNT(*) AS total')) {
return [[{ total: state.purchaseCount }], []];
}
if (sql.includes('INSERT INTO qipai_recharge_orders')) {
const order = {
id: String(state.nextOrderId++),
tenantId: String(params[0]),
userId: String(params[1]),
storeId: params[2] === null ? null : String(params[2]),
planId: String(params[3]),
rechargeNo: params[4],
clientRequestId: params[5],
payAmountCents: Number(params[6]),
giftAmountCents: Number(params[7]),
traceId: params[8],
status: 'PENDING_PAYMENT',
paymentId: null
};
state.orders.push(order);
return [{ insertId: order.id, affectedRows: 1 }, []];
}
if (sql.includes('FROM qipai_recharge_orders') && sql.includes('user_id = ?')) {
const order = state.orders.find((item) =>
item.tenantId === String(params[0])
&& item.id === String(params[1])
&& item.userId === String(params[2])
);
return [order ? [toOrderRow(order)] : [], []];
}
if (sql.includes('SELECT openid FROM qipai_user_identities')) {
return [[{ openid: 'openid-recharge-test' }], []];
}
if (sql.includes('provider_prepay_id')) {
const order = state.orders.find((item) =>
item.tenantId === String(params[1])
&& item.id === String(params[2])
&& item.userId === String(params[3])
);
order.providerPrepayId = params[0];
return [{ affectedRows: 1 }, []];
}
if (sql.includes('FROM qipai_recharge_orders') && sql.includes('recharge_no = ?')) {
const order = state.orders.find((item) => item.rechargeNo === String(params[0]));
return [order ? [{ ...toOrderRow(order), tenantId: order.tenantId }] : [], []];
}
if (sql.includes('FROM qipai_recharge_orders') && sql.includes('FOR UPDATE')) {
const order = state.orders.find((item) =>
item.tenantId === String(params[0]) && item.id === String(params[1])
);
return [order ? [toOrderRow(order)] : [], []];
}
if (sql.includes('INSERT IGNORE INTO qipai_payment_callbacks')) {
const duplicate = state.callbacks.some((item) =>
item.tenantId === String(params[0]) && item.callbackId === String(params[1])
);
if (!duplicate) {
state.callbacks.push({
tenantId: String(params[0]),
callbackId: String(params[1]),
status: 'RECEIVED',
payload: params[2]
});
}
return [{ affectedRows: duplicate ? 0 : 1 }, []];
}
if (sql.includes('provider_payment_id')) {
const order = state.orders.find((item) =>
item.tenantId === String(params[3]) && item.id === String(params[4])
);
order.status = 'PAID';
order.providerPaymentId = String(params[0]);
order.providerCallbackId = String(params[1]);
order.rawNotify = params[2];
return [{ affectedRows: 1 }, []];
}
if (sql.includes("SET status = 'PAID'")) {
const order = state.orders.find((item) =>
item.tenantId === String(params[1]) && item.id === String(params[2])
);
order.status = 'PAID';
order.paymentId = String(params[0]);
return [{ affectedRows: 1 }, []];
}
if (sql.includes("SET status = 'CREDITED'")) {
const order = state.orders.find((item) =>
item.tenantId === String(params[0]) && item.id === String(params[1])
);
order.status = 'CREDITED';
return [{ affectedRows: 1 }, []];
}
if (sql.includes('UPDATE qipai_payment_callbacks')) {
const callback = state.callbacks.find((item) =>
item.tenantId === String(params[0]) && item.callbackId === String(params[1])
) ?? state.callbacks.find((item) =>
item.tenantId === String(params[1]) && item.callbackId === String(params[2])
);
if (callback) callback.status = sql.includes('REJECTED') ? 'REJECTED' : 'PROCESSED';
return [{ affectedRows: 1 }, []];
}
throw new Error(`Unexpected SQL: ${sql}`);
}
};
const pool = {
async execute(sql, params) {
return connection.execute(sql, params);
},
async getConnection() {
return connection;
}
};
const wallet = {
async credit(input) {
state.walletCredits.push(input);
return {
ledgerEntryId: String(state.walletCredits.length),
cashBalanceCents: input.cashDeltaCents,
giftBalanceCents: input.giftDeltaCents,
idempotent: false
};
}
};
const wechat = {
paymentRepository: {
async resolveConfig(_connection, tenantId, platformAppId, storeId, provider) {
state.resolvedConfig = { tenantId, platformAppId, storeId, provider };
return {
credentialRef: 'wechat-test',
settings: {
rechargeDescription: '测试充值',
rechargeNotifyUrl: 'https://api.txyundm.cn/app-api/recharge/wechat/notify'
}
};
}
},
client: {
async createJsapiPrepay(_credential, input) {
state.prepayInput = input;
return {
prepayId: 'wx-recharge-prepay',
paymentParams: {
timeStamp: '1782360000',
nonceStr: 'nonce',
package: 'prepay_id=wx-recharge-prepay',
signType: 'RSA',
paySign: 'signed'
}
};
},
verifyAndDecrypt(_credential, headers, rawBody) {
state.verifiedNotification = { headers, rawBody };
return {
out_trade_no: state.orders[0].rechargeNo,
transaction_id: 'wx-recharge-transaction',
trade_state: 'SUCCESS',
amount: { total: state.orders[0].payAmountCents },
payer: { openid: 'redacted' }
};
}
},
credentials: new Map([[
'wechat-test',
{
appId: 'wx-test-app',
merchantId: '1900000109',
serialNo: 'MERCHANT-SERIAL',
privateKeyPem: 'test-private-key',
apiV3Key: '0123456789abcdef0123456789abcdef',
platformCertificates: { 'PLATFORM-SERIAL': 'test-cert' }
}
]])
};
return { service: new RechargeService(pool, wallet, wechat), state };
}
function toOrderRow(order) {
return {
id: order.id,
userId: order.userId,
storeId: order.storeId,
planId: order.planId,
rechargeNo: order.rechargeNo,
payAmountCents: order.payAmountCents,
giftAmountCents: order.giftAmountCents,
status: order.status
};
}
const baseOrder = {
tenantId: '7',
userId: '21',
planId: '31',
storeId: '11',
clientRequestId: 'client-001',
traceId: 'recharge-test'
};
{
const { service, state } = createHarness();
const created = await service.createRechargeOrder(baseOrder);
assert.equal(created.status, 'PENDING_PAYMENT');
assert.equal(created.payAmountCents, 10000);
assert.equal(created.giftAmountCents, 2000);
assert.equal(created.idempotent, false);
assert.equal(state.orders.length, 1);
const duplicate = await service.createRechargeOrder(baseOrder);
assert.equal(duplicate.rechargeOrderId, created.rechargeOrderId);
assert.equal(duplicate.idempotent, true);
assert.equal(state.orders.length, 1);
}
{
const { service } = createHarness({ plan: { status: 'DISABLED' } });
await assert.rejects(
() => service.createRechargeOrder(baseOrder),
(error) => error instanceof RechargeError && error.code === 'RECHARGE_PLAN_DISABLED'
);
}
{
const { service } = createHarness({ plan: { endsAt: past } });
await assert.rejects(
() => service.createRechargeOrder(baseOrder),
(error) => error instanceof RechargeError && error.code === 'RECHARGE_PLAN_EXPIRED'
);
}
{
const { service } = createHarness({ purchaseCount: 2 });
await assert.rejects(
() => service.createRechargeOrder(baseOrder),
(error) => error instanceof RechargeError && error.code === 'RECHARGE_LIMIT_REACHED'
);
}
{
const { service, state } = createHarness();
const created = await service.createRechargeOrder(baseOrder);
const credited = await service.markPaidAndCredit({
tenantId: '7',
rechargeOrderId: created.rechargeOrderId,
paymentId: '501',
traceId: 'payment-callback'
});
assert.equal(credited.status, 'CREDITED');
assert.equal(credited.idempotent, false);
assert.equal(state.orders[0].status, 'CREDITED');
assert.equal(state.walletCredits.length, 1);
assert.deepEqual(
{
businessType: state.walletCredits[0].businessType,
businessId: state.walletCredits[0].businessId,
cashDeltaCents: state.walletCredits[0].cashDeltaCents,
giftDeltaCents: state.walletCredits[0].giftDeltaCents,
storeId: state.walletCredits[0].storeId
},
{
businessType: 'RECHARGE',
businessId: created.rechargeOrderId,
cashDeltaCents: 10000,
giftDeltaCents: 2000,
storeId: '11'
}
);
const duplicateCallback = await service.markPaidAndCredit({
tenantId: '7',
rechargeOrderId: created.rechargeOrderId,
paymentId: '501',
traceId: 'payment-callback'
});
assert.equal(duplicateCallback.idempotent, true);
assert.equal(state.walletCredits.length, 1);
}
{
const { service, state } = createHarness();
const created = await service.createRechargeOrder(baseOrder);
const prepay = await service.createWechatPrepay({
tenantId: '7',
platformAppId: '9',
userId: '21',
rechargeOrderId: created.rechargeOrderId
});
assert.equal(prepay.package, 'prepay_id=wx-recharge-prepay');
assert.equal(state.resolvedConfig.storeId, '11');
assert.equal(state.prepayInput.outTradeNo, created.rechargeNo);
assert.equal(state.prepayInput.amountCents, 10000);
assert.equal(state.orders[0].providerPrepayId, 'wx-recharge-prepay');
const notified = await service.processWechatNotification({
timestamp: '1782360001',
nonce: 'notify-nonce',
serial: 'PLATFORM-SERIAL',
signature: 'signed'
}, '{"resource":"encrypted"}', 'recharge-notify');
assert.equal(notified.status, 'CREDITED');
assert.equal(state.orders[0].providerPaymentId, 'wx-recharge-transaction');
assert.equal(state.orders[0].providerCallbackId, 'PLATFORM-SERIAL:wx-recharge-transaction:SUCCESS');
assert.equal(state.walletCredits.length, 1);
assert.equal(JSON.parse(state.orders[0].rawNotify).payer, undefined);
const duplicate = await service.processWechatNotification({
timestamp: '1782360001',
nonce: 'notify-nonce',
serial: 'PLATFORM-SERIAL',
signature: 'signed'
}, '{"resource":"encrypted"}', 'recharge-notify');
assert.equal(duplicate.idempotent, true);
assert.equal(state.walletCredits.length, 1);
}
console.log('PASS: M07-B recharge plans validate limits and credit paid callbacks idempotently.');