Commit Graph
36 Commits
Author SHA1 Message Date
Albert Coady 27cb485d55 fix: share built-in payment method matching 2026-07-06 15:00:29 +08:00
Albert Coady 22ec77b570 fix: match built-in payment methods exactly 2026-07-06 15:00:29 +08:00
Albert Coady a5a2fea045 Polish EasyPay custom method UI 2026-07-06 15:00:29 +08:00
Albert Coady bf76168ba5 feat: add custom easypay payment methods 2026-07-06 15:00:29 +08:00
shaw 11a3da65c0 fix(group): harden peak-rate config handling and label peak windows with server timezone
Follow-up fixes to #3569 based on code audit:

- Expose server_timezone / server_utc_offset in public settings (and the
  __APP_CONFIG__ injection payload) and label every peak-window display
  with the server UTC offset, so users don't misread the billing window
  as browser-local time
- Unify CreateGroup/UpdateGroup peak-config sanitization via a single
  NormalizePeakRateConfig chokepoint: non-subscription groups always get
  peak fields cleared; unparseable window strings and negative
  multipliers are scrubbed when peak is disabled
- Replace hot-path time.Parse in PeakMultiplierAt with a manual HH:MM
  parser (accept set verified byte-for-byte identical to
  time.Parse("15:04") by exhaustive fuzzing) and reuse it in validation
- Revert the zero-behavior CalculateCost indirection churn in
  billing_service/gateway_service introduced by #3569
- Remove dead GetGroupPlatformMap and the duplicate deref helper in the
  admin handler package
- Share frontend peak formatting via utils/peak-rate.ts, unify the ×N
  label format, and move hardcoded Chinese tooltips to i18n keys
2026-07-02 16:11:07 +08:00
xueshiji 1034f576d7 fix: 高峰倍率全链路透传、计费术语修正与边界处理
- 高峰倍率信息透传至可用渠道、支付计划、结算信息等 API,前端
  GroupBadge / GroupOptionItem / SubscriptionPlanCard / PaymentView /
  SubscriptionsView 统一展示高峰时段与倍率标签
- 修正计费术语:"文本倍率" → "token 倍率",明确高峰倍率同时作用于
  token 计费的图片 token,图片按次计费不受高峰影响
- 允许高峰倍率 multiplier=0,支持高峰时段免费策略
- 切换分组类型为 standard 时自动清除高峰倍率配置
- 长上下文计费与标准计费路径改用内部实现,移除冗余中间调用
- 前端高峰倍率相关控件文案改为 i18n
- 新增多组高峰倍率相关单元测试
2026-07-01 17:46:12 +08:00
wucm667 7316d83027 fix(payment): 区分退款 pending 并收敛匿名查单 2026-06-30 10:00:48 +08:00
Wesley LiddickandGitHub fd9c945fab Merge pull request #3470 from wucm667/fix/order-currency-symbol-from-field
fix(admin): 订单金额币种符号读取 currency 字段,修复 USD 渠道显示为 ¥
2026-06-26 09:46:02 +08:00
SMGDev 65ad7df4f4 fix(payment): 修复后端返回空supported_types时支付提供商卡片消失的问题
统一处理后端返回的null类型supported_types,将其标准化为空数组,避免调用includes()时报错导致卡片无法显示,同时修复多处相关的类型判断逻辑
2026-06-26 00:50:22 +08:00
wucm667 55242ffac1 fix(admin): 订单金额币种符号读取 currency 字段 2026-06-25 16:23:45 +08:00
wucm667 e4c7927eff feat(payment): 支持强制移动端统一使用二维码支付 2026-05-19 18:22:12 +08:00
Wesley LiddickandGitHub a84bf3fab9 Merge pull request #2521 from Brisbanehuang/codex/fix-subscription-model-scopes
fix: hide model scopes for non-antigravity plans
2026-05-19 17:15:15 +08:00
LiuZhouZhouJieLun dbd80a0465 fix wxpay pending order reconciliation 2026-05-19 16:56:31 +08:00
Yuhao JiangandClaude Opus 4.7 1b03240515 fix(payment): 修复支付宝官方扫码二维码生成错误
支付宝官方服务商在 precreate(当面付)不可用回退到 page.pay 时,
错误地把网页跳转 URL 当作可扫码二维码内容返回。前端用 QRCode 库
把这段 URL 渲染成二维码后,支付宝 APP 无法识别(扫到的只是个 HTTP
URL,不是支付二维码),用户必须点"重新打开支付页面"跳转到支付宝
收银台才能扫到真正可用的二维码。

- 后端 alipay.go:createPagePayTrade 不再把 PayURL 塞给 QRCode;
  createDesktopTrade 在 paymentMode == "redirect" 时跳过 precreate
  直接走 page.pay,避免没开通"当面付"的商户走一次无用的 API 调用
- 前端管理端 PaymentProviderDialog:让支付宝官方实例可在"支付模式"
  中选择"跳转",开启后始终在新标签页打开支付宝收银台
- ProviderCard 的 modeLabel 增加 redirect 分支
- 补充 TestCreateTradeRedirectModeSkipsPrecreate 单元测试

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:01:17 -05:00
Brisbanehuang 26ca73a4cd fix: hide model scopes for non-antigravity plans 2026-05-17 02:20:41 +08:00
shaw b23055af5b feat: add Airwallex payments and multi-currency support 2026-05-11 11:17:26 +08:00
shaw 8f28a834f8 fix(payment): 同时启用易支付和 Stripe 时显示 Stripe 按钮
VISIBLE_METHOD_ALIASES 漏了 stripe,导致 getVisibleMethods 把后端返回
的 stripe 过滤掉。点 Stripe 按钮时省略 method 查询参数,让落地页渲染
完整的 Payment Element。
2026-04-25 09:46:27 +08:00
IanShaw027 f35e967516 fix payment qr fallback and admin guidance 2026-04-22 07:33:14 -07:00
IanShaw027 dd314c41e3 fix(payment): restore public resume and result flows 2026-04-22 11:17:23 +08:00
IanShaw027 906802abe3 Fix mobile payment launch detection 2026-04-22 00:36:55 +08:00
IanShaw027 da1d26001f Merge branch 'main' into rebuild/auth-identity-foundation 2026-04-22 00:35:34 +08:00
IanShaw027 07f23aaa7d fix wxpay config contract and h5 scene info 2026-04-21 08:35:53 +08:00
IanShaw027 a27a7add3d fix payment resume result consistency 2026-04-21 02:08:34 +08:00
IanShaw027 7ef7fd19e7 fix: restore wechat payment oauth and jsapi flow 2026-04-20 23:34:57 +08:00
erio 40d4e167cd feat(payment): i18n payment error codes and label localization
Pairs with the backend structured payment errors (reason + metadata). The
frontend now maps reason codes to localized messages with metadata as
interpolation variables, and automatically localizes raw config-field names
(e.g. "certSerial" → "证书序列号") using the existing UI-label i18n
namespace.

- frontend/src/utils/apiError.ts
  - extractApiErrorCode now prefers the string `reason` over the numeric HTTP
    `code`; reason is granular enough to drive i18n lookup, HTTP code is not.
  - New extractApiErrorMetadata to pull interpolation params off the error.
  - New extractI18nErrorMessage(err, t, namespace, fallback): looks up
    `<namespace>.<REASON>` in i18n and substitutes metadata. Before
    substitution, `metadata.key` and `metadata.keys` (slash-joined) are
    re-translated through `admin.settings.payment.field_<key>` so users see
    "缺少必填项:证书序列号" instead of "缺少必填项:certSerial".

- frontend/src/i18n/locales/{zh,en}.ts
  - Add payment.errors entries for every structured reason code returned by
    the backend (PAYMENT_DISABLED, INVALID_AMOUNT, TOO_MANY_PENDING,
    DAILY_LIMIT_EXCEEDED, NO_AVAILABLE_INSTANCE, PAYMENT_PROVIDER_MISCONFIGURED,
    WXPAY_CONFIG_MISSING_KEY / INVALID_KEY_LENGTH / INVALID_KEY, NOT_FOUND,
    FORBIDDEN, CONFLICT, INVALID_ORDER_TYPE, INVALID_STATUS,
    BALANCE_NOT_ENOUGH, REFUND_AMOUNT_EXCEEDED, REFUND_FAILED, and more),
    with placeholders for template variables.

- 13 payment-related Vue files
  - Migrate catch-block error reporting from extractApiErrorMessage to
    extractI18nErrorMessage(err, t, 'payment.errors', fallback).
  - Remove the ad-hoc paymentErrorMap computed in SettingsView.vue, which the
    new helper supersedes (it reads i18n directly via t).

- frontend/src/components/payment/providerConfig.ts
  - wxpay: publicKey and publicKeyId are now required (was optional), matching
    the pubkey-only verifier direction; certSerial is already required.

This PR is drop-in safe: reason-preferring extractApiErrorCode is backward
compatible with callers that pass their own i18nMap, and error codes missing
from i18n fall back to the existing message-based path.
2026-04-20 20:23:16 +08:00
IanShaw027 b51bc7ee24 feat: wire payment return url payloads 2026-04-20 20:19:23 +08:00
IanShaw027 e9de839d87 feat: rebuild auth identity foundation flow 2026-04-20 17:39:57 +08:00
erio 235f710853 feat(payment): redact provider secrets in admin config API
Admin GET /api/v1/admin/payment/providers previously returned every
config value — including privateKey / apiV3Key / secretKey etc. —
verbatim. Any future XSS on the admin UI would hand attackers the
full set of production payment credentials, and the plaintext values
sat unnecessarily in browser memory for every operator.

Treat those fields as write-only from the admin surface:

- decryptAndMaskConfig() strips sensitive keys from the GET response.
  The authoritative list is an explicit per-provider registry that
  mirrors the frontend's PROVIDER_CONFIG_FIELDS sensitive flag:
    alipay   → privateKey, publicKey, alipayPublicKey
    wxpay    → privateKey, apiV3Key, publicKey
    stripe   → secretKey, webhookSecret (publishableKey stays plain)
    easypay  → pkey
  Payment runtime still reads the full config via decryptConfig, so
  nothing at the gateway changes.

- mergeConfig() treats an empty value for a sensitive key as "leave
  unchanged" — the admin UI omits unchanged secrets so operators can
  tweak non-sensitive settings without re-entering credentials.

- Admin dialog (PaymentProviderDialog.vue):
  * secret inputs get autocomplete="new-password", data-1p-ignore,
    data-lpignore and data-bwignore so password managers do not
    offer to save provider credentials
  * in edit mode the required-field check skips sensitive fields
    (empty is the "keep existing" signal) and the placeholder shows
    "leave empty to keep" instead of the default example value
  * create mode still requires every non-optional field, including
    secrets, since there is nothing to preserve

- Unit test renamed to TestIsSensitiveProviderConfigField, covers
  the per-provider registry and specifically asserts that Stripe's
  publishableKey is NOT treated as a secret.
2026-04-19 02:22:53 +08:00
erio c3cb0280ef fix(payment): alipay redirect-only flow, H5 detection and popup sizing
The native Alipay provider previously tried to embed the payment page
URL into a QR code on the client — the URL is not a scannable payload
so the QR never worked. Merchants also hit a H5 detection mismatch
whenever the backend UA sniffer missed iPadOS 13+ or embedded browsers,
and the popup window was too small for Alipay's standard checkout
layout (QR + account-login panel on the right), forcing the user to
scroll horizontally and vertically.

Changes:

Backend
- alipay.go: drop QR-on-URL path. Use redirect-only flow —
  alipay.trade.page.pay for PC (returns a gateway URL the browser
  opens in a new window) and alipay.trade.wap.pay for H5 (returns a
  URL the browser jumps to). Both flows produce pages on
  openapi.alipaydev.com / excashier.alipay.com; the client never
  renders a QR itself.
- payment_handler.go: add optional is_mobile bool to
  CreateOrderRequest so the frontend can declare the device
  explicitly. Server still falls back to UA sniffing when absent.

Frontend
- types/payment.ts, PaymentView.vue: declare is_mobile in
  CreateOrderRequest and pass the computed isMobileDevice() value.
- providerConfig.ts: replace the two fixed POPUP_WINDOW_FEATURES
  constants with getPaymentPopupFeatures(), which prefers 1250×900
  (Alipay's checkout footprint), clamps to window.screen.avail* and
  centers the popup so it never overflows on smaller laptops.
- PaymentQRDialog.vue, PaymentStatusPanel.vue, StripePaymentInline.vue,
  PaymentView.vue: use the new helper at all popup call sites.
2026-04-19 02:22:41 +08:00
erio e761d38fd1 fix(payment): integrate recharge fee rate in order flow and fix UI display
Backend:
- Use cfg.RechargeFeeRate in order creation instead of hardcoded 0
- Remove dead getFeeRate stub method
- All amounts computed server-side: order_amount, pay_amount, fee_rate

Frontend - PaymentView:
- Read recharge_fee_rate from checkout-info API (not per-method)
- Show fee breakdown only when fee_rate > 0
- Show credited amount only when multiplier ≠ 1

Frontend - Order display (user + admin):
- Fix fee_rate * 100 bug (fee_rate is already a percentage)
- OrderTable: show pay_amount as primary, fee/credited as sub-lines
- AdminOrderDetail: full breakdown (base/fee/paid/credited)
- AdminRefundDialog: label "到账金额" for clarity
- PaymentResultView: show pay_amount with fee info

Types + i18n:
- Add recharge_fee_rate to CheckoutInfoResponse
- Add fee_rate to CreateOrderResult
- Add translations: creditedAmount, fee, baseAmount, includedInPayAmount
2026-04-15 01:27:24 +08:00
erio 98140f6cac feat(payment): add recharge fee rate setting and fix provider card UI
- Add recharge_fee_rate system setting (percentage fee on top of recharge amount)
- Full backend chain: config constant, PaymentConfig struct, update validation,
  read/write persistence, DTO, handler GET/PUT responses
- Frontend: settings input with preview, i18n (zh/en), API types
- Fix provider card toggle layout: labels above switches to save width
- Fix Chinese translation: "EasyPay" → "易支付" in provider description
2026-04-15 01:27:24 +08:00
erio 60a4b9316b feat(payment): balance recharge multiplier and refund amount separation
- Add balance_recharge_multiplier system setting (e.g. 1.2 = charge 100 get 120)
- Separate order_amount (credited balance) from pay_amount (actual payment)
- Refund calculates gateway amount proportionally from pay_amount
- Frontend shows both amounts in order details, payment status, refund dialog
- Admin settings UI for configuring recharge multiplier
2026-04-15 01:27:24 +08:00
erio f1297a3694 feat: add per-provider allow_user_refund control and align wildcard matching
allow_user_refund:
- Add allow_user_refund field to PaymentProviderInstance ent schema
- Migration 103: ALTER TABLE payment_provider_instances ADD COLUMN
- Cascade logic: disabling refund_enabled auto-disables allow_user_refund
- User refund validation: check provider instance allows user refund
- Admin refund validation: check provider instance allows admin refund
- Subscription refund: deduct days on refund, rollback on failure
- New endpoint: GET /payment/orders/refund-eligible-providers
- Frontend: ToggleSwitch in ProviderCard/Dialog, cascade in SettingsView

Wildcard matching:
- Change findPricingForModel from "longest prefix wins" to "config order
  priority (first match wins)", aligning with channel service behavior
2026-04-14 16:26:46 +08:00
erio 24f0eebcf7 fix(frontend): lower QR code error correction level to reduce density
Closes #1607
2026-04-13 14:07:12 +08:00
erio e3a000e0d4 refactor(payment): code standards fixes and regression repairs
Backend:
- Split payment_order.go (546→314 lines) into payment_order_lifecycle.go
- Replace magic strings with constants in factory, easypay, webhook handler
- Add rate limit/validity unit constants in payment_order_lifecycle, payment_service
- Fix critical regression: add PaymentEnabled to GetPublicSettings response
- Add missing migration 099_fix_migrated_purchase_menu_label_icon.sql

Frontend:
- Fix StripePopupView.vue: replace `as any` with typed interface, use extractApiErrorMessage
- Fix AdminOrderTable.vue: replace hardcoded column labels with i18n t() calls
- Fix SubscriptionsView.vue: replace hardcoded Today/Tomorrow with i18n
- Extract duplicate statusBadgeClass/canRefund/formatOrderDateTime to orderUtils.ts
- Add missing i18n keys: common.today, common.tomorrow, payment.orders.orderType/actions
- Remove dead PurchaseSubscriptionView.vue (replaced by PaymentView)
2026-04-11 13:16:35 +08:00
erio 63d1860dc0 feat(payment): add complete payment system with multi-provider support
Add a full payment and subscription system supporting EasyPay (Alipay/WeChat),
Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
2026-04-11 13:16:35 +08:00