mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 14:58:23 +08:00
Merge pull request #6152 from ranxi2001/fix/payment-result-balance-refresh
fix(payment): refresh balance after fulfillment
This commit is contained in:
@@ -107,6 +107,7 @@ import {
|
||||
readPaymentRecoverySnapshot,
|
||||
} from '@/components/payment/paymentFlow'
|
||||
import { usePaymentStore } from '@/stores/payment'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import type { PublicOrderVerifyResult } from '@/api/payment'
|
||||
import type { OrderStatus, PaymentOrder } from '@/types/payment'
|
||||
@@ -118,6 +119,7 @@ const { t } = i18n
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const paymentStore = usePaymentStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
type ResolvedOrder = PaymentOrder | PublicOrderVerifyResult
|
||||
|
||||
@@ -139,6 +141,7 @@ const STATUS_REFRESH_INTERVAL_MS = 2000
|
||||
const STATUS_REFRESH_MAX_ATTEMPTS = 15
|
||||
|
||||
let statusRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let userBalanceRefreshStarted = false
|
||||
const refreshAttempts = ref(0)
|
||||
|
||||
/** 充值金额 = pay_amount / (1 + fee_rate/100),fee_rate=0 时等于 pay_amount */
|
||||
@@ -197,6 +200,21 @@ function setResolvedOrder(nextOrder: ResolvedOrder | null): void {
|
||||
if (nextOrder && 'currency' in nextOrder && nextOrder.currency) {
|
||||
currency.value = normalizePaymentCurrency(nextOrder.currency)
|
||||
}
|
||||
refreshUserBalanceForSuccessfulOrder(nextOrder)
|
||||
}
|
||||
|
||||
function refreshUserBalanceForSuccessfulOrder(nextOrder: ResolvedOrder | null): void {
|
||||
if (!nextOrder || userBalanceRefreshStarted || normalizeOrderStatus(nextOrder.status) !== 'COMPLETED') {
|
||||
return
|
||||
}
|
||||
if ('order_type' in nextOrder && nextOrder.order_type !== 'balance') {
|
||||
return
|
||||
}
|
||||
|
||||
userBalanceRefreshStarted = true
|
||||
void authStore.refreshUser().catch(() => {
|
||||
// The order result remains authoritative even if refreshing profile data fails.
|
||||
})
|
||||
}
|
||||
|
||||
function hasOrderId(nextOrder: ResolvedOrder | null): nextOrder is PaymentOrder {
|
||||
|
||||
@@ -10,6 +10,7 @@ const pollOrderStatus = vi.hoisted(() => vi.fn())
|
||||
const verifyOrder = vi.hoisted(() => vi.fn())
|
||||
const verifyOrderPublic = vi.hoisted(() => vi.fn())
|
||||
const resolveOrderPublicByResumeToken = vi.hoisted(() => vi.fn())
|
||||
const refreshUser = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('vue-router', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-router')>('vue-router')
|
||||
@@ -36,6 +37,12 @@ vi.mock('@/stores/payment', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/auth', () => ({
|
||||
useAuthStore: () => ({
|
||||
refreshUser,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/payment', () => ({
|
||||
paymentAPI: {
|
||||
verifyOrder,
|
||||
@@ -91,6 +98,8 @@ describe('PaymentResultView', () => {
|
||||
verifyOrder.mockReset()
|
||||
verifyOrderPublic.mockReset()
|
||||
resolveOrderPublicByResumeToken.mockReset()
|
||||
refreshUser.mockReset()
|
||||
refreshUser.mockResolvedValue({})
|
||||
window.localStorage.clear()
|
||||
})
|
||||
|
||||
@@ -171,7 +180,7 @@ describe('PaymentResultView', () => {
|
||||
}))
|
||||
resolveOrderPublicByResumeToken.mockResolvedValue({
|
||||
data: {
|
||||
...orderFactory('PAID'),
|
||||
...orderFactory('COMPLETED'),
|
||||
amount: 100,
|
||||
pay_amount: 103,
|
||||
fee_rate: 3,
|
||||
@@ -190,13 +199,14 @@ describe('PaymentResultView', () => {
|
||||
|
||||
expect(pollOrderStatus).not.toHaveBeenCalled()
|
||||
expect(resolveOrderPublicByResumeToken).toHaveBeenCalledWith('resume-authoritative')
|
||||
expect(refreshUser).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('payment.result.success')
|
||||
expect(wrapper.text()).toContain('103.00')
|
||||
expect(wrapper.text()).toContain('100.00')
|
||||
expect(window.localStorage.getItem(PAYMENT_RECOVERY_STORAGE_KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it('refreshes a pending resume-token result until the order becomes paid', async () => {
|
||||
it('waits for completed fulfillment before refreshing the user balance', async () => {
|
||||
vi.useFakeTimers()
|
||||
routeState.query = {
|
||||
resume_token: 'resume-77',
|
||||
@@ -210,7 +220,7 @@ describe('PaymentResultView', () => {
|
||||
data: orderFactory('PENDING'),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: orderFactory('PAID'),
|
||||
data: orderFactory('COMPLETED'),
|
||||
})
|
||||
|
||||
const wrapper = mount(PaymentResultView, {
|
||||
@@ -224,6 +234,7 @@ describe('PaymentResultView', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(resolveOrderPublicByResumeToken).toHaveBeenCalledTimes(1)
|
||||
expect(refreshUser).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('payment.result.processing')
|
||||
expect(window.localStorage.getItem(PAYMENT_RECOVERY_STORAGE_KEY)).not.toBeNull()
|
||||
|
||||
@@ -231,11 +242,36 @@ describe('PaymentResultView', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(resolveOrderPublicByResumeToken).toHaveBeenCalledTimes(2)
|
||||
expect(refreshUser).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('payment.result.success')
|
||||
expect(wrapper.text()).not.toContain('payment.result.failed')
|
||||
expect(window.localStorage.getItem(PAYMENT_RECOVERY_STORAGE_KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the successful result when refreshing the user balance fails', async () => {
|
||||
routeState.query = {
|
||||
resume_token: 'resume-refresh-failure',
|
||||
}
|
||||
resolveOrderPublicByResumeToken.mockResolvedValue({
|
||||
data: orderFactory('COMPLETED'),
|
||||
})
|
||||
refreshUser.mockRejectedValueOnce(new Error('profile refresh failed'))
|
||||
|
||||
const wrapper = mount(PaymentResultView, {
|
||||
global: {
|
||||
stubs: {
|
||||
OrderStatusBadge: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(refreshUser).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('payment.result.success')
|
||||
expect(wrapper.text()).not.toContain('payment.result.failed')
|
||||
})
|
||||
|
||||
it('falls back to order_id polling when resume-token recovery fails', async () => {
|
||||
routeState.query = {
|
||||
resume_token: 'resume-fail',
|
||||
|
||||
Reference in New Issue
Block a user