mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-01 15:02:58 +08:00
fix(admin): 订单金额币种符号读取 currency 字段
This commit is contained in:
@@ -2,6 +2,7 @@ package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
@@ -115,24 +116,105 @@ func (h *PaymentHandler) RetryFulfillment(c *gin.Context) {
|
||||
response.Success(c, gin.H{"message": "fulfillment retried"})
|
||||
}
|
||||
|
||||
func sanitizeAdminPaymentOrdersForResponse(orders []*dbent.PaymentOrder) []*dbent.PaymentOrder {
|
||||
if len(orders) == 0 {
|
||||
return orders
|
||||
}
|
||||
out := make([]*dbent.PaymentOrder, 0, len(orders))
|
||||
type AdminPaymentOrderResult struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
UserEmail string `json:"user_email,omitempty"`
|
||||
UserName string `json:"user_name,omitempty"`
|
||||
UserNotes *string `json:"user_notes,omitempty"`
|
||||
Amount float64 `json:"amount"`
|
||||
PayAmount float64 `json:"pay_amount"`
|
||||
FeeRate float64 `json:"fee_rate"`
|
||||
Currency string `json:"currency"`
|
||||
RechargeCode string `json:"recharge_code,omitempty"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
PaymentTradeNo string `json:"payment_trade_no,omitempty"`
|
||||
PayURL *string `json:"pay_url,omitempty"`
|
||||
QRCode *string `json:"qr_code,omitempty"`
|
||||
QRCodeImg *string `json:"qr_code_img,omitempty"`
|
||||
OrderType string `json:"order_type"`
|
||||
PlanID *int64 `json:"plan_id,omitempty"`
|
||||
SubscriptionGroupID *int64 `json:"subscription_group_id,omitempty"`
|
||||
SubscriptionDays *int `json:"subscription_days,omitempty"`
|
||||
ProviderInstanceID *string `json:"provider_instance_id,omitempty"`
|
||||
ProviderKey *string `json:"provider_key,omitempty"`
|
||||
Status string `json:"status"`
|
||||
RefundAmount float64 `json:"refund_amount"`
|
||||
RefundReason *string `json:"refund_reason,omitempty"`
|
||||
RefundAt *time.Time `json:"refund_at,omitempty"`
|
||||
ForceRefund bool `json:"force_refund,omitempty"`
|
||||
RefundRequestedAt *time.Time `json:"refund_requested_at,omitempty"`
|
||||
RefundRequestReason *string `json:"refund_request_reason,omitempty"`
|
||||
RefundRequestedBy *string `json:"refund_requested_by,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
PaidAt *time.Time `json:"paid_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
FailedAt *time.Time `json:"failed_at,omitempty"`
|
||||
FailedReason *string `json:"failed_reason,omitempty"`
|
||||
ClientIP string `json:"client_ip,omitempty"`
|
||||
SrcHost string `json:"src_host,omitempty"`
|
||||
SrcURL *string `json:"src_url,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func sanitizeAdminPaymentOrdersForResponse(orders []*dbent.PaymentOrder) []*AdminPaymentOrderResult {
|
||||
out := make([]*AdminPaymentOrderResult, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
out = append(out, sanitizeAdminPaymentOrderForResponse(order))
|
||||
if item := sanitizeAdminPaymentOrderForResponse(order); item != nil {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sanitizeAdminPaymentOrderForResponse(order *dbent.PaymentOrder) *dbent.PaymentOrder {
|
||||
func sanitizeAdminPaymentOrderForResponse(order *dbent.PaymentOrder) *AdminPaymentOrderResult {
|
||||
if order == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *order
|
||||
cloned.ProviderSnapshot = nil
|
||||
return &cloned
|
||||
return &AdminPaymentOrderResult{
|
||||
ID: order.ID,
|
||||
UserID: order.UserID,
|
||||
UserEmail: order.UserEmail,
|
||||
UserName: order.UserName,
|
||||
UserNotes: order.UserNotes,
|
||||
Amount: order.Amount,
|
||||
PayAmount: order.PayAmount,
|
||||
FeeRate: order.FeeRate,
|
||||
Currency: service.PaymentOrderCurrency(order),
|
||||
RechargeCode: order.RechargeCode,
|
||||
OutTradeNo: order.OutTradeNo,
|
||||
PaymentType: order.PaymentType,
|
||||
PaymentTradeNo: order.PaymentTradeNo,
|
||||
PayURL: order.PayURL,
|
||||
QRCode: order.QrCode,
|
||||
QRCodeImg: order.QrCodeImg,
|
||||
OrderType: order.OrderType,
|
||||
PlanID: order.PlanID,
|
||||
SubscriptionGroupID: order.SubscriptionGroupID,
|
||||
SubscriptionDays: order.SubscriptionDays,
|
||||
ProviderInstanceID: order.ProviderInstanceID,
|
||||
ProviderKey: order.ProviderKey,
|
||||
Status: order.Status,
|
||||
RefundAmount: order.RefundAmount,
|
||||
RefundReason: order.RefundReason,
|
||||
RefundAt: order.RefundAt,
|
||||
ForceRefund: order.ForceRefund,
|
||||
RefundRequestedAt: order.RefundRequestedAt,
|
||||
RefundRequestReason: order.RefundRequestReason,
|
||||
RefundRequestedBy: order.RefundRequestedBy,
|
||||
ExpiresAt: order.ExpiresAt,
|
||||
PaidAt: order.PaidAt,
|
||||
CompletedAt: order.CompletedAt,
|
||||
FailedAt: order.FailedAt,
|
||||
FailedReason: order.FailedReason,
|
||||
ClientIP: order.ClientIP,
|
||||
SrcHost: order.SrcHost,
|
||||
SrcURL: order.SrcURL,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// AdminProcessRefundRequest is the request body for admin refund processing.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
)
|
||||
|
||||
func TestSanitizeAdminPaymentOrderForResponseAddsCurrency(t *testing.T) {
|
||||
now := time.Now()
|
||||
order := &dbent.PaymentOrder{
|
||||
ID: 1,
|
||||
UserID: 2,
|
||||
Amount: 100,
|
||||
PayAmount: 108,
|
||||
FeeRate: 8,
|
||||
OutTradeNo: "sub2_202606250001",
|
||||
PaymentType: "stripe",
|
||||
OrderType: "subscription",
|
||||
Status: "COMPLETED",
|
||||
ExpiresAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ProviderSnapshot: map[string]any{
|
||||
"schema_version": 2,
|
||||
"currency": "USD",
|
||||
},
|
||||
}
|
||||
|
||||
got := sanitizeAdminPaymentOrderForResponse(order)
|
||||
if got == nil {
|
||||
t.Fatal("expected sanitized order")
|
||||
}
|
||||
if got.Currency != "USD" {
|
||||
t.Fatalf("expected currency USD, got %q", got.Currency)
|
||||
}
|
||||
|
||||
body, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal sanitized order: %v", err)
|
||||
}
|
||||
if strings.Contains(string(body), "provider_snapshot") {
|
||||
t.Fatalf("expected provider_snapshot to be omitted, got %s", string(body))
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,19 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.baseAmount') }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ baseAmount.toFixed(2) }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ baseAmount.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div v-if="order.fee_rate > 0">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.fee') }} ({{ order.fee_rate }}%)</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ feeAmount.toFixed(2) }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ feeAmount.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ order.pay_amount.toFixed(2) }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ order.pay_amount.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div v-if="order.amount !== order.pay_amount">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.creditedAmount') }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.amount.toFixed(2) }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ order.amount.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.paymentMethod') }}</p>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundAmount') }}:</span>
|
||||
<span class="ml-1 font-medium text-red-700 dark:text-red-300">{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.refund_amount.toFixed(2) }}</span>
|
||||
<span class="ml-1 font-medium text-red-700 dark:text-red-300">{{ creditedAmountSymbol }}{{ order.refund_amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div v-if="order.refund_reason" class="col-span-2">
|
||||
<span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundReason') }}:</span>
|
||||
@@ -119,6 +119,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import { statusBadgeClass, canRefund as canRefundStatus, formatOrderDateTime } from '@/components/payment/orderUtils'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -127,6 +128,10 @@ const props = defineProps<{
|
||||
order: PaymentOrder | null
|
||||
}>()
|
||||
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
const paymentAmountSymbol = computed(() => currencySymbol(props.order?.currency))
|
||||
|
||||
/** 充值金额 (base amount before fee) = pay_amount - fee = pay_amount / (1 + fee_rate/100) */
|
||||
const baseAmount = computed(() => {
|
||||
if (!props.order) return 0
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
|
||||
<template #cell-pay_amount="{ value, row }">
|
||||
<div class="text-sm">
|
||||
<span class="font-medium text-gray-900 dark:text-white">¥{{ value.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol(row) }}{{ value.toFixed(2) }}</span>
|
||||
<span v-if="row.fee_rate > 0" class="ml-1 text-xs text-gray-400" :title="t('payment.orders.fee') + ': ' + row.fee_rate + '%'">
|
||||
({{ row.fee_rate }}%)
|
||||
</span>
|
||||
<div v-if="row.amount !== row.pay_amount" class="text-xs text-gray-500">
|
||||
{{ t('payment.orders.creditedAmount') }}: {{ row.order_type === 'balance' ? '$' : '¥' }}{{ row.amount.toFixed(2) }}
|
||||
{{ t('payment.orders.creditedAmount') }}: {{ creditedAmountSymbol }}{{ row.amount.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -143,6 +143,7 @@ import Pagination from '@/components/common/Pagination.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { statusBadgeClass, canRefund, formatOrderDateTime } from '@/components/payment/orderUtils'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -167,6 +168,11 @@ const emit = defineEmits<{
|
||||
|
||||
const searchQuery = ref('')
|
||||
const filters = reactive({ status: '', payment_type: '', order_type: '' })
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
function paymentAmountSymbol(order: PaymentOrder): string {
|
||||
return currencySymbol(order.currency)
|
||||
}
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function handleSearch() {
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.creditedAmount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ order?.amount?.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ order?.amount?.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">¥{{ order?.pay_amount?.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ order?.pay_amount?.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div v-if="actuallyRefunded > 0" class="mt-1 flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.admin.alreadyRefunded') }}</span>
|
||||
<span class="font-medium text-red-600 dark:text-red-400">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ actuallyRefunded.toFixed(2) }}</span>
|
||||
<span class="font-medium text-red-600 dark:text-red-400">{{ creditedAmountSymbol }}{{ actuallyRefunded.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,11 +66,11 @@
|
||||
<div v-if="form.deduct_balance && userBalance != null" class="mt-3 grid grid-cols-2 gap-3">
|
||||
<div class="rounded-lg bg-gray-50 p-3 text-sm dark:bg-dark-700">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ t('payment.admin.userBalance') }}</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-white">${{ userBalance.toFixed(2) }}</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ userBalance.toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-gray-50 p-3 text-sm dark:bg-dark-700">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ t('payment.admin.orderAmount') }}</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-white">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ order?.amount?.toFixed(2) }}</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ order?.amount?.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.refundAmount') }}</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">{{ order?.order_type === 'balance' ? '$' : '¥' }}</span>
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">{{ creditedAmountSymbol }}</span>
|
||||
<input
|
||||
v-model.number="form.amount"
|
||||
type="number"
|
||||
@@ -107,7 +107,7 @@
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('payment.admin.maxRefundable') }}: {{ order?.order_type === 'balance' ? '$' : '¥' }}{{ maxRefundable.toFixed(2) }}
|
||||
{{ t('payment.admin.maxRefundable') }}: {{ creditedAmountSymbol }}{{ maxRefundable.toFixed(2) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -169,6 +169,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import { formatOrderDateTime } from '@/components/payment/orderUtils'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -186,6 +187,10 @@ const emit = defineEmits<{
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
const paymentAmountSymbol = computed(() => currencySymbol(props.order?.currency))
|
||||
|
||||
const form = reactive({
|
||||
amount: 0,
|
||||
reason: '',
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import AdminOrderDetail from '../AdminOrderDetail.vue'
|
||||
import AdminOrderTable from '../AdminOrderTable.vue'
|
||||
import AdminRefundDialog from '../AdminRefundDialog.vue'
|
||||
import OrderTable from '@/components/payment/OrderTable.vue'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const BaseDialogStub = {
|
||||
props: ['show'],
|
||||
template: '<div v-if="show"><slot /><slot name="footer" /></div>',
|
||||
}
|
||||
|
||||
const DataTableStub = {
|
||||
props: ['data'],
|
||||
template: `
|
||||
<div>
|
||||
<div v-for="row in data" :key="row.id">
|
||||
<slot name="cell-pay_amount" :value="row.pay_amount" :row="row" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
||||
function orderFactory(overrides: Partial<PaymentOrder> = {}): PaymentOrder {
|
||||
return {
|
||||
id: 1,
|
||||
user_id: 10,
|
||||
amount: 100,
|
||||
pay_amount: 108,
|
||||
currency: 'USD',
|
||||
fee_rate: 8,
|
||||
payment_type: 'stripe',
|
||||
out_trade_no: 'sub2_202606250001',
|
||||
status: 'COMPLETED',
|
||||
order_type: 'subscription',
|
||||
created_at: '2026-06-25T10:00:00Z',
|
||||
expires_at: '2026-06-25T10:30:00Z',
|
||||
refund_amount: 25,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('admin order currency display', () => {
|
||||
it('uses order currency for paid/base/fee amounts and USD for credited/refund amounts', () => {
|
||||
const wrapper = mount(AdminOrderDetail, {
|
||||
props: {
|
||||
show: true,
|
||||
order: orderFactory({ currency: 'CNY' }),
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: BaseDialogStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('¥100.00')
|
||||
expect(text).toContain('¥8.00')
|
||||
expect(text).toContain('¥108.00')
|
||||
expect(text).toContain('$100.00')
|
||||
expect(text).toContain('$25.00')
|
||||
})
|
||||
|
||||
it('uses order currency for pay_amount and USD for refundable balance amounts', () => {
|
||||
const wrapper = mount(AdminRefundDialog, {
|
||||
props: {
|
||||
show: true,
|
||||
order: orderFactory({
|
||||
currency: 'USD',
|
||||
status: 'PARTIALLY_REFUNDED',
|
||||
refund_amount: 20,
|
||||
}),
|
||||
userBalance: 200,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: BaseDialogStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('$108.00')
|
||||
expect(text).toContain('$100.00')
|
||||
expect(text).toContain('$20.00')
|
||||
expect(text).toContain('$80.00')
|
||||
expect(text).toContain('$200.00')
|
||||
})
|
||||
|
||||
it('renders payment currency consistently in the shared order table', () => {
|
||||
const wrapper = mount(OrderTable, {
|
||||
props: {
|
||||
orders: [
|
||||
orderFactory({ id: 1, currency: 'USD', amount: 100, pay_amount: 108 }),
|
||||
orderFactory({ id: 2, currency: 'CNY', amount: 100, pay_amount: 108 }),
|
||||
],
|
||||
loading: false,
|
||||
showUser: true,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
DataTable: DataTableStub,
|
||||
OrderStatusBadge: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('$108.00')
|
||||
expect(text).toContain('¥108.00')
|
||||
expect(text).toContain('$100.00')
|
||||
})
|
||||
|
||||
it('renders payment currency consistently in the admin order table', () => {
|
||||
const wrapper = mount(AdminOrderTable, {
|
||||
props: {
|
||||
orders: [
|
||||
orderFactory({ id: 1, currency: 'USD', amount: 100, pay_amount: 108 }),
|
||||
orderFactory({ id: 2, currency: 'CNY', amount: 100, pay_amount: 108 }),
|
||||
],
|
||||
loading: false,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 2,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
DataTable: DataTableStub,
|
||||
Icon: true,
|
||||
Pagination: true,
|
||||
Select: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('$108.00')
|
||||
expect(text).toContain('¥108.00')
|
||||
expect(text).toContain('$100.00')
|
||||
})
|
||||
})
|
||||
@@ -14,12 +14,12 @@
|
||||
</template>
|
||||
<template #cell-pay_amount="{ value, row }">
|
||||
<div class="text-sm">
|
||||
<span class="font-medium text-gray-900 dark:text-white">¥{{ value.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol(row) }}{{ value.toFixed(2) }}</span>
|
||||
<span v-if="row.fee_rate > 0" class="ml-1 text-xs text-gray-400" :title="t('payment.orders.fee') + ': ' + row.fee_rate + '%'">
|
||||
({{ t('payment.orders.fee') }} {{ row.fee_rate }}%)
|
||||
</span>
|
||||
<div v-if="row.amount !== row.pay_amount" class="text-xs text-gray-500">
|
||||
{{ t('payment.orders.creditedAmount') }}: {{ row.order_type === 'balance' ? '$' : '¥' }}{{ row.amount.toFixed(2) }}
|
||||
{{ t('payment.orders.creditedAmount') }}: {{ creditedAmountSymbol }}{{ row.amount.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -45,6 +45,7 @@ import type { PaymentOrder } from '@/types/payment'
|
||||
import type { Column } from '@/components/common/types'
|
||||
import DataTable from '@/components/common/DataTable.vue'
|
||||
import OrderStatusBadge from '@/components/payment/OrderStatusBadge.vue'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -56,6 +57,12 @@ const props = defineProps<{
|
||||
|
||||
function formatDate(dateStr: string) { return new Date(dateStr).toLocaleString() }
|
||||
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
function paymentAmountSymbol(order: PaymentOrder): string {
|
||||
return currencySymbol(order.currency)
|
||||
}
|
||||
|
||||
const columns = computed((): Column[] => {
|
||||
const cols: Column[] = [
|
||||
{ key: 'id', label: t('payment.orders.orderId') },
|
||||
|
||||
@@ -45,11 +45,11 @@
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paidOrder.order_type === 'balance' ? '$' : '¥' }}{{ paidOrder.amount.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ paidOrder.amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">¥{{ paidOrder.pay_amount.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol(paidOrder) }}{{ paidOrder.pay_amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,6 +81,7 @@ import { paymentAPI } from '@/api/payment'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
import QRCode from 'qrcode'
|
||||
import alipayIcon from '@/assets/icons/alipay.svg'
|
||||
import wxpayIcon from '@/assets/icons/wxpay.svg'
|
||||
@@ -111,6 +112,7 @@ const expired = ref(false)
|
||||
const cancelling = ref(false)
|
||||
const success = ref(false)
|
||||
const paidOrder = ref<PaymentOrder | null>(null)
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||
@@ -137,6 +139,10 @@ const scanHint = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
function paymentAmountSymbol(order: PaymentOrder): string {
|
||||
return currencySymbol(order.currency)
|
||||
}
|
||||
|
||||
const countdownDisplay = computed(() => {
|
||||
const m = Math.floor(remainingSeconds.value / 60)
|
||||
const s = remainingSeconds.value % 60
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paidOrder.order_type === 'balance' ? '$' + paidOrder.amount.toFixed(2) : formatGatewayAmount(paidOrder.amount) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ paidOrder.amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ formatGatewayAmount(paidOrder.pay_amount) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ formatGatewayAmount(paidOrder.pay_amount, paidOrder.currency) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@ import { useAppStore } from '@/stores'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import { formatPaymentAmount, normalizePaymentCurrency } from '@/components/payment/currency'
|
||||
import { currencySymbol, formatPaymentAmount, normalizePaymentCurrency } from '@/components/payment/currency'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import QRCode from 'qrcode'
|
||||
@@ -161,6 +161,7 @@ const remainingSeconds = ref(0)
|
||||
const cancelling = ref(false)
|
||||
const paidOrder = ref<PaymentOrder | null>(null)
|
||||
const paymentCurrency = computed(() => normalizePaymentCurrency(props.currency))
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
const localeCode = computed(() => {
|
||||
const raw = i18n.locale as unknown
|
||||
if (typeof raw === 'string') return raw
|
||||
@@ -214,8 +215,8 @@ const countdownDisplay = computed(() => {
|
||||
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0')
|
||||
})
|
||||
|
||||
function formatGatewayAmount(value: number): string {
|
||||
return formatPaymentAmount(value, paymentCurrency.value, localeCode.value)
|
||||
function formatGatewayAmount(value: number, currency?: string | null): string {
|
||||
return formatPaymentAmount(value, currency || paymentCurrency.value, localeCode.value)
|
||||
}
|
||||
|
||||
function isSuccessStatus(status: string | null | undefined): boolean {
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
</div>
|
||||
<div v-if="amount > 0" class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ orderType === 'balance' ? '$' : '¥' }}{{ amount.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">¥{{ payAmount.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ payAmount.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="card overflow-hidden">
|
||||
<div class="bg-gradient-to-br from-[#635bff] to-[#4f46e5] px-6 py-5 text-center">
|
||||
<p class="text-sm font-medium text-indigo-200">{{ t('payment.actualPay') }}</p>
|
||||
<p class="mt-1 text-3xl font-bold text-white">¥{{ payAmount.toFixed(2) }}</p>
|
||||
<p class="mt-1 text-3xl font-bold text-white">{{ paymentAmountSymbol }}{{ payAmount.toFixed(2) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Stripe Payment Element -->
|
||||
@@ -64,13 +64,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { computed, ref, onMounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
import type { Stripe, StripeElements } from '@stripe/stripe-js'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
@@ -84,6 +85,7 @@ const props = defineProps<{
|
||||
orderType?: 'balance' | 'subscription'
|
||||
publishableKey: string
|
||||
payAmount: number
|
||||
currency?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ success: []; done: []; back: []; redirect: [orderId: number, payUrl: string] }>()
|
||||
@@ -101,6 +103,8 @@ const cancelling = ref(false)
|
||||
const success = ref(false)
|
||||
const ready = ref(false)
|
||||
const selectedType = ref('')
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
const paymentAmountSymbol = computed(() => currencySymbol(props.currency))
|
||||
|
||||
let stripeInstance: Stripe | null = null
|
||||
let elementsInstance: StripeElements | null = null
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import PaymentQRDialog from '../PaymentQRDialog.vue'
|
||||
|
||||
const pollOrderStatus = vi.hoisted(() => vi.fn())
|
||||
const cancelOrder = vi.hoisted(() => vi.fn())
|
||||
const verifyOrder = vi.hoisted(() => vi.fn())
|
||||
const showError = vi.hoisted(() => vi.fn())
|
||||
const toCanvas = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/stores/payment', () => ({
|
||||
usePaymentStore: () => ({
|
||||
pollOrderStatus,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores', () => ({
|
||||
useAppStore: () => ({
|
||||
showError,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/payment', () => ({
|
||||
paymentAPI: {
|
||||
cancelOrder,
|
||||
verifyOrder,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('qrcode', () => ({
|
||||
default: {
|
||||
toCanvas,
|
||||
},
|
||||
}))
|
||||
|
||||
const paidOrder = {
|
||||
id: 42,
|
||||
user_id: 9,
|
||||
amount: 100,
|
||||
pay_amount: 108,
|
||||
currency: 'CNY',
|
||||
fee_rate: 8,
|
||||
payment_type: 'alipay',
|
||||
out_trade_no: 'sub2_202606250001',
|
||||
status: 'COMPLETED',
|
||||
order_type: 'subscription',
|
||||
created_at: '2026-06-25T10:00:00Z',
|
||||
expires_at: '2099-01-01T10:30:00Z',
|
||||
refund_amount: 0,
|
||||
}
|
||||
|
||||
describe('PaymentQRDialog currency display', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
pollOrderStatus.mockReset().mockResolvedValue(paidOrder)
|
||||
cancelOrder.mockReset()
|
||||
verifyOrder.mockReset()
|
||||
showError.mockReset()
|
||||
toCanvas.mockReset().mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('uses order currency for pay_amount and USD for credited amount', async () => {
|
||||
const wrapper = mount(PaymentQRDialog, {
|
||||
props: {
|
||||
show: false,
|
||||
orderId: 42,
|
||||
qrCode: '',
|
||||
expiresAt: '2099-01-01T10:30:00Z',
|
||||
paymentType: 'alipay',
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: {
|
||||
props: ['show'],
|
||||
template: '<div v-if="show"><slot /><slot name="footer" /></div>',
|
||||
},
|
||||
Icon: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.setProps({ show: true })
|
||||
await flushPromises()
|
||||
await vi.advanceTimersByTimeAsync(3000)
|
||||
await flushPromises()
|
||||
|
||||
expect(pollOrderStatus).toHaveBeenCalledWith(42)
|
||||
expect(wrapper.text()).toContain('$100.00')
|
||||
expect(wrapper.text()).toContain('¥108.00')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatPaymentAmount } from '../currency'
|
||||
import { currencySymbol, formatPaymentAmount } from '../currency'
|
||||
|
||||
describe('formatPaymentAmount', () => {
|
||||
it('uses the currency default fraction digits', () => {
|
||||
@@ -8,3 +8,13 @@ describe('formatPaymentAmount', () => {
|
||||
expect(formatPaymentAmount(100, 'HKD', 'en-US')).toContain('.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('currencySymbol', () => {
|
||||
it('maps common payment currencies and falls back safely', () => {
|
||||
expect(currencySymbol('USD')).toBe('$')
|
||||
expect(currencySymbol('cny')).toBe('¥')
|
||||
expect(currencySymbol('EUR')).toBe('€')
|
||||
expect(currencySymbol('')).toBe('¥')
|
||||
expect(currencySymbol('XYZ')).toBe('XYZ')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
export const DEFAULT_PAYMENT_CURRENCY = 'CNY'
|
||||
|
||||
const PAYMENT_CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
USD: '$',
|
||||
CNY: '¥',
|
||||
RMB: '¥',
|
||||
EUR: '€',
|
||||
GBP: '£',
|
||||
JPY: '¥',
|
||||
HKD: 'HK$',
|
||||
TWD: 'NT$',
|
||||
KRW: '₩',
|
||||
AUD: 'A$',
|
||||
CAD: 'C$',
|
||||
SGD: 'S$',
|
||||
NZD: 'NZ$',
|
||||
MOP: 'MOP$',
|
||||
MYR: 'RM',
|
||||
THB: '฿',
|
||||
PHP: '₱',
|
||||
INR: '₹',
|
||||
}
|
||||
|
||||
export function normalizePaymentCurrency(currency?: string | null): string {
|
||||
const normalized = String(currency || '').trim().toUpperCase()
|
||||
return /^[A-Z]{3}$/.test(normalized) ? normalized : DEFAULT_PAYMENT_CURRENCY
|
||||
}
|
||||
|
||||
export function currencySymbol(currency?: string | null): string {
|
||||
const normalized = normalizePaymentCurrency(currency)
|
||||
return PAYMENT_CURRENCY_SYMBOLS[normalized] || normalized
|
||||
}
|
||||
|
||||
function paymentCurrencyFractionDigits(currency: string): number {
|
||||
try {
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
{{ t('payment.admin.retry') }}
|
||||
</button>
|
||||
<template v-if="row.status === 'REFUND_REQUESTED'">
|
||||
<span v-if="row.refund_amount" class="rounded-full bg-purple-100 px-1.5 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">{{ row.order_type === 'balance' ? '$' : '¥' }}{{ row.refund_amount.toFixed(2) }}</span>
|
||||
<span v-if="row.refund_amount" class="rounded-full bg-purple-100 px-1.5 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">{{ creditedAmountSymbol }}{{ row.refund_amount.toFixed(2) }}</span>
|
||||
<button @click="openRefundDialog(row)" class="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-purple-600 hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-purple-900/20">
|
||||
<Icon name="check" size="sm" />
|
||||
{{ t('payment.admin.approveRefund') }}
|
||||
@@ -62,14 +62,14 @@
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.orderId') }}</p><p class="font-mono text-sm font-medium text-gray-900 dark:text-white">#{{ selectedOrder.id }}</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.orderNo') }}</p><p class="text-sm font-medium text-gray-900 dark:text-white">{{ selectedOrder.out_trade_no }}</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.status') }}</p><OrderStatusBadge :status="selectedOrder.status" /></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</p><p class="text-sm font-medium text-gray-900 dark:text-white">{{ selectedOrder.order_type === 'balance' ? '$' : '¥' }}{{ selectedOrder.amount.toFixed(2) }}</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</p><p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ selectedOrder.pay_amount.toFixed(2) }}</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</p><p class="text-sm font-medium text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ selectedOrder.amount.toFixed(2) }}</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</p><p class="text-sm font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol(selectedOrder) }}{{ selectedOrder.pay_amount.toFixed(2) }}</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.paymentMethod') }}</p><p class="text-sm text-gray-700 dark:text-gray-300">{{ t('payment.methods.' + selectedOrder.payment_type, selectedOrder.payment_type) }}</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.feeRate') }}</p><p class="text-sm text-gray-700 dark:text-gray-300">{{ selectedOrder.fee_rate }}%</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.createdAt') }}</p><p class="text-sm text-gray-700 dark:text-gray-300">{{ formatDateTime(selectedOrder.created_at) }}</p></div>
|
||||
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.expiresAt') }}</p><p class="text-sm text-gray-700 dark:text-gray-300">{{ formatDateTime(selectedOrder.expires_at) }}</p></div>
|
||||
<div v-if="selectedOrder.paid_at"><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.paidAt') }}</p><p class="text-sm text-gray-700 dark:text-gray-300">{{ formatDateTime(selectedOrder.paid_at) }}</p></div>
|
||||
<div v-if="selectedOrder.refund_amount"><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.refundAmount') }}</p><p class="text-sm font-medium text-red-600 dark:text-red-400">{{ selectedOrder.order_type === 'balance' ? '$' : '¥' }}{{ selectedOrder.refund_amount.toFixed(2) }}</p></div>
|
||||
<div v-if="selectedOrder.refund_amount"><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.refundAmount') }}</p><p class="text-sm font-medium text-red-600 dark:text-red-400">{{ creditedAmountSymbol }}{{ selectedOrder.refund_amount.toFixed(2) }}</p></div>
|
||||
<div v-if="selectedOrder.refund_reason" class="col-span-2"><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.refundReason') }}</p><p class="text-sm text-gray-700 dark:text-gray-300">{{ selectedOrder.refund_reason }}</p></div>
|
||||
<!-- Refund request info -->
|
||||
<div v-if="selectedOrder.refund_requested_at" class="col-span-2 border-t border-gray-200 pt-3 dark:border-dark-600">
|
||||
@@ -127,6 +127,7 @@ import Icon from '@/components/icons/Icon.vue'
|
||||
import AdminRefundDialog from '@/components/admin/payment/AdminRefundDialog.vue'
|
||||
import OrderStatusBadge from '@/components/payment/OrderStatusBadge.vue'
|
||||
import OrderTable from '@/components/payment/OrderTable.vue'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
|
||||
interface AuditLog {
|
||||
id: number
|
||||
@@ -149,6 +150,11 @@ const showDetailDialog = ref(false)
|
||||
const showRefundDialog = ref(false)
|
||||
const refundSubmitting = ref(false)
|
||||
const orderAuditLogs = ref<AuditLog[]>([])
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
function paymentAmountSymbol(order: PaymentOrder | null | undefined): string {
|
||||
return currencySymbol(order?.currency)
|
||||
}
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function debounceLoadOrders() {
|
||||
|
||||
Reference in New Issue
Block a user