feat(M07-B): 接入充值优惠规则

This commit is contained in:
Codex
2026-06-24 15:37:01 +08:00
parent db1d08ecf8
commit d9743d36d8
9 changed files with 578 additions and 6 deletions
+16 -1
View File
@@ -72,6 +72,9 @@ const iotMessagesVerifySql = read('database/migrations/2026062220_m06c_iot_messa
const walletUpSql = read('database/migrations/2026062421_m07a_wallet_ledger.up.sql');
const walletDownSql = read('database/migrations/2026062421_m07a_wallet_ledger.down.sql');
const walletVerifySql = read('database/migrations/2026062421_m07a_wallet_ledger.verify.sql');
const rechargeUpSql = read('database/migrations/2026062422_m07b_recharge_plans.up.sql');
const rechargeDownSql = read('database/migrations/2026062422_m07b_recharge_plans.down.sql');
const rechargeVerifySql = read('database/migrations/2026062422_m07b_recharge_plans.verify.sql');
const coreTables = [
'qipai_schema_migrations',
@@ -331,4 +334,16 @@ assert.match(walletUpSql, /uq_qipai_wallet_ledger_business/);
assert.match(walletUpSql, /cash_delta_cents INT NOT NULL DEFAULT 0/);
assert.match(walletUpSql, /gift_delta_cents INT NOT NULL DEFAULT 0/);
console.log('PASS: M01-B through M07-A migration contracts are present.');
for (const table of ['qipai_recharge_plans', 'qipai_recharge_orders']) {
assert.match(rechargeUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
assert.match(rechargeDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
assert.match(rechargeVerifySql, new RegExp(`'${table}'`));
}
assert.match(rechargeUpSql, /pay_amount_cents INT UNSIGNED NOT NULL/);
assert.match(rechargeUpSql, /gift_amount_cents INT UNSIGNED NOT NULL DEFAULT 0/);
assert.match(rechargeUpSql, /purchase_limit_per_user INT UNSIGNED/);
assert.match(rechargeUpSql, /UNIQUE KEY uq_qipai_recharge_order_request/);
assert.match(rechargeUpSql, /payment_id BIGINT UNSIGNED NULL/);
assert.match(rechargeUpSql, /credited_at DATETIME\(3\) NULL/);
console.log('PASS: M01-B through M07-B migration contracts are present.');
+2 -1
View File
@@ -32,7 +32,8 @@ assert.match(plan.file, /2026062217_m05c_third_party\.up\.sql/);
assert.match(plan.file, /2026062218_m05d_profit_sharing\.up\.sql/);
assert.match(plan.file, /2026062219_m06b_device_topology\.up\.sql/);
assert.match(plan.file, /2026062220_m06c_iot_messages\.up\.sql/);
assert.match(plan.file, /2026062421_m07a_wallet_ledger\.up\.sql$/);
assert.match(plan.file, /2026062421_m07a_wallet_ledger\.up\.sql/);
assert.match(plan.file, /2026062422_m07b_recharge_plans\.up\.sql$/);
assert.match(plan.checksum, /^[a-f0-9]{64}$/);
assert.ok(plan.statements.length >= 11);
+220
View File
@@ -0,0 +1,220 @@
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: [],
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('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("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 }, []];
}
throw new Error(`Unexpected SQL: ${sql}`);
}
};
const pool = {
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
};
}
};
return { service: new RechargeService(pool, wallet), 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);
}
console.log('PASS: M07-B recharge plans validate limits and credit paid callbacks idempotently.');