Files
Genarrative/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx
T
kdletters a597fcd076 收紧退款人工复核与重试契约
补齐退款事实对比字段与人工复核审计展示
人工复核重放严格校验管理员、原因和错误码
退款占用重试完整匹配管理员与归一化原因
统一退款原因持久化上限并修复刷新恢复
补充前后端回归测试、生成绑定与数据契约文档
2026-07-14 19:20:25 +08:00

766 lines
26 KiB
TypeScript

/* @vitest-environment jsdom */
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, expect, test, vi } from 'vitest';
import {
executeAdminRechargeRefund,
isAdminApiError,
listAdminRechargeOrders,
previewAdminRechargeRefund,
registerAdminRechargeRefund,
resolveAdminRechargeRefundManualReview,
} from '../api/adminApiClient';
import type {
AdminRechargeOrderEntryPayload,
AdminRechargeRefundActionResponse,
AdminRechargeRefundPayload,
AdminRechargeRefundPreviewResponse,
} from '../api/adminApiTypes';
import { AdminRechargeOrderPage } from './AdminRechargeOrderPage';
vi.mock('../api/adminApiClient', () => ({
executeAdminRechargeRefund: vi.fn(),
formatAdminApiError: vi.fn((error: unknown) =>
error instanceof Error ? error.message : '请求失败',
),
isAdminApiError: vi.fn(() => false),
listAdminRechargeOrders: vi.fn(),
previewAdminRechargeRefund: vi.fn(),
registerAdminRechargeRefund: vi.fn(),
resolveAdminRechargeRefundManualReview: vi.fn(),
}));
vi.mock('../components/AdminUserReferenceButton', () => ({
AdminUserReferenceButton: () => (
<button aria-label="查看用户信息" type="button" />
),
}));
vi.mock('../components/useAdminWriteConfirm', () => ({
useAdminWriteConfirm: () => ({
confirmDialog: null,
confirmWrite: vi.fn(() => Promise.resolve(true)),
}),
}));
const baseOrder: AdminRechargeOrderEntryPayload = {
orderId: 'order-1001',
userId: 'user-1',
user: {
userId: 'user-1',
publicUserCode: 'TN1001',
displayName: '测试用户',
avatarUrl: null,
},
productId: 'points_60',
productTitle: '60泥点',
productKind: 'points',
amountCents: 600,
status: 'paid',
paymentChannel: 'wechat_native',
paidAtMicros: 1_720_000_000_000_000,
providerTransactionId: 'wx-transaction-1',
createdAtMicros: 1_720_000_000_000_000,
pointsDelta: 60,
cumulativeSuccessRefundCents: 0,
targetRecoveryPoints: 0,
recoveredPoints: 0,
unrecoveredPoints: 0,
recoveryStatus: null,
wallet: {
userId: 'user-1',
totalBalance: 60,
spendableBalance: 60,
dailyFreePoints: 0,
membershipLimitedPoints: 0,
permanentPoints: 60,
heldPoints: 0,
refundDebtPoints: 0,
manualFrozen: false,
refundDebtFrozen: false,
walletFrozen: false,
manualRestriction: null,
},
refunds: [],
activeHold: null,
remainingRefundableCents: 600,
refundEligible: true,
refundBlockReasonCode: null,
};
const allowedPreview: AdminRechargeRefundPreviewResponse = {
order: baseOrder,
paymentCheck: {
verified: true,
tradeState: 'SUCCESS',
transactionId: 'wx-transaction-1',
amountTotalCents: 600,
knownRefundsRefreshed: 0,
},
refundAmountCents: 300,
incrementalRecoveryPoints: 30,
remainingRefundableCents: 600,
canSubmit: true,
blockReasonCode: null,
};
beforeEach(() => {
vi.clearAllMocks();
sessionStorage.clear();
vi.mocked(isAdminApiError).mockReturnValue(false);
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [baseOrder],
});
vi.mocked(previewAdminRechargeRefund).mockResolvedValue(allowedPreview);
vi.mocked(executeAdminRechargeRefund).mockResolvedValue(actionFor(baseOrder));
vi.mocked(registerAdminRechargeRefund).mockResolvedValue(
actionFor(baseOrder),
);
vi.mocked(resolveAdminRechargeRefundManualReview).mockResolvedValue(
actionFor(baseOrder),
);
});
test('充值订单查询传递全部筛选字段', async () => {
const user = userEvent.setup();
renderPage();
await screen.findByText('order-1001');
await user.type(screen.getByRole('textbox', { name: '订单号' }), 'order-x');
await user.type(
screen.getByRole('textbox', { name: '微信交易单号' }),
'wx-x',
);
await user.type(screen.getByRole('textbox', { name: '陶泥号' }), 'TNX');
await user.selectOptions(screen.getByLabelText('支付渠道'), 'wechat_native');
await user.selectOptions(screen.getByLabelText('订单状态'), 'paid');
fireEvent.change(screen.getByLabelText('创建时间起'), {
target: { value: '2026-07-01T08:30' },
});
fireEvent.change(screen.getByLabelText('创建时间止'), {
target: { value: '2026-07-13T18:45' },
});
await user.clear(screen.getByLabelText('条数'));
await user.type(screen.getByLabelText('条数'), '50');
await user.click(screen.getByRole('button', { name: '查询' }));
await waitFor(() => {
expect(listAdminRechargeOrders).toHaveBeenLastCalledWith(
'admin-token',
expect.objectContaining({
orderId: 'order-x',
providerTransactionId: 'wx-x',
userId: '',
publicUserCode: 'TNX',
paymentChannel: 'wechat_native',
status: 'paid',
createdAfter: new Date('2026-07-01T08:30').toISOString(),
createdBefore: new Date('2026-07-13T18:45').toISOString(),
limit: 50,
}),
);
});
});
test.each([
['wechat_mp', '微信小程序'],
['wechat_mp_virtual', '微信小程序虚拟支付'],
])('充值订单可按真实微信渠道 %s 查询', async (paymentChannel) => {
const user = userEvent.setup();
renderPage();
await screen.findByText('order-1001');
await user.selectOptions(screen.getByLabelText('支付渠道'), paymentChannel);
await user.click(screen.getByRole('button', { name: '查询' }));
await waitFor(() => {
expect(listAdminRechargeOrders).toHaveBeenLastCalledWith(
'admin-token',
expect.objectContaining({ paymentChannel }),
);
});
});
test('充值订单展示两种微信小程序渠道中文名称', async () => {
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [
{ ...baseOrder, orderId: 'order-mp', paymentChannel: 'wechat_mp' },
{
...baseOrder,
orderId: 'order-mp-virtual',
paymentChannel: 'wechat_mp_virtual',
},
],
});
renderPage();
const mpRow = (await screen.findByText('order-mp')).closest('tr');
const virtualRow = screen.getByText('order-mp-virtual').closest('tr');
expect(mpRow && within(mpRow).getByText('微信小程序')).toBeTruthy();
expect(
virtualRow && within(virtualRow).getByText('微信小程序虚拟支付'),
).toBeTruthy();
});
test('修改部分退款金额会作废已有预检', async () => {
const user = userEvent.setup();
renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), '退还剩余金额');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
expect(
await screen.findByRole('region', { name: '退款预检结果' }),
).toBeTruthy();
await user.clear(screen.getByLabelText('退款金额(元)'));
await user.type(screen.getByLabelText('退款金额(元)'), '2.00');
expect(screen.queryByRole('region', { name: '退款预检结果' })).toBeNull();
expect(
screen.getByRole('button', { name: '确认退款' }).hasAttribute('disabled'),
).toBe(true);
});
test('预检被阻止时不会调用退款执行接口', async () => {
const user = userEvent.setup();
vi.mocked(previewAdminRechargeRefund).mockResolvedValue({
...allowedPreview,
canSubmit: false,
blockReasonCode: 'insufficient_permanent_points',
});
renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
expect(
await screen.findByText('用户永久泥点不足,本次不会向微信发起退款'),
).toBeTruthy();
expect(
screen.getByRole('button', { name: '确认退款' }).hasAttribute('disabled'),
).toBe(true);
expect(executeAdminRechargeRefund).not.toHaveBeenCalled();
});
test('已登记的部分退款允许在 REFUND 支付状态下继续退剩余额度', async () => {
const user = userEvent.setup();
vi.mocked(previewAdminRechargeRefund).mockResolvedValue({
...allowedPreview,
paymentCheck: {
...allowedPreview.paymentCheck,
verified: true,
tradeState: 'REFUND',
knownRefundsRefreshed: 1,
},
remainingRefundableCents: 300,
});
renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), '退还剩余金额');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
expect(await screen.findByText('已核验 / REFUND')).toBeTruthy();
expect(
screen.getByRole('button', { name: '确认退款' }).hasAttribute('disabled'),
).toBe(false);
});
test('网络结果不明后当前弹窗直接复用稳定 requestId', async () => {
const user = userEvent.setup();
vi.mocked(executeAdminRechargeRefund)
.mockRejectedValueOnce(new Error('network disconnected'))
.mockResolvedValueOnce(actionFor(baseOrder));
renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), '用户申请');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
await user.click(screen.getByRole('button', { name: '确认退款' }));
expect(await screen.findByText(/微信侧退款状态未知/)).toBeTruthy();
expect(executeAdminRechargeRefund).toHaveBeenCalledTimes(1);
const firstRequestId = vi.mocked(executeAdminRechargeRefund).mock
.calls[0]?.[1].requestId;
expect(
screen
.getByRole('button', { name: '核验支付账单' })
.hasAttribute('disabled'),
).toBe(true);
await user.click(screen.getByRole('button', { name: '继续核对退款' }));
await waitFor(() =>
expect(executeAdminRechargeRefund).toHaveBeenCalledTimes(2),
);
expect(
vi.mocked(executeAdminRechargeRefund).mock.calls[1]?.[1].requestId,
).toBe(firstRequestId);
expect(previewAdminRechargeRefund).toHaveBeenCalledTimes(1);
});
test('刷新后根据 active hold 恢复原退款请求并复用 requestId', async () => {
const user = userEvent.setup();
vi.mocked(executeAdminRechargeRefund)
.mockRejectedValueOnce(new Error('network disconnected'))
.mockResolvedValueOnce(actionFor(baseOrder));
const firstRender = renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), '用户申请');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
await user.click(screen.getByRole('button', { name: '确认退款' }));
expect(await screen.findByText(/微信侧退款状态未知/)).toBeTruthy();
const firstRequestId = vi.mocked(executeAdminRechargeRefund).mock
.calls[0]?.[1].requestId;
firstRender.unmount();
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [orderWithActiveHold()],
});
renderPage();
await user.click(await screen.findByRole('button', { name: '核对退款' }));
expect(screen.getByLabelText('退款金额(元)')).toHaveProperty(
'value',
'3.00',
);
expect(screen.getByLabelText('退款原因')).toHaveProperty('value', '用户申请');
expect(
screen
.getByRole('button', { name: '核验支付账单' })
.hasAttribute('disabled'),
).toBe(true);
await user.click(screen.getByRole('button', { name: '继续核对退款' }));
await waitFor(() =>
expect(executeAdminRechargeRefund).toHaveBeenCalledTimes(2),
);
expect(vi.mocked(executeAdminRechargeRefund).mock.calls[1]?.[1]).toEqual(
expect.objectContaining({
orderId: 'order-1001',
refundAmountCents: 300,
reason: '用户申请',
requestId: firstRequestId,
}),
);
expect(previewAdminRechargeRefund).toHaveBeenCalledTimes(1);
});
test('超长退款原因按服务端上限持久化并可在刷新后恢复', async () => {
const user = userEvent.setup();
const normalizedReason = '退'.repeat(80);
vi.mocked(executeAdminRechargeRefund)
.mockRejectedValueOnce(new Error('network disconnected'))
.mockResolvedValueOnce(actionFor(baseOrder));
const firstRender = renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), `${normalizedReason}多`);
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
await user.click(screen.getByRole('button', { name: '确认退款' }));
expect(await screen.findByText(/微信侧退款状态未知/)).toBeTruthy();
const firstRequestId = vi.mocked(executeAdminRechargeRefund).mock
.calls[0]?.[1].requestId;
expect(vi.mocked(executeAdminRechargeRefund).mock.calls[0]?.[1].reason).toBe(
normalizedReason,
);
firstRender.unmount();
const activeHoldOrder = orderWithActiveHold();
activeHoldOrder.activeHold = {
...activeHoldOrder.activeHold!,
reason: normalizedReason,
};
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [activeHoldOrder],
});
renderPage();
await user.click(await screen.findByRole('button', { name: '核对退款' }));
expect(screen.getByRole('dialog', { name: '退款处理' })).toBeTruthy();
expect(screen.getByLabelText('退款原因')).toHaveProperty(
'value',
normalizedReason,
);
await user.click(screen.getByRole('button', { name: '继续核对退款' }));
await waitFor(() =>
expect(executeAdminRechargeRefund).toHaveBeenCalledTimes(2),
);
expect(vi.mocked(executeAdminRechargeRefund).mock.calls[1]?.[1]).toEqual(
expect.objectContaining({
reason: normalizedReason,
requestId: firstRequestId,
}),
);
});
test('过期退款请求上下文不会恢复 active hold,只开放安全查单', async () => {
const user = userEvent.setup();
vi.mocked(executeAdminRechargeRefund).mockRejectedValueOnce(
new Error('network disconnected'),
);
const firstRender = renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), '用户申请');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
await user.click(screen.getByRole('button', { name: '确认退款' }));
expect(await screen.findByText(/微信侧退款状态未知/)).toBeTruthy();
const storageKey = sessionStorage.key(0);
expect(storageKey).toBeTruthy();
const contexts = JSON.parse(
sessionStorage.getItem(storageKey ?? '') ?? '{}',
) as Record<string, { createdAtMillis: number }>;
const persistedContext = contexts['order-1001'];
expect(persistedContext).toBeTruthy();
if (!persistedContext) {
throw new Error('退款请求上下文未写入 sessionStorage');
}
persistedContext.createdAtMillis = Date.now() - 3 * 60 * 60 * 1_000;
sessionStorage.setItem(storageKey ?? '', JSON.stringify(contexts));
firstRender.unmount();
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [orderWithActiveHold()],
});
renderPage();
await user.click(await screen.findByRole('button', { name: '核对退款' }));
expect(screen.getByRole('dialog', { name: '登记商户平台退款' })).toBeTruthy();
expect(screen.queryByRole('dialog', { name: '退款处理' })).toBeNull();
});
test('切换管理员会话后不会复用上一会话的退款 requestId', async () => {
const user = userEvent.setup();
vi.mocked(executeAdminRechargeRefund).mockRejectedValueOnce(
new Error('network disconnected'),
);
const firstRender = renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), '用户申请');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
await user.click(screen.getByRole('button', { name: '确认退款' }));
expect(await screen.findByText(/微信侧退款状态未知/)).toBeTruthy();
firstRender.unmount();
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [orderWithActiveHold()],
});
renderPage('other-admin-token');
await user.click(await screen.findByRole('button', { name: '核对退款' }));
expect(screen.getByRole('dialog', { name: '登记商户平台退款' })).toBeTruthy();
expect(executeAdminRechargeRefund).toHaveBeenCalledTimes(1);
});
test('服务端明确拒绝退款后清理恢复上下文', async () => {
const user = userEvent.setup();
vi.mocked(isAdminApiError).mockReturnValue(true);
vi.mocked(executeAdminRechargeRefund).mockRejectedValueOnce(
new Error('退款前置条件不满足'),
);
const firstRender = renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), '用户申请');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
await user.click(screen.getByRole('button', { name: '确认退款' }));
expect(await screen.findByText('退款前置条件不满足')).toBeTruthy();
firstRender.unmount();
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [orderWithActiveHold()],
});
renderPage();
await user.click(await screen.findByRole('button', { name: '核对退款' }));
expect(screen.getByRole('dialog', { name: '登记商户平台退款' })).toBeTruthy();
});
test('active hold 缺少原 requestId 时只开放预填退款单号的查单登记', async () => {
const user = userEvent.setup();
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [orderWithActiveHold()],
});
renderPage();
await user.click(await screen.findByRole('button', { name: '核对退款' }));
const dialog = screen.getByRole('dialog', { name: '登记商户平台退款' });
expect(
within(dialog).getByRole('textbox', {
name: '商户退款单号 out_refund_no',
}),
).toHaveProperty('value', 'refund-1001');
expect(screen.queryByRole('dialog', { name: '退款处理' })).toBeNull();
expect(executeAdminRechargeRefund).not.toHaveBeenCalled();
await user.click(within(dialog).getByRole('button', { name: '查询并登记' }));
await waitFor(() => {
expect(registerAdminRechargeRefund).toHaveBeenCalledWith('admin-token', {
outRefundNo: 'refund-1001',
});
});
});
test.each(['provider_transaction_id_mismatch', 'order_total_mismatch'])(
'允许处理可确认的退款人工复核错误 %s',
async (lastErrorCode) => {
const user = userEvent.setup();
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [
{
...baseOrder,
refunds: [
manualReviewRefund({
lastErrorCode,
providerTransactionId:
lastErrorCode === 'provider_transaction_id_mismatch'
? 'wx-transaction-other'
: 'wx-transaction-1',
totalCents: lastErrorCode === 'order_total_mismatch' ? 700 : 600,
}),
],
},
],
});
renderPage();
await user.click(await screen.findByRole('button', { name: '人工复核' }));
const dialog = screen.getByRole('dialog', { name: '确认退款人工复核' });
const submitButton = within(dialog).getByRole('button', {
name: '确认并追回',
});
expect(
within(dialog).getByText(
lastErrorCode === 'provider_transaction_id_mismatch'
? '微信交易单号不一致'
: '订单总额不一致',
),
).toBeTruthy();
expect(
within(dialog).getByText('本地微信交易单号').parentElement?.textContent,
).toContain('wx-transaction-1');
expect(
within(dialog).getByText('退款微信交易单号').parentElement?.textContent,
).toContain(
lastErrorCode === 'provider_transaction_id_mismatch'
? 'wx-transaction-other'
: 'wx-transaction-1',
);
expect(
within(dialog).getByText('本地订单总额').parentElement?.textContent,
).toContain('¥6.00');
expect(
within(dialog).getByText('退款订单总额').parentElement?.textContent,
).toContain(lastErrorCode === 'order_total_mismatch' ? '¥7.00' : '¥6.00');
expect(submitButton.hasAttribute('disabled')).toBe(true);
await user.type(
within(dialog).getByRole('textbox', { name: '人工复核原因' }),
'已核对微信商户平台原始账单',
);
await user.click(submitButton);
await waitFor(() => {
expect(resolveAdminRechargeRefundManualReview).toHaveBeenCalledWith(
'admin-token',
{
outRefundNo: 'refund-manual-1',
reason: '已核对微信商户平台原始账单',
expectedErrorCode: lastErrorCode,
},
);
});
},
);
test('非白名单、已处理和会员退款不显示人工复核按钮', async () => {
vi.mocked(listAdminRechargeOrders).mockResolvedValue({
entries: [
{
...baseOrder,
orderId: 'order-non-whitelist',
refunds: [manualReviewRefund({ lastErrorCode: 'other_error' })],
},
{
...baseOrder,
orderId: 'order-resolved',
refunds: [
manualReviewRefund({
manualReviewResolvedAtMicros: 1_720_000,
manualReviewResolvedByAdminUserId: 'admin-1',
manualReviewResolutionReason: '已核对商户平台',
manualReviewResolvedErrorCode: 'provider_transaction_id_mismatch',
}),
],
},
{
...baseOrder,
orderId: 'order-membership',
productKind: 'membership',
refunds: [manualReviewRefund()],
},
],
});
renderPage();
await screen.findByText('order-membership');
expect(screen.queryByRole('button', { name: '人工复核' })).toBeNull();
expect(screen.getByText(/复核 微信交易单号不一致/).textContent).toContain(
'admin-1',
);
expect(screen.getByText(/已核对商户平台/)).toBeTruthy();
});
test('支付侧已退款但泥点不足时持续展示异常欠账与消费限制', async () => {
const user = userEvent.setup();
const debtOrder = {
...baseOrder,
unrecoveredPoints: 25,
wallet: {
...baseOrder.wallet,
spendableBalance: 0,
permanentPoints: 0,
refundDebtPoints: 25,
refundDebtFrozen: true,
walletFrozen: true,
},
};
vi.mocked(executeAdminRechargeRefund).mockResolvedValue(actionFor(debtOrder));
renderPage();
await openPartialRefund(user);
await user.type(screen.getByLabelText('退款金额(元)'), '3.00');
await user.type(screen.getByLabelText('退款原因'), '外部退款追缴');
await user.click(screen.getByRole('button', { name: '核验支付账单' }));
await user.click(screen.getByRole('button', { name: '确认退款' }));
const banner = await screen.findByText('退款异常欠账 25 泥点');
expect(banner).toBeTruthy();
expect(
screen.getByText(
'支付侧退款已记录,当前泥点不足,已限制继续消费并进入人工处理。',
),
).toBeTruthy();
});
test('商户平台退款登记只提交 outRefundNo', async () => {
const user = userEvent.setup();
renderPage();
await screen.findByText('order-1001');
await user.click(screen.getByRole('button', { name: '登记商户退款' }));
const dialog = screen.getByRole('dialog', { name: '登记商户平台退款' });
await user.type(
within(dialog).getByRole('textbox', { name: '商户退款单号 out_refund_no' }),
'refund-1001',
);
await user.click(within(dialog).getByRole('button', { name: '查询并登记' }));
await waitFor(() => {
expect(registerAdminRechargeRefund).toHaveBeenCalledWith('admin-token', {
outRefundNo: 'refund-1001',
});
});
});
test('疑似微信 refund_id 的编号交由服务端按真实查单结果判断', async () => {
const user = userEvent.setup();
renderPage();
await screen.findByText('order-1001');
await user.click(screen.getByRole('button', { name: '登记商户退款' }));
const dialog = screen.getByRole('dialog', { name: '登记商户平台退款' });
await user.type(
within(dialog).getByRole('textbox', { name: '商户退款单号 out_refund_no' }),
'50000000000000000000000000000',
);
await user.click(within(dialog).getByRole('button', { name: '查询并登记' }));
await waitFor(() => {
expect(registerAdminRechargeRefund).toHaveBeenCalledWith('admin-token', {
outRefundNo: '50000000000000000000000000000',
});
});
});
function renderPage(token = 'admin-token') {
return render(
<AdminRechargeOrderPage token={token} onUnauthorized={vi.fn()} />,
);
}
async function openPartialRefund(user: ReturnType<typeof userEvent.setup>) {
await screen.findByText('order-1001');
await user.click(screen.getByRole('button', { name: '退款' }));
await user.click(screen.getByRole('button', { name: '部分退款' }));
}
function actionFor(
order: AdminRechargeOrderEntryPayload,
): AdminRechargeRefundActionResponse {
return {
outRefundNo: 'refund-1001',
providerStatus: 'SUCCESS',
resultCode: 'settled',
providerStatusUnknown: false,
order,
};
}
function orderWithActiveHold(): AdminRechargeOrderEntryPayload {
return {
...baseOrder,
activeHold: {
outRefundNo: 'refund-1001',
refundCents: 300,
heldPoints: 30,
status: 'pending',
adminUserId: 'admin-1',
reason: '用户申请',
createdAtMicros: 1_720_000_000_000_000,
updatedAtMicros: 1_720_000_000_000_000,
},
refundEligible: false,
refundBlockReasonCode: 'refund_in_progress',
};
}
function manualReviewRefund(
overrides: Partial<AdminRechargeRefundPayload> = {},
): AdminRechargeRefundPayload {
return {
outRefundNo: 'refund-manual-1',
providerRefundId: 'provider-refund-1',
providerTransactionId: 'wx-transaction-other',
providerStatus: 'SUCCESS',
totalCents: 600,
refundCents: 300,
payerRefundCents: 300,
successAtMicros: 1_720_000_000_000_000,
firstObservedAtMicros: 1_720_000_000_000_000,
updatedAtMicros: 1_720_000_000_000_000,
observationSource: 'wechat_query',
targetRecoveryPoints: 30,
recoveredPoints: 0,
unrecoveredPoints: 0,
recoveryStatus: 'manual_review',
lastErrorCode: 'provider_transaction_id_mismatch',
manualReviewResolvedByAdminUserId: null,
manualReviewResolutionReason: null,
manualReviewResolvedAtMicros: null,
manualReviewResolvedErrorCode: null,
...overrides,
};
}