Commit Graph

31 Commits

Author SHA1 Message Date
Nobody-Zhang
1a0cabbfd6 Fix Zpay refund endpoint handling 2026-04-26 04:57:34 +00:00
IanShaw027
f35e967516 fix payment qr fallback and admin guidance 2026-04-22 07:33:14 -07:00
IanShaw027
6b19490393 fix(ci): align openai account tests and remove dead wxpay const 2026-04-22 18:09:46 +08:00
IanShaw027
9de7a72cce fix(upgrade): close payment and oidc compatibility gaps 2026-04-22 18:01:51 +08:00
IanShaw027
1aab084ecb fix(payment): restore upgrade-safe payment flows 2026-04-22 14:57:16 +08:00
IanShaw027
c229f33e9e fix(review): harden payment, oauth, and migration paths 2026-04-22 10:26:22 +08:00
IanShaw027
b13e34f831 fix(ci): align auth and payment verification tests 2026-04-22 02:32:53 +08:00
IanShaw027
da1d26001f Merge branch 'main' into rebuild/auth-identity-foundation 2026-04-22 00:35:34 +08:00
IanShaw027
0934f737d5 fix: snapshot merchant identity for alipay and easypay 2026-04-21 13:35:54 +08:00
IanShaw027
119f784d19 fix: validate wxpay payments against order snapshots 2026-04-21 12:57:35 +08:00
IanShaw027
07f23aaa7d fix wxpay config contract and h5 scene info 2026-04-21 08:35:53 +08:00
IanShaw027
1d8432b8a4 fix: harden payment resume and wxpay webhook routing 2026-04-21 01:40:56 +08:00
IanShaw027
16be82b959 fix payment visible methods and resume recovery 2026-04-21 00:14:05 +08:00
IanShaw027
f83fd59dca Refine payment UX for wallet flows 2026-04-21 00:05:09 +08:00
IanShaw027
7ef7fd19e7 fix: restore wechat payment oauth and jsapi flow 2026-04-20 23:34:57 +08:00
IanShaw027
6f00efa350 fix: support legacy payment method aliases 2026-04-20 23:05:29 +08:00
erio
79192cf65b feat(payment): harden wxpay config validation with structured errors
Motivation: platform-certificate mode is being phased out by WeChat (2024-10+,
newly-provisioned merchants already cannot download platform certificates at
all), and wxpay config errors currently surface only when an order is being
created — admins have no feedback at save time. Also, errors were returned as
natural-language strings, leaving the frontend no way to localize them.

Changes:

- backend/internal/payment/provider/wxpay.go
  - Replace fmt.Errorf with structured infraerrors.BadRequest errors:
    - WXPAY_CONFIG_MISSING_KEY    (metadata: key)
    - WXPAY_CONFIG_INVALID_KEY_LENGTH  (metadata: key, expected, actual)
    - WXPAY_CONFIG_INVALID_KEY    (metadata: key) for malformed PEMs
  - Parse privateKey and publicKey PEMs in NewWxpay so malformed keys fail
    at save time instead of at order creation.
  - Keep the pubkey verifier (NewSHA256WithRSAPubkeyVerifier) as the single
    supported verifier; no more loadKeyPair helper.

- backend/internal/service/payment_order.go invokeProvider
  - If CreateProvider or CreatePayment returns a structured ApplicationError,
    pass it through (optionally enriching metadata with provider/instance_id)
    instead of wrapping it as generic PAYMENT_GATEWAY_ERROR — so clients see
    the actual reason code (e.g. WXPAY_CONFIG_MISSING_KEY) and can localize.
  - Simplify a few messages (TOO_MANY_PENDING, DAILY_LIMIT_EXCEEDED,
    PAYMENT_GATEWAY_ERROR, NO_AVAILABLE_INSTANCE) to keyword form with
    metadata for template variables.

- backend/internal/service/payment_config_providers.go
  - New helper validateProviderConfig calls provider.CreateProvider at save
    time. Enabled instances are validated on both Create and Update so admins
    see config errors immediately in the dialog, not later at order creation.
  - Disabled instances are not validated (half-filled drafts are allowed).

- backend/internal/payment/provider/wxpay_test.go
  - Add generateTestKeyPair helper that produces valid RSA-2048 PKCS8/PKIX
    PEMs per test, used by the valid-config baseline (prior fake strings no
    longer pass the eager PEM check).
  - Cover each structured-error branch (missing/invalid-length/malformed PEM).
2026-04-20 19:49:45 +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
61a008f7e4 chore(payment): mark legacy AES ciphertext fallback as deprecated
明文 JSON 已经是新写入的默认格式;保留 AES 密文读取仅为兼容迁移期间的旧
记录,一旦所有部署通过管理后台重存过一次即可删除。标记为 deprecated 并加
TODO,几个版本后统一清理掉:payment.Encrypt / payment.Decrypt、两处
decryptConfig 的 AES 分支、PaymentConfigService.encryptionKey 和
DefaultLoadBalancer.encryptionKey 字段。
2026-04-17 23:24:27 +08:00
erio
fd0c9a1305 fix(payment): store provider config as plaintext JSON with legacy ciphertext fallback
Without TOTP_ENCRYPTION_KEY, saved payment configs were lost on restart because
the AES round-trip failed silently. Write new records as plaintext JSON; read
path tries JSON first, falls back to legacy AES decrypt when a key is present,
and treats unreadable values as empty so admins can re-enter them via the UI.
2026-04-17 22:06:32 +08:00
erio
6ac8ccde46 fix: merge 30 general improvements from release branch
Bug fixes:
- Detached context for GetAccountConcurrencyBatch (prevent all-zero on request cancel)
- Filter soft-deleted users in GetByGroupID
- Stripe CSP policy (allow Stripe.js in script-src and frame-src)
- WebSearch API key validation on save
- RECHARGING status in payment result success check
- Windows test fixes (logger Sync deadlock, config path escaping)

Feature enhancements:
- Webhook multi-instance dispatch (extractOutTradeNo + GetWebhookProvider)
- EasyPay mobile H5 payment (device param + PayURL2)
- SSE error propagation in WebSearch emulation
- AccountStatsCost DTO field for admin usage logs
- Plans sort by sort_order instead of created_at
- UsageMapHook for streaming response usage data
- apicompat Instructions field passthrough
- EffectiveLoadFactor for ops concurrency/metrics
- Usage billing RETURNING balance for notify system
- BulkUpdate mixed channel warning with details
- println to slog migration in auth cache
- Wire ProviderSet cleanup
- CI cache-dependency-path optimization

Frontend:
- Refund eligibility check per provider (canRequestRefund)
- Plan sort_order editing
- Dead code cleanup (simulate_claude_max, client_affinity)
- GroupsView platform switch guard
- channels features_config API type
- UsageView account_stats_cost export
2026-04-14 17:35:27 +08:00
erio
6a08efeef9 fix: resolve upstream CI failures (lint, test, gofmt)
- Fix errcheck: handle Write/Encode return values in brave_test.go
- Fix errcheck: defer resp.Body.Close() with _ assignment in tavily.go
- Fix gofmt: payment.go, channel.go, payment_config_providers.go
- Fix unused: remove dead decodeURLValue in easypay.go
- Restore shouldFallbackGeminiModel function (deleted during cherry-pick)
- Add missing balanceNotifyService param to NewGatewayService in test
- Fix platform default test expectation (empty stays empty)
- Fix wildcard pricing test (longest prefix wins, not config order)
- Fix subscription group test (SUBSCRIPTION_REPOSITORY_UNAVAILABLE)
2026-04-14 12:11:08 +08:00
erio
4aa0070e3d fix: Stripe payment type matching in load balancer
Checkout page aggregates Stripe sub-types (card,link,alipay,wxpay) under
"stripe", but SelectInstance matched against supported_types literally,
which doesn't contain "stripe". Now matches by provider_key for Stripe.
2026-04-14 11:31:44 +08:00
erio
1e6912ea2e fix: gofmt formatting across all Go source files 2026-04-14 09:36:26 +08:00
erio
c738cfec93 fix(payment): critical audit fixes for security, idempotency and correctness
Backend fixes:
- #1: doSub subscription idempotency via audit log check
- #2: markFailed only when status=RECHARGING (prevents overwriting COMPLETED)
- #3: ExpireTimedOutOrders checks upstream payment before expiring
- #4: Public verify endpoint for payment result page (no auth required)
- #5: EasyPay QueryOrder returns amount, confirmPayment handles zero amount
- #6: WxPay notifyUrl priority: request-first, config-fallback
- #7: EasyPay remove double URL decode in VerifyNotification
- #8: checkPaid/cancelUpstreamPayment use order's provider instance
- #9: Amount NaN/Inf/negative validation in order creation and refund
- #10: Refund amount comparison uses tolerance instead of float64 ==
- #11: Skip balance deduction on retry when previous rollback failed
- #12: checkPaid logs fulfillment errors instead of silently ignoring
- #13: WxPay certSerial added to required config fields

Frontend fixes:
- Payment result page no longer requires authentication
- Public verify API fallback for expired sessions
2026-04-14 09:19:33 +08:00
erio
56e4a9a914 fix: audit fixes - magic strings to constants, frontend any/catch, LB tests
Backend:
- Define OrderTypeBalance/Subscription, EntityStatusActive, DeductionType*,
  NotificationStatus* constants in payment/types.go
- Replace all magic strings in payment_order, payment_fulfillment, payment_refund
- Add local constants in easypay.go (tradeStatusSuccess, signTypeMD5)
- Add 27 unit tests for load balancer (filterByLimits, pickLeastAmount,
  getInstanceChannelLimits, startOfDay)

Frontend:
- Remove all `any` types in SettingsView.vue (18 catch blocks + 1 payload)
- Fix bare catch blocks in PaymentResultView, PaymentView
- Add `unknown` type annotation to all catch blocks

chore: bump version to 0.1.108.140
2026-04-14 09:18:58 +08:00
erio
3c884f8e30 test(payment): add unit tests for payment audit fixes + allow empty supported_types
Tests (1033 new lines, 100% coverage on modified functions):
- amount.go: YuanToFen/FenToYuan with precision edge cases
- wxpay: mapWxState, wxSV, formatPEM, NewWxpay validation
- alipay: isTradeNotExist, NewAlipay validation
- webhook: writeSuccessResponse (wxpay JSON, stripe empty, others text)
- config: validateProviderRequest, isSensitiveConfigField, joinTypes
- fulfillment: resolveRedeemAction idempotency logic

Business logic changes:
- Allow empty supported_types on provider instances
- Block removing payment types when instance has pending orders
- Extract resolveRedeemAction as testable pure function
2026-04-14 09:18:22 +08:00
erio
f498eb8fde fix(payment): fix Alipay/Wxpay direct provider type mapping and enable cross-provider load balancing
Two issues fixed:

1. Alipay.SupportedTypes() returned ["alipay_direct"] and Wxpay returned
   ["wxpay_direct"], but the frontend sends payment_type="alipay"/"wxpay".
   The registry lookup failed with "payment method (alipay) is not
   configured". Fix: return the base types ["alipay"]/["wxpay"].

2. When multiple providers support the same payment type (e.g. EasyPay
   and Alipay direct both handle "alipay"), only the last-registered
   provider's instances were reachable — the registry mapped one type to
   one provider key, and SelectInstance queried by that single key.

   Fix: bypass the registry in invokeProvider and let SelectInstance
   query across all providers when providerKey is empty. The selected
   instance's own ProviderKey (now included in InstanceSelection) is
   used to create the correct provider, enabling true cross-provider
   load balancing.

Closes #1592
2026-04-13 14:07:12 +08:00
erio
7515590324 feat(payment): add H5/mobile payment support
Backend:
- Parse EasyPay `payurl2` field, prefer H5 link on mobile
- Add `device=mobile` to EasyPay submit.php (popup) mode
- Expand isMobile detection keywords (add ipad/ipod)

Frontend:
- Add `isMobileDevice()` utility (userAgentData + UA regex)
- Mobile + pay_url: direct redirect instead of QR/popup
- Popup blocked fallback: auto-redirect when window.open fails
- Stripe WeChat Pay: dynamic client param (mobile_web vs web)
2026-04-11 13:16:35 +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