forked from wangziqi/gongxue-base
feat: query provider refunds
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user