feat: query provider refunds

This commit is contained in:
Codex
2026-06-29 05:19:16 +08:00
parent 8d752881c1
commit e9cf363df9
11 changed files with 348 additions and 25 deletions

View File

@@ -338,6 +338,21 @@ async function startFakeWechatPayServer() {
return;
}
if (req.method === 'GET' && url.pathname.startsWith('/v3/refund/domestic/refunds/')) {
const outRefundNo = decodeURIComponent(url.pathname.split('/').pop() || '');
res.writeHead(200, { 'content-type': 'application/json' });
res.end(
JSON.stringify({
refund_id: `refund-${outRefundNo || 'unknown'}`,
out_refund_no: outRefundNo,
status: 'SUCCESS',
amount: { refund: 100, total: 500, currency: 'CNY' },
success_time: '2026-06-28T00:00:00+08:00',
}),
);
return;
}
if (url.pathname !== '/v3/pay/transactions/jsapi') {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ code: 'NOT_FOUND' }));
@@ -406,12 +421,29 @@ async function startFakeAlipayServer() {
pathname: req.url || '/',
params,
});
if (params.method !== 'alipay.trade.refund') {
if (!['alipay.trade.refund', 'alipay.trade.fastpay.refund.query'].includes(params.method)) {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error_response: { code: '404', msg: 'not found' } }));
return;
}
const biz = JSON.parse(params.biz_content || '{}');
if (params.method === 'alipay.trade.fastpay.refund.query') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(
JSON.stringify({
alipay_trade_fastpay_refund_query_response: {
code: '10000',
msg: 'Success',
trade_no: biz.trade_no || `ali-trade-${biz.out_trade_no || 'unknown'}`,
out_trade_no: biz.out_trade_no,
out_request_no: biz.out_request_no,
refund_amount: '5.00',
refund_status: 'REFUND_SUCCESS',
},
}),
);
return;
}
res.writeHead(200, { 'content-type': 'application/json' });
res.end(
JSON.stringify({
@@ -422,6 +454,7 @@ async function startFakeAlipayServer() {
out_trade_no: biz.out_trade_no,
out_request_no: biz.out_request_no,
refund_fee: biz.refund_amount,
fund_change: 'Y',
},
}),
);
@@ -1722,6 +1755,23 @@ async function testCommerce() {
assert.equal(wechatRefundRequest?.body?.out_refund_no, 'RF-WECHAT-PROVIDER-001', 'WeChat refund should use out_refund_no');
assert.equal(wechatRefundRequest?.body?.amount?.refund, 100, 'WeChat refund should send refund cents');
assert.equal(wechatRefundRequest?.body?.amount?.total, wechatOrder.item.amountCents, 'WeChat refund should send total cents');
const wechatQueriedRefund = await request('/api/commerce/refunds/status', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
refundId: wechatRefund.item.id,
action: 'query_provider_refund',
},
});
assert.equal(wechatQueriedRefund.item?.status, 'succeeded', 'WeChat refund query should confirm provider success');
assert.equal(wechatQueriedRefund.item?.providerRefundNo, 'refund-RF-WECHAT-PROVIDER-001', 'WeChat refund query should keep provider refund id');
const wechatRefundQueryRequest = fakeWechatPay.requests.find(item => item.method === 'GET' && item.pathname.endsWith('/RF-WECHAT-PROVIDER-001'));
assert.ok(wechatRefundQueryRequest, 'WeChat refund query should call query-by-out-refund-no endpoint');
const wechatPartiallyRefundedStatus = await request('/api/commerce/orders/status', {
query: { orderNo: wechatOrder.item.orderNo },
});
assert.equal(wechatPartiallyRefundedStatus.item?.status, 'partially_refunded', 'confirmed WeChat partial refund should update order status');
assert.equal(wechatPartiallyRefundedStatus.item?.refundedAmountCents, 100, 'confirmed WeChat partial refund should update refunded amount');
const fakeAlipay = await startFakeAlipayServer();
const alipayAccount = await request('/api/tenant-admin/payment-accounts', {
@@ -1827,6 +1877,20 @@ async function testCommerce() {
query: { orderNo: alipayOrder.item.orderNo },
});
assert.equal(alipayRefundedStatus.item?.status, 'refunded', 'synchronous Alipay refund should update order status');
const alipayRefundQueryRejected = await request('/api/commerce/refunds/status', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
refundId: alipayRefund.item.id,
action: 'query_provider_refund',
},
expectStatus: 409,
});
assert.equal(alipayRefundQueryRejected.code, 'REFUND_STATUS_INVALID', 'succeeded provider refund must not be queried and applied twice');
assert.ok(
!fakeAlipay.requests.some(item => item.params?.method === 'alipay.trade.fastpay.refund.query'),
'already succeeded refund query should be rejected before calling Alipay',
);
const tamperedAlipayNotify = await request('/api/commerce/payments/notify/alipay', {
userId: false,