diff --git a/backend/internal/payment/provider/easypay_refund_test.go b/backend/internal/payment/provider/easypay_refund_test.go index 8d535b5126..730e82feff 100644 --- a/backend/internal/payment/provider/easypay_refund_test.go +++ b/backend/internal/payment/provider/easypay_refund_test.go @@ -202,7 +202,7 @@ func TestEasyPayCustomMethodsUseConfiguredUpstreamType(t *testing.T) { "notifyUrl": "https://example.com/notify", "returnUrl": "https://example.com/return", "paymentMode": paymentModePopup, - "customMethods": `[{"type":"ldc","upstreamType":"epay","displayName":"LDC"},{"type":"usdt_trc20","upstreamType":"usdt","displayName":"USDT-TRC20"}]`, + "customMethods": `[{"type":"ldc","upstreamType":"epay","displayName":"LDC"},{"type":"usdt_trc20","upstreamType":"usdt.trc20","displayName":"USDT-TRC20"}]`, }) if err != nil { t.Fatalf("NewEasyPay: %v", err) @@ -221,8 +221,8 @@ func TestEasyPayCustomMethodsUseConfiguredUpstreamType(t *testing.T) { if err != nil { t.Fatalf("parse pay url: %v", err) } - if got := payURL.Query().Get("type"); got != "usdt" { - t.Fatalf("pay url type = %q, want usdt (%s)", got, resp.PayURL) + if got := payURL.Query().Get("type"); got != "usdt.trc20" { + t.Fatalf("pay url type = %q, want usdt.trc20 (%s)", got, resp.PayURL) } } diff --git a/backend/internal/service/payment_config_providers.go b/backend/internal/service/payment_config_providers.go index d1bf2de7aa..b4d8f2b22d 100644 --- a/backend/internal/service/payment_config_providers.go +++ b/backend/internal/service/payment_config_providers.go @@ -224,6 +224,7 @@ func validateProviderRequest(providerKey, name, supportedTypes string) error { } var easyPayCustomMethodCodePattern = regexp.MustCompile(`^[a-z0-9_-]+$`) +var easyPayUpstreamMethodCodePattern = regexp.MustCompile(`^[a-z0-9_.-]+$`) type easyPayCustomMethodConfig struct { Type string `json:"type"` @@ -253,8 +254,8 @@ func validateEasyPayCustomMethods(config map[string]string, supportedTypes strin if !easyPayCustomMethodCodePattern.MatchString(method.Type) { return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods type may only contain lowercase letters, digits, underscores, and hyphens") } - if !easyPayCustomMethodCodePattern.MatchString(method.UpstreamType) { - return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods upstreamType may only contain lowercase letters, digits, underscores, and hyphens") + if !easyPayUpstreamMethodCodePattern.MatchString(method.UpstreamType) { + return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods upstreamType may only contain lowercase letters, digits, periods, underscores, and hyphens") } if easyPayCustomMethodTypeConflictsWithBuiltin(method.Type) { return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods type cannot start with alipay or wxpay") diff --git a/backend/internal/service/payment_config_providers_test.go b/backend/internal/service/payment_config_providers_test.go index 74fd2a3467..c539449c69 100644 --- a/backend/internal/service/payment_config_providers_test.go +++ b/backend/internal/service/payment_config_providers_test.go @@ -128,6 +128,23 @@ func TestValidateEasyPayCustomMethods(t *testing.T) { config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay","displayName":"LDC"}]`}, supportedTypes: "alipay,wxpay,ldc", }, + { + name: "upstream type allows periods", + config: map[string]string{"customMethods": `[{"type":"usdt_trc20","upstreamType":"usdt.trc20"}]`}, + supportedTypes: "alipay,wxpay,usdt_trc20", + }, + { + name: "custom type still rejects periods", + config: map[string]string{"customMethods": `[{"type":"usdt.trc20","upstreamType":"usdt.trc20"}]`}, + supportedTypes: "alipay,wxpay,usdt.trc20", + wantErr: "customMethods type may only contain lowercase letters", + }, + { + name: "upstream type still rejects slashes", + config: map[string]string{"customMethods": `[{"type":"usdt_trc20","upstreamType":"usdt/trc20"}]`}, + supportedTypes: "alipay,wxpay,usdt_trc20", + wantErr: "customMethods upstreamType may only contain lowercase letters", + }, { name: "malformed custom methods json", config: map[string]string{"customMethods": `not-json`}, diff --git a/frontend/src/components/payment/PaymentProviderDialog.vue b/frontend/src/components/payment/PaymentProviderDialog.vue index 73724bbf5f..7743e4073a 100644 --- a/frontend/src/components/payment/PaymentProviderDialog.vue +++ b/frontend/src/components/payment/PaymentProviderDialog.vue @@ -759,7 +759,7 @@ function validateEasyPayCustomMethods(): string | null { if (!/^[a-z0-9_-]+$/.test(method.type)) { return t('admin.settings.payment.validationEasyPayCustomMethodTypeInvalid') } - if (!/^[a-z0-9_-]+$/.test(method.upstreamType)) { + if (!/^[a-z0-9_.-]+$/.test(method.upstreamType)) { return t('admin.settings.payment.validationEasyPayCustomMethodUpstreamTypeInvalid') } if ((PROVIDER_SUPPORTED_TYPES.easypay || []).includes(method.type)) { diff --git a/frontend/src/components/payment/__tests__/PaymentProviderDialog.spec.ts b/frontend/src/components/payment/__tests__/PaymentProviderDialog.spec.ts index 2596952d67..18ca4cb483 100644 --- a/frontend/src/components/payment/__tests__/PaymentProviderDialog.spec.ts +++ b/frontend/src/components/payment/__tests__/PaymentProviderDialog.spec.ts @@ -166,7 +166,7 @@ describe('PaymentProviderDialog payment guide', () => { expect(payload.config.accountId).toBe('') }) - it('serializes EasyPay custom methods and adds them to supported_types', async () => { + it.each(['epay', 'usdt.trc20'])('serializes EasyPay upstream type %s and adds the local type to supported_types', async (upstreamType) => { const provider = providerFactory({ provider_key: 'easypay', name: 'EasyPay', @@ -197,7 +197,7 @@ describe('PaymentProviderDialog payment guide', () => { } await ldcTypeInput.setValue('ldc') - await upstreamTypeInput.setValue('epay') + await upstreamTypeInput.setValue(upstreamType) await displayNameInput.setValue('LDC') await wrapper.find('form').trigger('submit.prevent') @@ -205,11 +205,15 @@ describe('PaymentProviderDialog payment guide', () => { config: Record supported_types: string[] } - expect(payload.config.customMethods).toBe('[{"type":"ldc","upstreamType":"epay","displayName":"LDC"}]') + expect(JSON.parse(payload.config.customMethods)).toEqual([{ type: 'ldc', upstreamType, displayName: 'LDC' }]) expect(payload.supported_types).toEqual(['alipay', 'wxpay', 'ldc']) }) - it('rejects custom EasyPay method types with built-in payment prefixes', async () => { + it.each([ + ['alipay_hk', 'hkpay'], + ['usdt.trc20', 'usdt.trc20'], + ['usdt_trc20', 'usdt/trc20'], + ])('rejects invalid EasyPay mapping %s to %s', async (type, upstreamType) => { const provider = providerFactory({ provider_key: 'easypay', name: 'EasyPay', @@ -239,9 +243,9 @@ describe('PaymentProviderDialog payment guide', () => { throw new Error('custom method inputs not found') } - await typeInput.setValue('alipay_hk') - await upstreamTypeInput.setValue('hkpay') - await displayNameInput.setValue('Hong Kong Alipay') + await typeInput.setValue(type) + await upstreamTypeInput.setValue(upstreamType) + await displayNameInput.setValue('Custom payment') await wrapper.find('form').trigger('submit.prevent') expect(wrapper.emitted('save')).toBeUndefined() diff --git a/frontend/src/i18n/locales/en/admin/settings.ts b/frontend/src/i18n/locales/en/admin/settings.ts index eb5c74dfe3..88f5e1d760 100644 --- a/frontend/src/i18n/locales/en/admin/settings.ts +++ b/frontend/src/i18n/locales/en/admin/settings.ts @@ -759,7 +759,7 @@ export default { 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', + validationEasyPayCustomMethodUpstreamTypeInvalid: 'EasyPay upstream types may only contain lowercase letters, digits, periods, 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', diff --git a/frontend/src/i18n/locales/zh/admin/settings.ts b/frontend/src/i18n/locales/zh/admin/settings.ts index 5e0db540b0..85b9701c3c 100644 --- a/frontend/src/i18n/locales/zh/admin/settings.ts +++ b/frontend/src/i18n/locales/zh/admin/settings.ts @@ -754,7 +754,7 @@ export default { validationFieldRequired: '{field} 不能为空', validationEasyPayCustomMethodRequired: '每个易支付自定义方式都必须填写支付方式和上游 type', validationEasyPayCustomMethodTypeInvalid: '易支付自定义支付方式只能包含小写字母、数字、下划线和短横线', - validationEasyPayCustomMethodUpstreamTypeInvalid: '易支付上游 type 只能包含小写字母、数字、下划线和短横线', + validationEasyPayCustomMethodUpstreamTypeInvalid: '易支付上游 type 只能包含小写字母、数字、点号、下划线和短横线', validationEasyPayCustomMethodReserved: '易支付自定义支付方式不能使用内置的 alipay 或 wxpay', validationEasyPayCustomMethodPrefixReserved: '易支付自定义支付方式不能以 alipay 或 wxpay 开头', validationEasyPayCustomMethodDuplicate: '易支付自定义支付方式不能重复',