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.');