feat: add custom easypay payment methods

This commit is contained in:
Albert Coady
2026-07-06 15:00:29 +08:00
parent 2854ab2ace
commit bf76168ba5
22 changed files with 779 additions and 22 deletions
@@ -20,9 +20,9 @@
@click="method.available && emit('select', method.type)"
>
<span class="flex items-center gap-2">
<img :src="methodIcon(method.type)" :alt="t(`payment.methods.${method.type}`)" class="h-7 w-7 object-contain" />
<img :src="methodIcon(method.type)" :alt="methodLabel(method)" class="h-7 w-7 object-contain" />
<span class="flex flex-col items-start leading-none">
<span class="text-base font-semibold">{{ t(`payment.methods.${method.type}`) }}</span>
<span class="text-base font-semibold">{{ methodLabel(method) }}</span>
<span
v-if="method.fee_rate > 0"
class="text-[10px] tracking-wide text-gray-500 dark:text-dark-400"
@@ -47,6 +47,7 @@ import airwallexIcon from '@/assets/icons/airwallex.svg'
export interface PaymentMethodOption {
type: string
display_name?: string
fee_rate: number
available: boolean
}
@@ -82,7 +83,11 @@ function methodIcon(type: string): string {
if (type.includes('alipay')) return METHOD_ICONS.alipay
if (type.includes('wxpay')) return METHOD_ICONS.wxpay
if (type === 'airwallex') return METHOD_ICONS.airwallex
return METHOD_ICONS[type] || alipayIcon
return METHOD_ICONS[type] || stripeIcon
}
function methodLabel(method: PaymentMethodOption): string {
return method.display_name || t(`payment.methods.${method.type}`, method.type)
}
function methodSelectedClass(type: string): string {
@@ -168,6 +168,49 @@
</div>
</div>
<div v-if="form.provider_key === 'easypay'" class="mt-4 space-y-3 rounded-lg border border-gray-100 p-3 dark:border-dark-700">
<div class="flex items-center justify-between gap-3">
<div>
<h5 class="text-sm font-medium text-gray-900 dark:text-white">
{{ t('admin.settings.payment.easypayCustomMethods') }}
</h5>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.payment.easypayCustomMethodsHint') }}
</p>
</div>
<button type="button" class="btn btn-secondary btn-sm" @click="addEasyPayCustomMethod">
{{ t('admin.settings.payment.addCustomMethod') }}
</button>
</div>
<div v-if="easyPayCustomMethods.length" class="space-y-2">
<div
v-for="(method, index) in easyPayCustomMethods"
:key="index"
class="grid grid-cols-[1fr_1fr_1fr_auto] items-end gap-2"
>
<div>
<label class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.settings.payment.customMethodType') }}</label>
<input v-model="method.type" type="text" class="input mt-0.5" placeholder="ldc" />
</div>
<div>
<label class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.settings.payment.customMethodUpstreamType') }}</label>
<input v-model="method.upstreamType" type="text" class="input mt-0.5" placeholder="epay" />
</div>
<div>
<label class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.settings.payment.customMethodDisplayName') }}</label>
<input v-model="method.displayName" type="text" class="input mt-0.5" placeholder="LDC" />
</div>
<button
type="button"
class="rounded-lg border border-red-200 px-2.5 py-2 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 dark:border-red-800/60 dark:text-red-300 dark:hover:bg-red-900/20"
@click="removeEasyPayCustomMethod(index)"
>
{{ t('common.delete') }}
</button>
</div>
</div>
</div>
<!-- Callback URLs (each = editable URL + fixed path) -->
<div v-if="callbackPaths" class="mt-4 space-y-3">
<div v-if="callbackPaths.notifyUrl">
@@ -270,7 +313,7 @@ import Select from '@/components/common/Select.vue'
import type { SelectOption } from '@/components/common/Select.vue'
import ToggleSwitch from './ToggleSwitch.vue'
import type { ProviderInstance } from '@/types/payment'
import type { TypeOption } from './providerConfig'
import type { EasyPayCustomMethod, TypeOption } from './providerConfig'
import {
PROVIDER_CONFIG_FIELDS,
PROVIDER_SUPPORTED_TYPES,
@@ -282,6 +325,8 @@ import {
STRIPE_SDK_API_VERSION,
getAvailableTypes,
extractBaseUrl,
parseEasyPayCustomMethods,
serializeEasyPayCustomMethods,
} from './providerConfig'
/** Default payment_mode per provider key — "" means "no preference, use
@@ -365,6 +410,7 @@ const notifyBaseUrl = ref('')
const returnBaseUrl = ref('')
const limitsExpanded = ref(false)
const visibleFields = reactive<Record<string, boolean>>({})
const easyPayCustomMethods = reactive<EasyPayCustomMethod[]>([])
// --- Computed ---
const defaultBaseUrl = typeof window !== 'undefined' ? window.location.origin : ''
@@ -404,6 +450,16 @@ const paymentModeOptions = computed(() => {
const availableTypes = computed(() => {
const base = getAvailableTypes(form.provider_key, props.allPaymentTypes, props.redirectLabel)
if (form.provider_key === 'easypay') {
for (const method of normalizedEasyPayCustomMethods()) {
if (!base.some(opt => opt.value === method.type)) {
base.push({
value: method.type,
label: method.displayName || method.type,
})
}
}
}
// Resolve i18n labels for types not in allPaymentTypes (e.g. card, link inside stripe)
return base.map(opt =>
opt.label === opt.value
@@ -510,6 +566,28 @@ function toggleType(type: string) {
}
}
function normalizedEasyPayCustomMethods(): EasyPayCustomMethod[] {
return easyPayCustomMethods
.map(method => ({
type: normalizeEasyPayCustomMethodCode(method.type),
upstreamType: normalizeEasyPayCustomMethodCode(method.upstreamType),
displayName: method.displayName.trim(),
}))
.filter(method => method.type || method.upstreamType || method.displayName)
}
function normalizeEasyPayCustomMethodCode(value: string): string {
return value.trim().toLowerCase()
}
function addEasyPayCustomMethod() {
easyPayCustomMethods.push({ type: '', upstreamType: '', displayName: '' })
}
function removeEasyPayCustomMethod(index: number) {
easyPayCustomMethods.splice(index, 1)
}
function onKeyChange() {
form.supported_types = [...(PROVIDER_SUPPORTED_TYPES[form.provider_key] || [])]
form.payment_mode = defaultPaymentMode(form.provider_key)
@@ -524,6 +602,7 @@ function clearConfig() {
notifyBaseUrl.value = ''
returnBaseUrl.value = ''
limitsExpanded.value = false
easyPayCustomMethods.splice(0, easyPayCustomMethods.length)
}
function applyDefaults() {
@@ -581,6 +660,14 @@ function handleSave() {
emitValidationError(t('admin.settings.payment.validationNameRequired'))
return
}
if (form.provider_key === 'easypay') {
const validationError = validateEasyPayCustomMethods()
if (validationError) {
emitValidationError(validationError)
return
}
syncEasyPayCustomMethods()
}
// Validate required config fields — all non-optional fields must be filled.
// In edit mode, sensitive fields may be left blank to preserve the stored
// value (backend merges blanks by preserving the existing secret).
@@ -610,6 +697,9 @@ function handleSave() {
}
filteredConfig[k] = v
}
if (form.provider_key === 'easypay') {
filteredConfig.customMethods = serializeEasyPayCustomMethods(normalizedEasyPayCustomMethods())
}
// Inject computed callback URLs (each URL = independent base + fixed path)
// If base URL is empty, auto-fill with current domain
@@ -636,6 +726,56 @@ function handleSave() {
})
}
function syncEasyPayCustomMethods(): string[] {
if (form.provider_key !== 'easypay') return []
const baseTypes = new Set(PROVIDER_SUPPORTED_TYPES.easypay || [])
const customTypes: string[] = []
const seen = new Set<string>()
for (const method of normalizedEasyPayCustomMethods()) {
if (!method.type || !method.upstreamType) continue
if (seen.has(method.type)) continue
seen.add(method.type)
customTypes.push(method.type)
}
form.supported_types = form.supported_types
.map(type => normalizeEasyPayCustomMethodCode(type))
.filter(type => baseTypes.has(type) || customTypes.includes(type))
for (const customType of customTypes) {
if (!form.supported_types.includes(customType)) {
form.supported_types.push(customType)
}
}
return customTypes
}
function validateEasyPayCustomMethods(): string | null {
const seen = new Set<string>()
for (const method of normalizedEasyPayCustomMethods()) {
const hasAnyValue = Boolean(method.type || method.upstreamType || method.displayName)
if (!hasAnyValue) continue
if (!method.type || !method.upstreamType) {
return t('admin.settings.payment.validationEasyPayCustomMethodRequired')
}
if (!/^[a-z0-9_-]+$/.test(method.type)) {
return t('admin.settings.payment.validationEasyPayCustomMethodTypeInvalid')
}
if (!/^[a-z0-9_-]+$/.test(method.upstreamType)) {
return t('admin.settings.payment.validationEasyPayCustomMethodUpstreamTypeInvalid')
}
if ((PROVIDER_SUPPORTED_TYPES.easypay || []).includes(method.type)) {
return t('admin.settings.payment.validationEasyPayCustomMethodReserved')
}
if (method.type.startsWith('alipay') || method.type.startsWith('wxpay')) {
return t('admin.settings.payment.validationEasyPayCustomMethodPrefixReserved')
}
if (seen.has(method.type)) {
return t('admin.settings.payment.validationEasyPayCustomMethodDuplicate')
}
seen.add(method.type)
}
return null
}
function emitValidationError(msg: string) {
// Use a custom event or inject appStore — for now use window alert fallback
// The parent handles this via the save event validation
@@ -677,6 +817,10 @@ function loadProvider(provider: ProviderInstance) {
for (const [k, v] of Object.entries(provider.config)) {
// Skip notifyUrl/returnUrl — they are derived from callbackBaseUrl
if (k === 'notifyUrl' || k === 'returnUrl') continue
if (k === 'customMethods' && provider.provider_key === 'easypay') {
easyPayCustomMethods.push(...parseEasyPayCustomMethods(v))
continue
}
config[k] = v
}
// Extract base URLs from existing callback URLs
@@ -0,0 +1,24 @@
import { describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import PaymentMethodSelector from '@/components/payment/PaymentMethodSelector.vue'
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string, fallback?: string) => fallback ?? key,
}),
}))
describe('PaymentMethodSelector', () => {
it('shows the configured display name for custom EasyPay methods', () => {
const wrapper = mount(PaymentMethodSelector, {
props: {
selected: 'ldc',
methods: [{ type: 'ldc', display_name: 'LDC Pay', fee_rate: 0, available: true }],
},
})
expect(wrapper.text()).toContain('LDC Pay')
expect(wrapper.text()).not.toContain('ldc')
expect(wrapper.text()).not.toContain('payment.methods.ldc')
})
})
@@ -7,6 +7,12 @@ import type { ProviderInstance } from '@/types/payment'
const messages: Record<string, string> = {
'admin.settings.payment.providerConfig': 'Credentials',
'admin.settings.payment.easypayCustomMethods': 'Custom EasyPay methods',
'admin.settings.payment.easypayCustomMethodsHint': 'Add provider-specific EasyPay type values.',
'admin.settings.payment.addCustomMethod': 'Add method',
'admin.settings.payment.customMethodType': 'Payment type',
'admin.settings.payment.customMethodUpstreamType': 'Upstream type',
'admin.settings.payment.customMethodDisplayName': 'Display name',
'admin.settings.payment.paymentGuideTrigger': 'View payment guide',
'admin.settings.payment.alipayGuideSummary': 'Desktop prefers QR precreate and falls back to cashier; mobile prefers WAP checkout.',
'admin.settings.payment.wxpayGuideSummary': 'Desktop prefers Native QR; mobile routes to JSAPI or H5 based on browser context.',
@@ -53,12 +59,14 @@ function mountDialog(options: { editing?: ProviderInstance | null } = {}) {
saving: false,
editing: options.editing ?? null,
allKeyOptions: [
{ value: 'easypay', label: 'EasyPay' },
{ value: 'alipay', label: 'Alipay' },
{ value: 'wxpay', label: 'WeChat Pay' },
{ value: 'stripe', label: 'Stripe' },
{ value: 'airwallex', label: 'Airwallex' },
],
enabledKeyOptions: [
{ value: 'easypay', label: 'EasyPay' },
{ value: 'alipay', label: 'Alipay' },
{ value: 'wxpay', label: 'WeChat Pay' },
{ value: 'airwallex', label: 'Airwallex' },
@@ -156,4 +164,83 @@ describe('PaymentProviderDialog payment guide', () => {
const payload = wrapper.emitted('save')?.[0]?.[0] as { config: Record<string, string> }
expect(payload.config.accountId).toBe('')
})
it('serializes EasyPay custom methods and adds them to supported_types', async () => {
const provider = providerFactory({
provider_key: 'easypay',
name: 'EasyPay',
config: {
pid: 'pid-1',
apiBase: 'https://pay.example.com',
notifyUrl: 'https://example.com/api/v1/payment/webhook/easypay',
returnUrl: 'https://example.com/payment/result',
},
supported_types: ['alipay', 'wxpay'],
payment_mode: 'qrcode',
})
const wrapper = mountDialog({ editing: provider })
;(wrapper.vm as unknown as { loadProvider: (provider: ProviderInstance) => void }).loadProvider(provider)
await nextTick()
await wrapper.find('button.btn-sm').trigger('click')
await nextTick()
const inputs = wrapper.findAll('input[type="text"]')
const ldcTypeInput = inputs.find(input => (input.element as HTMLInputElement).placeholder === 'ldc')
const upstreamTypeInput = inputs.find(input => (input.element as HTMLInputElement).placeholder === 'epay')
const displayNameInput = inputs.find(input => (input.element as HTMLInputElement).placeholder === 'LDC')
if (!ldcTypeInput || !upstreamTypeInput || !displayNameInput) {
throw new Error('custom method inputs not found')
}
await ldcTypeInput.setValue('ldc')
await upstreamTypeInput.setValue('epay')
await displayNameInput.setValue('LDC')
await wrapper.find('form').trigger('submit.prevent')
const payload = wrapper.emitted('save')?.[0]?.[0] as {
config: Record<string, string>
supported_types: string[]
}
expect(payload.config.customMethods).toBe('[{"type":"ldc","upstreamType":"epay","displayName":"LDC"}]')
expect(payload.supported_types).toEqual(['alipay', 'wxpay', 'ldc'])
})
it('rejects custom EasyPay method types with built-in payment prefixes', async () => {
const provider = providerFactory({
provider_key: 'easypay',
name: 'EasyPay',
config: {
pid: 'pid-1',
apiBase: 'https://pay.example.com',
notifyUrl: 'https://example.com/api/v1/payment/webhook/easypay',
returnUrl: 'https://example.com/payment/result',
},
supported_types: ['alipay', 'wxpay'],
payment_mode: 'qrcode',
})
const wrapper = mountDialog({ editing: provider })
;(wrapper.vm as unknown as { loadProvider: (provider: ProviderInstance) => void }).loadProvider(provider)
await nextTick()
await wrapper.find('button.btn-sm').trigger('click')
await nextTick()
const inputs = wrapper.findAll('input[type="text"]')
const typeInput = inputs.find(input => (input.element as HTMLInputElement).placeholder === 'ldc')
const upstreamTypeInput = inputs.find(input => (input.element as HTMLInputElement).placeholder === 'epay')
const displayNameInput = inputs.find(input => (input.element as HTMLInputElement).placeholder === 'LDC')
if (!typeInput || !upstreamTypeInput || !displayNameInput) {
throw new Error('custom method inputs not found')
}
await typeInput.setValue('alipay_hk')
await upstreamTypeInput.setValue('hkpay')
await displayNameInput.setValue('Hong Kong Alipay')
await wrapper.find('form').trigger('submit.prevent')
expect(wrapper.emitted('save')).toBeUndefined()
})
})
@@ -59,6 +59,18 @@ describe('getVisibleMethods', () => {
expect(visible.alipay.single_min).toBe(2)
expect(visible.wxpay.fee_rate).toBe(1.2)
})
it('keeps custom EasyPay methods as visible methods', () => {
const visible = getVisibleMethods({
ldc: methodLimit({ single_min: 3 }),
usdt_trc20: methodLimit({ fee_rate: 1 }),
})
expect(visible).toEqual({
ldc: methodLimit({ single_min: 3 }),
usdt_trc20: methodLimit({ fee_rate: 1 }),
})
})
})
describe('decidePaymentLaunch', () => {
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest'
import { PAYMENT_CURRENCY_OPTIONS, PROVIDER_CONFIG_FIELDS } from '@/components/payment/providerConfig'
import {
PAYMENT_CURRENCY_OPTIONS,
PROVIDER_CONFIG_FIELDS,
parseEasyPayCustomMethods,
serializeEasyPayCustomMethods,
} from '@/components/payment/providerConfig'
function findField(providerKey: string, key: string) {
const fields = PROVIDER_CONFIG_FIELDS[providerKey] || []
@@ -50,3 +55,27 @@ describe('PROVIDER_CONFIG_FIELDS.stripe', () => {
expect(currency?.options).toBe(PAYMENT_CURRENCY_OPTIONS)
})
})
describe('EasyPay custom methods config', () => {
it('parses customMethods from the JSON string stored in provider config', () => {
expect(parseEasyPayCustomMethods(
'[{"type":"ldc","upstreamType":"epay","displayName":"LDC"},{"type":"usdt_trc20","upstreamType":"usdt","displayName":"USDT-TRC20"}]',
)).toEqual([
{ type: 'ldc', upstreamType: 'epay', displayName: 'LDC' },
{ type: 'usdt_trc20', upstreamType: 'usdt', displayName: 'USDT-TRC20' },
])
})
it('serializes non-empty custom methods into the config string format', () => {
expect(serializeEasyPayCustomMethods([
{ type: 'ldc', upstreamType: 'epay', displayName: 'LDC' },
{ type: ' ', upstreamType: 'ignored', displayName: 'Ignored' },
{ type: 'usdt_trc20', upstreamType: 'usdt', displayName: '' },
])).toBe('[{"type":"ldc","upstreamType":"epay","displayName":"LDC"},{"type":"usdt_trc20","upstreamType":"usdt","displayName":""}]')
})
it('returns an empty string for invalid or empty custom methods', () => {
expect(parseEasyPayCustomMethods('not-json')).toEqual([])
expect(serializeEasyPayCustomMethods([{ type: '', upstreamType: 'epay', displayName: 'LDC' }])).toBe('')
})
})
@@ -99,7 +99,7 @@ export function getVisibleMethods(methods: Record<string, MethodLimit>): Record<
const visible: Record<string, MethodLimit> = {}
Object.entries(methods).forEach(([type, limit]) => {
const normalized = normalizeVisibleMethod(type)
const normalized = normalizeVisibleMethod(type) || type.trim()
if (!normalized) return
const isCanonical = type === normalized
@@ -21,6 +21,12 @@ export interface TypeOption {
[key: string]: unknown
}
export interface EasyPayCustomMethod {
type: string
upstreamType: string
displayName: string
}
/** Callback URL paths for a provider. */
export interface CallbackPaths {
notifyUrl?: string
@@ -171,6 +177,34 @@ export function getAvailableTypes(
return types.map(t => resolveTypeLabel(t, providerKey, allTypes, redirectLabel))
}
export function parseEasyPayCustomMethods(raw: string | undefined): EasyPayCustomMethod[] {
if (!raw || !raw.trim()) return []
try {
const parsed = JSON.parse(raw)
if (!Array.isArray(parsed)) return []
return parsed
.map(item => ({
type: String(item?.type || '').trim(),
upstreamType: String(item?.upstreamType || '').trim(),
displayName: String(item?.displayName || '').trim(),
}))
.filter(item => item.type && item.upstreamType)
} catch {
return []
}
}
export function serializeEasyPayCustomMethods(methods: EasyPayCustomMethod[]): string {
const clean = methods
.map(method => ({
type: method.type.trim(),
upstreamType: method.upstreamType.trim(),
displayName: method.displayName.trim(),
}))
.filter(method => method.type && method.upstreamType)
return clean.length ? JSON.stringify(clean) : ''
}
/** Extract base URL from a full callback URL by removing the known path suffix. */
export function extractBaseUrl(fullUrl: string, path: string): string {
if (!fullUrl) return ''
+12
View File
@@ -6232,6 +6232,12 @@ export default {
validationNameRequired: 'Provider name is required',
validationTypesRequired: 'Please select at least one supported payment type',
validationFieldRequired: '{field} is required',
validationEasyPayCustomMethodRequired: 'Each custom EasyPay method requires both a payment type and an upstream type',
validationEasyPayCustomMethodTypeInvalid: 'Custom EasyPay payment types may only contain lowercase letters, digits, underscores, and hyphens',
validationEasyPayCustomMethodUpstreamTypeInvalid: 'EasyPay upstream types may only contain lowercase letters, digits, underscores, and hyphens',
validationEasyPayCustomMethodReserved: 'Custom EasyPay payment types cannot use built-in alipay or wxpay',
validationEasyPayCustomMethodPrefixReserved: 'Custom EasyPay payment types cannot start with alipay or wxpay',
validationEasyPayCustomMethodDuplicate: 'Custom EasyPay payment types must be unique',
field_apiBase: 'API Base URL',
field_notifyUrl: 'Notify URL',
field_returnUrl: 'Return URL',
@@ -6261,6 +6267,12 @@ export default {
field_cid: 'Channel ID',
field_cidAlipay: 'Alipay Channel ID',
field_cidWxpay: 'WeChat Channel ID',
easypayCustomMethods: 'Custom EasyPay methods',
easypayCustomMethodsHint: 'Add provider-specific methods supported by this EasyPay endpoint. The payment type is stored on Sub2API orders; the upstream type is sent as EasyPay type.',
addCustomMethod: 'Add method',
customMethodType: 'Payment type',
customMethodUpstreamType: 'Upstream type',
customMethodDisplayName: 'Display name',
stripeWebhookHint: 'Configure the following URL as a Webhook endpoint in Stripe Dashboard:',
stripeWebhookApiVersionHint: 'Set this Webhook endpoint API version to match the integrated Stripe SDK. Recommended: {version}. A mismatch can cause webhook parsing errors.',
airwallexWebhookHint: 'Configure the following URL as a Webhook endpoint in Airwallex. Select at least Payment Intent -> Succeeded (payment_intent.succeeded), preferably also Payment Intent -> Cancelled (payment_intent.cancelled). Use the account default or latest stable API version.',
+12
View File
@@ -6387,6 +6387,12 @@ export default {
validationNameRequired: '服务商名称不能为空',
validationTypesRequired: '请至少选择一种支持的支付方式',
validationFieldRequired: '{field} 不能为空',
validationEasyPayCustomMethodRequired: '每个易支付自定义方式都必须填写支付方式和上游 type',
validationEasyPayCustomMethodTypeInvalid: '易支付自定义支付方式只能包含小写字母、数字、下划线和短横线',
validationEasyPayCustomMethodUpstreamTypeInvalid: '易支付上游 type 只能包含小写字母、数字、下划线和短横线',
validationEasyPayCustomMethodReserved: '易支付自定义支付方式不能使用内置的 alipay 或 wxpay',
validationEasyPayCustomMethodPrefixReserved: '易支付自定义支付方式不能以 alipay 或 wxpay 开头',
validationEasyPayCustomMethodDuplicate: '易支付自定义支付方式不能重复',
field_apiBase: 'API 基础地址',
field_notifyUrl: '异步通知地址',
field_returnUrl: '同步跳转地址',
@@ -6416,6 +6422,12 @@ export default {
field_cid: '支付渠道 ID',
field_cidAlipay: '支付宝渠道 ID',
field_cidWxpay: '微信渠道 ID',
easypayCustomMethods: '易支付自定义方式',
easypayCustomMethodsHint: '添加当前易支付接口额外支持的方式。支付方式会记录到 Sub2API 订单中,上游 type 会作为易支付 type 参数提交。',
addCustomMethod: '添加方式',
customMethodType: '支付方式',
customMethodUpstreamType: '上游 type',
customMethodDisplayName: '显示名称',
stripeWebhookHint: '请在 Stripe Dashboard 中将以下地址配置为 Webhook 端点:',
stripeWebhookApiVersionHint: 'Webhook 端点的 API 版本请与当前集成的 Stripe SDK 对齐,建议选择 {version};版本不一致可能导致回调事件解析失败。',
airwallexWebhookHint: '请在 Airwallex 后台将以下地址配置为 Webhook 端点;事件至少选择 Payment Intent -> Succeeded(payment_intent.succeeded),建议同时选择 Payment Intent -> Cancelled(payment_intent.cancelled);API version 选择账户默认或最新稳定版本。',
+1
View File
@@ -43,6 +43,7 @@ export interface PaymentConfig {
export interface MethodLimit {
currency?: string
display_name?: string
daily_limit: number
daily_used: number
daily_remaining: number
+2
View File
@@ -603,6 +603,7 @@ const methodOptions = computed<PaymentMethodOption[]>(() =>
const ml = visibleMethods.value[type]
return {
type,
display_name: ml?.display_name,
fee_rate: ml?.fee_rate ?? 0,
available: ml?.available !== false && amountFitsMethod(validAmount.value, type),
}
@@ -672,6 +673,7 @@ const subMethodOptions = computed<PaymentMethodOption[]>(() => {
const currency = normalizePaymentCurrency(ml?.currency)
return {
type,
display_name: ml?.display_name,
fee_rate: ml?.fee_rate ?? 0,
available: ml?.available !== false && amountFitsMethod(subscriptionTotalAmountForCurrency(price, currency), type),
}