Commit 60a4b931 authored by erio's avatar erio
Browse files

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
parent 7c671b53
...@@ -188,6 +188,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { ...@@ -188,6 +188,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders, PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders,
PaymentEnabledTypes: paymentCfg.EnabledTypes, PaymentEnabledTypes: paymentCfg.EnabledTypes,
PaymentBalanceDisabled: paymentCfg.BalanceDisabled, PaymentBalanceDisabled: paymentCfg.BalanceDisabled,
PaymentBalanceRechargeMultiplier: paymentCfg.BalanceRechargeMultiplier,
PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy, PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy,
PaymentProductNamePrefix: paymentCfg.ProductNamePrefix, PaymentProductNamePrefix: paymentCfg.ProductNamePrefix,
PaymentProductNameSuffix: paymentCfg.ProductNameSuffix, PaymentProductNameSuffix: paymentCfg.ProductNameSuffix,
...@@ -323,9 +324,10 @@ type UpdateSettingsRequest struct { ...@@ -323,9 +324,10 @@ type UpdateSettingsRequest struct {
PaymentDailyLimit *float64 `json:"payment_daily_limit"` PaymentDailyLimit *float64 `json:"payment_daily_limit"`
PaymentOrderTimeoutMin *int `json:"payment_order_timeout_minutes"` PaymentOrderTimeoutMin *int `json:"payment_order_timeout_minutes"`
PaymentMaxPendingOrders *int `json:"payment_max_pending_orders"` PaymentMaxPendingOrders *int `json:"payment_max_pending_orders"`
PaymentEnabledTypes []string `json:"payment_enabled_types"` PaymentEnabledTypes []string `json:"payment_enabled_types"`
PaymentBalanceDisabled *bool `json:"payment_balance_disabled"` PaymentBalanceDisabled *bool `json:"payment_balance_disabled"`
PaymentLoadBalanceStrat *string `json:"payment_load_balance_strategy"` PaymentBalanceRechargeMultiplier *float64 `json:"payment_balance_recharge_multiplier"`
PaymentLoadBalanceStrat *string `json:"payment_load_balance_strategy"`
PaymentProductNamePrefix *string `json:"payment_product_name_prefix"` PaymentProductNamePrefix *string `json:"payment_product_name_prefix"`
PaymentProductNameSuffix *string `json:"payment_product_name_suffix"` PaymentProductNameSuffix *string `json:"payment_product_name_suffix"`
PaymentHelpImageURL *string `json:"payment_help_image_url"` PaymentHelpImageURL *string `json:"payment_help_image_url"`
...@@ -934,24 +936,25 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -934,24 +936,25 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
// Skip if no payment fields were provided (prevents accidental wipe). // Skip if no payment fields were provided (prevents accidental wipe).
if h.paymentConfigService != nil && hasPaymentFields(req) { if h.paymentConfigService != nil && hasPaymentFields(req) {
paymentReq := service.UpdatePaymentConfigRequest{ paymentReq := service.UpdatePaymentConfigRequest{
Enabled: req.PaymentEnabled, Enabled: req.PaymentEnabled,
MinAmount: req.PaymentMinAmount, MinAmount: req.PaymentMinAmount,
MaxAmount: req.PaymentMaxAmount, MaxAmount: req.PaymentMaxAmount,
DailyLimit: req.PaymentDailyLimit, DailyLimit: req.PaymentDailyLimit,
OrderTimeoutMin: req.PaymentOrderTimeoutMin, OrderTimeoutMin: req.PaymentOrderTimeoutMin,
MaxPendingOrders: req.PaymentMaxPendingOrders, MaxPendingOrders: req.PaymentMaxPendingOrders,
EnabledTypes: req.PaymentEnabledTypes, EnabledTypes: req.PaymentEnabledTypes,
BalanceDisabled: req.PaymentBalanceDisabled, BalanceDisabled: req.PaymentBalanceDisabled,
LoadBalanceStrategy: req.PaymentLoadBalanceStrat, BalanceRechargeMultiplier: req.PaymentBalanceRechargeMultiplier,
ProductNamePrefix: req.PaymentProductNamePrefix, LoadBalanceStrategy: req.PaymentLoadBalanceStrat,
ProductNameSuffix: req.PaymentProductNameSuffix, ProductNamePrefix: req.PaymentProductNamePrefix,
HelpImageURL: req.PaymentHelpImageURL, ProductNameSuffix: req.PaymentProductNameSuffix,
HelpText: req.PaymentHelpText, HelpImageURL: req.PaymentHelpImageURL,
CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled, HelpText: req.PaymentHelpText,
CancelRateLimitMax: req.PaymentCancelRateLimitMax, CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled,
CancelRateLimitWindow: req.PaymentCancelRateLimitWindow, CancelRateLimitMax: req.PaymentCancelRateLimitMax,
CancelRateLimitUnit: req.PaymentCancelRateLimitUnit, CancelRateLimitWindow: req.PaymentCancelRateLimitWindow,
CancelRateLimitMode: req.PaymentCancelRateLimitMode, CancelRateLimitUnit: req.PaymentCancelRateLimitUnit,
CancelRateLimitMode: req.PaymentCancelRateLimitMode,
} }
if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil { if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
...@@ -1082,6 +1085,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -1082,6 +1085,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders, PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders,
PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes, PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes,
PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled, PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled,
PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier,
PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy, PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy,
PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix, PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix,
PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix, PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix,
...@@ -1101,6 +1105,7 @@ func hasPaymentFields(req UpdateSettingsRequest) bool { ...@@ -1101,6 +1105,7 @@ func hasPaymentFields(req UpdateSettingsRequest) bool {
req.PaymentMaxAmount != nil || req.PaymentDailyLimit != nil || req.PaymentMaxAmount != nil || req.PaymentDailyLimit != nil ||
req.PaymentOrderTimeoutMin != nil || req.PaymentMaxPendingOrders != nil || req.PaymentOrderTimeoutMin != nil || req.PaymentMaxPendingOrders != nil ||
req.PaymentEnabledTypes != nil || req.PaymentBalanceDisabled != nil || req.PaymentEnabledTypes != nil || req.PaymentBalanceDisabled != nil ||
req.PaymentBalanceRechargeMultiplier != nil ||
req.PaymentLoadBalanceStrat != nil || req.PaymentProductNamePrefix != nil || req.PaymentLoadBalanceStrat != nil || req.PaymentProductNamePrefix != nil ||
req.PaymentProductNameSuffix != nil || req.PaymentHelpImageURL != nil || req.PaymentProductNameSuffix != nil || req.PaymentHelpImageURL != nil ||
req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil || req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil ||
......
...@@ -134,9 +134,10 @@ type SystemSettings struct { ...@@ -134,9 +134,10 @@ type SystemSettings struct {
PaymentDailyLimit float64 `json:"payment_daily_limit"` PaymentDailyLimit float64 `json:"payment_daily_limit"`
PaymentOrderTimeoutMin int `json:"payment_order_timeout_minutes"` PaymentOrderTimeoutMin int `json:"payment_order_timeout_minutes"`
PaymentMaxPendingOrders int `json:"payment_max_pending_orders"` PaymentMaxPendingOrders int `json:"payment_max_pending_orders"`
PaymentEnabledTypes []string `json:"payment_enabled_types"` PaymentEnabledTypes []string `json:"payment_enabled_types"`
PaymentBalanceDisabled bool `json:"payment_balance_disabled"` PaymentBalanceDisabled bool `json:"payment_balance_disabled"`
PaymentLoadBalanceStrat string `json:"payment_load_balance_strategy"` PaymentBalanceRechargeMultiplier float64 `json:"payment_balance_recharge_multiplier"`
PaymentLoadBalanceStrat string `json:"payment_load_balance_strategy"`
PaymentProductNamePrefix string `json:"payment_product_name_prefix"` PaymentProductNamePrefix string `json:"payment_product_name_prefix"`
PaymentProductNameSuffix string `json:"payment_product_name_suffix"` PaymentProductNameSuffix string `json:"payment_product_name_suffix"`
PaymentHelpImageURL string `json:"payment_help_image_url"` PaymentHelpImageURL string `json:"payment_help_image_url"`
......
...@@ -126,26 +126,28 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) { ...@@ -126,26 +126,28 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
} }
response.Success(c, checkoutInfoResponse{ response.Success(c, checkoutInfoResponse{
Methods: limitsResp.Methods, Methods: limitsResp.Methods,
GlobalMin: limitsResp.GlobalMin, GlobalMin: limitsResp.GlobalMin,
GlobalMax: limitsResp.GlobalMax, GlobalMax: limitsResp.GlobalMax,
Plans: planList, Plans: planList,
BalanceDisabled: cfg.BalanceDisabled, BalanceDisabled: cfg.BalanceDisabled,
HelpText: cfg.HelpText, BalanceRechargeMultiplier: cfg.BalanceRechargeMultiplier,
HelpImageURL: cfg.HelpImageURL, HelpText: cfg.HelpText,
StripePublishableKey: cfg.StripePublishableKey, HelpImageURL: cfg.HelpImageURL,
StripePublishableKey: cfg.StripePublishableKey,
}) })
} }
type checkoutInfoResponse struct { type checkoutInfoResponse struct {
Methods map[string]service.MethodLimits `json:"methods"` Methods map[string]service.MethodLimits `json:"methods"`
GlobalMin float64 `json:"global_min"` GlobalMin float64 `json:"global_min"`
GlobalMax float64 `json:"global_max"` GlobalMax float64 `json:"global_max"`
Plans []checkoutPlan `json:"plans"` Plans []checkoutPlan `json:"plans"`
BalanceDisabled bool `json:"balance_disabled"` BalanceDisabled bool `json:"balance_disabled"`
HelpText string `json:"help_text"` BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"`
HelpImageURL string `json:"help_image_url"` HelpText string `json:"help_text"`
StripePublishableKey string `json:"stripe_publishable_key"` HelpImageURL string `json:"help_image_url"`
StripePublishableKey string `json:"stripe_publishable_key"`
} }
type checkoutPlan struct { type checkoutPlan struct {
...@@ -381,6 +383,7 @@ type PublicOrderResult struct { ...@@ -381,6 +383,7 @@ type PublicOrderResult struct {
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
PayAmount float64 `json:"pay_amount"` PayAmount float64 `json:"pay_amount"`
PaymentType string `json:"payment_type"` PaymentType string `json:"payment_type"`
OrderType string `json:"order_type"`
Status string `json:"status"` Status string `json:"status"`
} }
...@@ -404,6 +407,7 @@ func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) { ...@@ -404,6 +407,7 @@ func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) {
Amount: order.Amount, Amount: order.Amount,
PayAmount: order.PayAmount, PayAmount: order.PayAmount,
PaymentType: order.PaymentType, PaymentType: order.PaymentType,
OrderType: order.OrderType,
Status: order.Status, Status: order.Status,
}) })
} }
......
package service
import (
"math"
"github.com/shopspring/decimal"
)
const defaultBalanceRechargeMultiplier = 1.0
func normalizeBalanceRechargeMultiplier(multiplier float64) float64 {
if math.IsNaN(multiplier) || math.IsInf(multiplier, 0) || multiplier <= 0 {
return defaultBalanceRechargeMultiplier
}
return multiplier
}
func calculateCreditedBalance(paymentAmount, multiplier float64) float64 {
return decimal.NewFromFloat(paymentAmount).
Mul(decimal.NewFromFloat(normalizeBalanceRechargeMultiplier(multiplier))).
Round(2).
InexactFloat64()
}
func calculateGatewayRefundAmount(orderAmount, payAmount, refundAmount float64) float64 {
if orderAmount <= 0 || payAmount <= 0 || refundAmount <= 0 {
return 0
}
if math.Abs(refundAmount-orderAmount) <= amountToleranceCNY {
return decimal.NewFromFloat(payAmount).Round(2).InexactFloat64()
}
return decimal.NewFromFloat(payAmount).
Mul(decimal.NewFromFloat(refundAmount)).
Div(decimal.NewFromFloat(orderAmount)).
Round(2).
InexactFloat64()
}
...@@ -3,12 +3,14 @@ package service ...@@ -3,12 +3,14 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"strconv" "strconv"
"strings" "strings"
dbent "github.com/Wei-Shaw/sub2api/ent" dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance" "github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
"github.com/Wei-Shaw/sub2api/internal/payment" "github.com/Wei-Shaw/sub2api/internal/payment"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
) )
const ( const (
...@@ -21,6 +23,7 @@ const ( ...@@ -21,6 +23,7 @@ const (
SettingEnabledPaymentTypes = "ENABLED_PAYMENT_TYPES" SettingEnabledPaymentTypes = "ENABLED_PAYMENT_TYPES"
SettingLoadBalanceStrategy = "LOAD_BALANCE_STRATEGY" SettingLoadBalanceStrategy = "LOAD_BALANCE_STRATEGY"
SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED" SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED"
SettingBalanceRechargeMult = "BALANCE_RECHARGE_MULTIPLIER"
SettingProductNamePrefix = "PRODUCT_NAME_PREFIX" SettingProductNamePrefix = "PRODUCT_NAME_PREFIX"
SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX" SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX"
SettingHelpImageURL = "PAYMENT_HELP_IMAGE_URL" SettingHelpImageURL = "PAYMENT_HELP_IMAGE_URL"
...@@ -46,9 +49,10 @@ type PaymentConfig struct { ...@@ -46,9 +49,10 @@ type PaymentConfig struct {
DailyLimit float64 `json:"daily_limit"` DailyLimit float64 `json:"daily_limit"`
OrderTimeoutMin int `json:"order_timeout_minutes"` OrderTimeoutMin int `json:"order_timeout_minutes"`
MaxPendingOrders int `json:"max_pending_orders"` MaxPendingOrders int `json:"max_pending_orders"`
EnabledTypes []string `json:"enabled_payment_types"` EnabledTypes []string `json:"enabled_payment_types"`
BalanceDisabled bool `json:"balance_disabled"` BalanceDisabled bool `json:"balance_disabled"`
LoadBalanceStrategy string `json:"load_balance_strategy"` BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"`
LoadBalanceStrategy string `json:"load_balance_strategy"`
ProductNamePrefix string `json:"product_name_prefix"` ProductNamePrefix string `json:"product_name_prefix"`
ProductNameSuffix string `json:"product_name_suffix"` ProductNameSuffix string `json:"product_name_suffix"`
HelpImageURL string `json:"help_image_url"` HelpImageURL string `json:"help_image_url"`
...@@ -71,9 +75,10 @@ type UpdatePaymentConfigRequest struct { ...@@ -71,9 +75,10 @@ type UpdatePaymentConfigRequest struct {
DailyLimit *float64 `json:"daily_limit"` DailyLimit *float64 `json:"daily_limit"`
OrderTimeoutMin *int `json:"order_timeout_minutes"` OrderTimeoutMin *int `json:"order_timeout_minutes"`
MaxPendingOrders *int `json:"max_pending_orders"` MaxPendingOrders *int `json:"max_pending_orders"`
EnabledTypes []string `json:"enabled_payment_types"` EnabledTypes []string `json:"enabled_payment_types"`
BalanceDisabled *bool `json:"balance_disabled"` BalanceDisabled *bool `json:"balance_disabled"`
LoadBalanceStrategy *string `json:"load_balance_strategy"` BalanceRechargeMultiplier *float64 `json:"balance_recharge_multiplier"`
LoadBalanceStrategy *string `json:"load_balance_strategy"`
ProductNamePrefix *string `json:"product_name_prefix"` ProductNamePrefix *string `json:"product_name_prefix"`
ProductNameSuffix *string `json:"product_name_suffix"` ProductNameSuffix *string `json:"product_name_suffix"`
HelpImageURL *string `json:"help_image_url"` HelpImageURL *string `json:"help_image_url"`
...@@ -183,7 +188,7 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo ...@@ -183,7 +188,7 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo
keys := []string{ keys := []string{
SettingPaymentEnabled, SettingMinRechargeAmount, SettingMaxRechargeAmount, SettingPaymentEnabled, SettingMinRechargeAmount, SettingMaxRechargeAmount,
SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders, SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders,
SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingLoadBalanceStrategy, SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingBalanceRechargeMult, SettingLoadBalanceStrategy,
SettingProductNamePrefix, SettingProductNameSuffix, SettingProductNamePrefix, SettingProductNameSuffix,
SettingHelpImageURL, SettingHelpText, SettingHelpImageURL, SettingHelpText,
SettingCancelRateLimitOn, SettingCancelRateLimitMax, SettingCancelRateLimitOn, SettingCancelRateLimitMax,
...@@ -207,8 +212,9 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme ...@@ -207,8 +212,9 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme
DailyLimit: pcParseFloat(vals[SettingDailyRechargeLimit], 0), DailyLimit: pcParseFloat(vals[SettingDailyRechargeLimit], 0),
OrderTimeoutMin: pcParseInt(vals[SettingOrderTimeoutMinutes], defaultOrderTimeoutMin), OrderTimeoutMin: pcParseInt(vals[SettingOrderTimeoutMinutes], defaultOrderTimeoutMin),
MaxPendingOrders: pcParseInt(vals[SettingMaxPendingOrders], defaultMaxPendingOrders), MaxPendingOrders: pcParseInt(vals[SettingMaxPendingOrders], defaultMaxPendingOrders),
BalanceDisabled: vals[SettingBalancePayDisabled] == "true", BalanceDisabled: vals[SettingBalancePayDisabled] == "true",
LoadBalanceStrategy: vals[SettingLoadBalanceStrategy], BalanceRechargeMultiplier: normalizeBalanceRechargeMultiplier(pcParseFloat(vals[SettingBalanceRechargeMult], defaultBalanceRechargeMultiplier)),
LoadBalanceStrategy: vals[SettingLoadBalanceStrategy],
ProductNamePrefix: vals[SettingProductNamePrefix], ProductNamePrefix: vals[SettingProductNamePrefix],
ProductNameSuffix: vals[SettingProductNameSuffix], ProductNameSuffix: vals[SettingProductNameSuffix],
HelpImageURL: vals[SettingHelpImageURL], HelpImageURL: vals[SettingHelpImageURL],
...@@ -256,6 +262,11 @@ func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) stri ...@@ -256,6 +262,11 @@ func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) stri
// nil-check before serialisation — this is inherent to patch-style update patterns // nil-check before serialisation — this is inherent to patch-style update patterns
// and cannot be meaningfully decomposed without introducing unnecessary abstraction. // and cannot be meaningfully decomposed without introducing unnecessary abstraction.
func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req UpdatePaymentConfigRequest) error { func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req UpdatePaymentConfigRequest) error {
if req.BalanceRechargeMultiplier != nil {
if math.IsNaN(*req.BalanceRechargeMultiplier) || math.IsInf(*req.BalanceRechargeMultiplier, 0) || *req.BalanceRechargeMultiplier <= 0 {
return infraerrors.BadRequest("INVALID_BALANCE_RECHARGE_MULTIPLIER", "balance recharge multiplier must be greater than 0")
}
}
m := map[string]string{ m := map[string]string{
SettingPaymentEnabled: formatBoolOrEmpty(req.Enabled), SettingPaymentEnabled: formatBoolOrEmpty(req.Enabled),
SettingMinRechargeAmount: formatPositiveFloat(req.MinAmount), SettingMinRechargeAmount: formatPositiveFloat(req.MinAmount),
...@@ -264,6 +275,7 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda ...@@ -264,6 +275,7 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
SettingOrderTimeoutMinutes: formatPositiveInt(req.OrderTimeoutMin), SettingOrderTimeoutMinutes: formatPositiveInt(req.OrderTimeoutMin),
SettingMaxPendingOrders: formatPositiveInt(req.MaxPendingOrders), SettingMaxPendingOrders: formatPositiveInt(req.MaxPendingOrders),
SettingBalancePayDisabled: formatBoolOrEmpty(req.BalanceDisabled), SettingBalancePayDisabled: formatBoolOrEmpty(req.BalanceDisabled),
SettingBalanceRechargeMult: formatPositiveFloat(req.BalanceRechargeMultiplier),
SettingLoadBalanceStrategy: derefStr(req.LoadBalanceStrategy), SettingLoadBalanceStrategy: derefStr(req.LoadBalanceStrategy),
SettingProductNamePrefix: derefStr(req.ProductNamePrefix), SettingProductNamePrefix: derefStr(req.ProductNamePrefix),
SettingProductNameSuffix: derefStr(req.ProductNameSuffix), SettingProductNameSuffix: derefStr(req.ProductNameSuffix),
......
...@@ -216,7 +216,11 @@ func (s *PaymentService) markCompleted(ctx context.Context, o *dbent.PaymentOrde ...@@ -216,7 +216,11 @@ func (s *PaymentService) markCompleted(ctx context.Context, o *dbent.PaymentOrde
if err != nil { if err != nil {
return fmt.Errorf("mark completed: %w", err) return fmt.Errorf("mark completed: %w", err)
} }
s.writeAuditLog(ctx, o.ID, auditAction, "system", map[string]any{"rechargeCode": o.RechargeCode, "amount": o.Amount}) s.writeAuditLog(ctx, o.ID, auditAction, "system", map[string]any{
"rechargeCode": o.RechargeCode,
"creditedAmount": o.Amount,
"payAmount": o.PayAmount,
})
return nil return nil
} }
......
...@@ -43,14 +43,18 @@ func (s *PaymentService) CreateOrder(ctx context.Context, req CreateOrderRequest ...@@ -43,14 +43,18 @@ func (s *PaymentService) CreateOrder(ctx context.Context, req CreateOrderRequest
if user.Status != payment.EntityStatusActive { if user.Status != payment.EntityStatusActive {
return nil, infraerrors.Forbidden("USER_INACTIVE", "user account is disabled") return nil, infraerrors.Forbidden("USER_INACTIVE", "user account is disabled")
} }
amount := req.Amount orderAmount := req.Amount
limitAmount := req.Amount
if plan != nil { if plan != nil {
amount = plan.Price orderAmount = plan.Price
limitAmount = plan.Price
} else if req.OrderType == payment.OrderTypeBalance {
orderAmount = calculateCreditedBalance(req.Amount, cfg.BalanceRechargeMultiplier)
} }
feeRate := s.getFeeRate(req.PaymentType) feeRate := s.getFeeRate(req.PaymentType)
payAmountStr := payment.CalculatePayAmount(amount, feeRate) payAmountStr := payment.CalculatePayAmount(limitAmount, feeRate)
payAmount, _ := strconv.ParseFloat(payAmountStr, 64) payAmount, _ := strconv.ParseFloat(payAmountStr, 64)
order, err := s.createOrderInTx(ctx, req, user, plan, cfg, amount, feeRate, payAmount) order, err := s.createOrderInTx(ctx, req, user, plan, cfg, orderAmount, limitAmount, feeRate, payAmount)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -99,7 +103,7 @@ func (s *PaymentService) validateSubOrder(ctx context.Context, req CreateOrderRe ...@@ -99,7 +103,7 @@ func (s *PaymentService) validateSubOrder(ctx context.Context, req CreateOrderRe
return plan, nil return plan, nil
} }
func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderRequest, user *User, plan *dbent.SubscriptionPlan, cfg *PaymentConfig, amount, feeRate, payAmount float64) (*dbent.PaymentOrder, error) { func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderRequest, user *User, plan *dbent.SubscriptionPlan, cfg *PaymentConfig, orderAmount, limitAmount, feeRate, payAmount float64) (*dbent.PaymentOrder, error) {
tx, err := s.entClient.Tx(ctx) tx, err := s.entClient.Tx(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("begin transaction: %w", err) return nil, fmt.Errorf("begin transaction: %w", err)
...@@ -108,7 +112,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq ...@@ -108,7 +112,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq
if err := s.checkPendingLimit(ctx, tx, req.UserID, cfg.MaxPendingOrders); err != nil { if err := s.checkPendingLimit(ctx, tx, req.UserID, cfg.MaxPendingOrders); err != nil {
return nil, err return nil, err
} }
if err := s.checkDailyLimit(ctx, tx, req.UserID, amount, cfg.DailyLimit); err != nil { if err := s.checkDailyLimit(ctx, tx, req.UserID, limitAmount, cfg.DailyLimit); err != nil {
return nil, err return nil, err
} }
tm := cfg.OrderTimeoutMin tm := cfg.OrderTimeoutMin
...@@ -121,7 +125,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq ...@@ -121,7 +125,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq
SetUserEmail(user.Email). SetUserEmail(user.Email).
SetUserName(user.Username). SetUserName(user.Username).
SetNillableUserNotes(psNilIfEmpty(user.Notes)). SetNillableUserNotes(psNilIfEmpty(user.Notes)).
SetAmount(amount). SetAmount(orderAmount).
SetPayAmount(payAmount). SetPayAmount(payAmount).
SetFeeRate(feeRate). SetFeeRate(feeRate).
SetRechargeCode(""). SetRechargeCode("").
...@@ -180,6 +184,10 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user ...@@ -180,6 +184,10 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user
} }
var used float64 var used float64
for _, o := range orders { for _, o := range orders {
if o.OrderType == payment.OrderTypeBalance {
used += o.PayAmount
continue
}
used += o.Amount used += o.Amount
} }
if used+amount > limit { if used+amount > limit {
...@@ -213,7 +221,13 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen ...@@ -213,7 +221,13 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen
if err != nil { if err != nil {
return nil, fmt.Errorf("update order with payment details: %w", err) return nil, fmt.Errorf("update order with payment details: %w", err)
} }
s.writeAuditLog(ctx, order.ID, "ORDER_CREATED", fmt.Sprintf("user:%d", req.UserID), map[string]any{"amount": req.Amount, "paymentType": req.PaymentType, "orderType": req.OrderType}) s.writeAuditLog(ctx, order.ID, "ORDER_CREATED", fmt.Sprintf("user:%d", req.UserID), map[string]any{
"paymentAmount": req.Amount,
"creditedAmount": order.Amount,
"payAmount": order.PayAmount,
"paymentType": req.PaymentType,
"orderType": req.OrderType,
})
return &CreateOrderResponse{OrderID: order.ID, Amount: order.Amount, PayAmount: payAmount, FeeRate: order.FeeRate, Status: OrderStatusPending, PaymentType: req.PaymentType, PayURL: pr.PayURL, QRCode: pr.QRCode, ClientSecret: pr.ClientSecret, ExpiresAt: order.ExpiresAt, PaymentMode: sel.PaymentMode}, nil return &CreateOrderResponse{OrderID: order.ID, Amount: order.Amount, PayAmount: payAmount, FeeRate: order.FeeRate, Status: OrderStatusPending, PaymentType: req.PaymentType, PayURL: pr.PayURL, QRCode: pr.QRCode, ClientSecret: pr.ClientSecret, ExpiresAt: order.ExpiresAt, PaymentMode: sel.PaymentMode}, nil
} }
......
...@@ -113,11 +113,7 @@ func (s *PaymentService) PrepareRefund(ctx context.Context, oid int64, amt float ...@@ -113,11 +113,7 @@ func (s *PaymentService) PrepareRefund(ctx context.Context, oid int64, amt float
if amt-o.Amount > amountToleranceCNY { if amt-o.Amount > amountToleranceCNY {
return nil, nil, infraerrors.BadRequest("REFUND_AMOUNT_EXCEEDED", "refund amount exceeds recharge") return nil, nil, infraerrors.BadRequest("REFUND_AMOUNT_EXCEEDED", "refund amount exceeds recharge")
} }
// Full refund: use actual pay_amount for gateway (includes fees) ga := calculateGatewayRefundAmount(o.Amount, o.PayAmount, amt)
ga := amt
if math.Abs(amt-o.Amount) <= amountToleranceCNY {
ga = o.PayAmount
}
rr := strings.TrimSpace(reason) rr := strings.TrimSpace(reason)
if rr == "" && o.RefundRequestReason != nil { if rr == "" && o.RefundRequestReason != nil {
rr = *o.RefundRequestReason rr = *o.RefundRequestReason
......
...@@ -23,6 +23,7 @@ export interface AdminPaymentConfig { ...@@ -23,6 +23,7 @@ export interface AdminPaymentConfig {
max_pending_orders: number max_pending_orders: number
enabled_payment_types: string[] enabled_payment_types: string[]
balance_disabled: boolean balance_disabled: boolean
balance_recharge_multiplier: number
load_balance_strategy: string load_balance_strategy: string
product_name_prefix: string product_name_prefix: string
product_name_suffix: string product_name_suffix: string
...@@ -40,6 +41,7 @@ export interface UpdatePaymentConfigRequest { ...@@ -40,6 +41,7 @@ export interface UpdatePaymentConfigRequest {
max_pending_orders?: number max_pending_orders?: number
enabled_payment_types?: string[] enabled_payment_types?: string[]
balance_disabled?: boolean balance_disabled?: boolean
balance_recharge_multiplier?: number
load_balance_strategy?: string load_balance_strategy?: string
product_name_prefix?: string product_name_prefix?: string
product_name_suffix?: string product_name_suffix?: string
......
...@@ -125,6 +125,7 @@ export interface SystemSettings { ...@@ -125,6 +125,7 @@ export interface SystemSettings {
payment_max_pending_orders: number payment_max_pending_orders: number
payment_enabled_types: string[] payment_enabled_types: string[]
payment_balance_disabled: boolean payment_balance_disabled: boolean
payment_balance_recharge_multiplier: number
payment_load_balance_strategy: string payment_load_balance_strategy: string
payment_product_name_prefix: string payment_product_name_prefix: string
payment_product_name_suffix: string payment_product_name_suffix: string
...@@ -231,6 +232,7 @@ export interface UpdateSettingsRequest { ...@@ -231,6 +232,7 @@ export interface UpdateSettingsRequest {
payment_max_pending_orders?: number payment_max_pending_orders?: number
payment_enabled_types?: string[] payment_enabled_types?: string[]
payment_balance_disabled?: boolean payment_balance_disabled?: boolean
payment_balance_recharge_multiplier?: number
payment_load_balance_strategy?: string payment_load_balance_strategy?: string
payment_product_name_prefix?: string payment_product_name_prefix?: string
payment_product_name_suffix?: string payment_product_name_suffix?: string
......
...@@ -19,11 +19,11 @@ ...@@ -19,11 +19,11 @@
</div> </div>
<div> <div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</p> <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">${{ order.amount.toFixed(2) }}</p> <p class="text-sm font-medium text-gray-900 dark:text-white">{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.amount.toFixed(2) }}</p>
</div> </div>
<div> <div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</p> <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">¥{{ order.pay_amount.toFixed(2) }}</p>
</div> </div>
<div> <div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.paymentMethod') }}</p> <p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.paymentMethod') }}</p>
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
<div class="grid grid-cols-2 gap-2 text-sm"> <div class="grid grid-cols-2 gap-2 text-sm">
<div> <div>
<span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundAmount') }}:</span> <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.refund_amount.toFixed(2) }}</span> <span class="ml-1 font-medium text-red-700 dark:text-red-300">{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.refund_amount.toFixed(2) }}</span>
</div> </div>
<div v-if="order.refund_reason" class="col-span-2"> <div v-if="order.refund_reason" class="col-span-2">
<span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundReason') }}:</span> <span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundReason') }}:</span>
......
...@@ -53,9 +53,9 @@ ...@@ -53,9 +53,9 @@
<template #cell-amount="{ value, row }"> <template #cell-amount="{ value, row }">
<div class="text-sm"> <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">{{ row.order_type === 'balance' ? '$' : '¥' }}{{ value.toFixed(2) }}</span>
<span v-if="row.pay_amount !== value" class="ml-1 text-xs text-gray-500"> <span v-if="row.pay_amount !== value" class="ml-1 text-xs text-gray-500">
({{ t('payment.orders.payAmount') }}: ${{ row.pay_amount.toFixed(2) }}) ({{ t('payment.orders.payAmount') }}: ¥{{ row.pay_amount.toFixed(2) }})
</span> </span>
</div> </div>
</template> </template>
......
...@@ -35,11 +35,15 @@ ...@@ -35,11 +35,15 @@
</div> </div>
<div class="mt-1 flex justify-between text-sm"> <div class="mt-1 flex justify-between text-sm">
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</span> <span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</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">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ 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>
</div> </div>
<div v-if="actuallyRefunded > 0" class="mt-1 flex justify-between text-sm"> <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="text-gray-500 dark:text-gray-400">{{ t('payment.admin.alreadyRefunded') }}</span>
<span class="font-medium text-red-600 dark:text-red-400">${{ actuallyRefunded.toFixed(2) }}</span> <span class="font-medium text-red-600 dark:text-red-400">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ actuallyRefunded.toFixed(2) }}</span>
</div> </div>
</div> </div>
...@@ -66,7 +70,7 @@ ...@@ -66,7 +70,7 @@
</div> </div>
<div class="rounded-lg bg-gray-50 p-3 text-sm dark:bg-dark-700"> <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="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?.pay_amount?.toFixed(2) }}</div> <div class="mt-1 font-semibold text-gray-900 dark:text-white">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ order?.amount?.toFixed(2) }}</div>
</div> </div>
</div> </div>
...@@ -91,7 +95,7 @@ ...@@ -91,7 +95,7 @@
<div> <div>
<label class="input-label">{{ t('payment.admin.refundAmount') }}</label> <label class="input-label">{{ t('payment.admin.refundAmount') }}</label>
<div class="relative"> <div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">$</span> <span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">{{ order?.order_type === 'balance' ? '$' : '¥' }}</span>
<input <input
v-model.number="form.amount" v-model.number="form.amount"
type="number" type="number"
...@@ -103,7 +107,7 @@ ...@@ -103,7 +107,7 @@
/> />
</div> </div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('payment.admin.maxRefundable') }}: ${{ maxRefundable.toFixed(2) }} {{ t('payment.admin.maxRefundable') }}: {{ order?.order_type === 'balance' ? '$' : '¥' }}{{ maxRefundable.toFixed(2) }}
</p> </p>
</div> </div>
...@@ -200,12 +204,12 @@ const actuallyRefunded = computed(() => { ...@@ -200,12 +204,12 @@ const actuallyRefunded = computed(() => {
const maxRefundable = computed(() => { const maxRefundable = computed(() => {
if (!props.order) return 0 if (!props.order) return 0
return props.order.pay_amount - actuallyRefunded.value return props.order.amount - actuallyRefunded.value
}) })
const balanceInsufficient = computed(() => { const balanceInsufficient = computed(() => {
if (props.userBalance == null || !props.order) return false if (props.userBalance == null || !props.order) return false
return props.userBalance < props.order.pay_amount return props.userBalance < props.order.amount
}) })
watch(() => props.show, (val) => { watch(() => props.show, (val) => {
......
...@@ -14,8 +14,8 @@ ...@@ -14,8 +14,8 @@
</template> </template>
<template #cell-amount="{ value, row }"> <template #cell-amount="{ value, row }">
<div class="text-sm"> <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">{{ row.order_type === 'balance' ? '$' : '¥' }}{{ value.toFixed(2) }}</span>
<span v-if="row.pay_amount !== value" class="ml-1 text-xs text-gray-500">(${{ row.pay_amount.toFixed(2) }})</span> <span v-if="row.pay_amount !== value" class="ml-1 text-xs text-gray-500">(¥{{ row.pay_amount.toFixed(2) }})</span>
</div> </div>
</template> </template>
<template #cell-payment_type="{ value }"> <template #cell-payment_type="{ value }">
......
...@@ -45,7 +45,11 @@ ...@@ -45,7 +45,11 @@
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</span> <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.pay_amount.toFixed(2) }}</span> <span class="font-medium text-gray-900 dark:text-white">{{ paidOrder.order_type === 'balance' ? '$' : '¥' }}{{ 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>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -22,7 +22,11 @@ ...@@ -22,7 +22,11 @@
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</span> <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.pay_amount.toFixed(2) }}</span> <span class="font-medium text-gray-900 dark:text-white">{{ paidOrder.order_type === 'balance' ? '$' : '¥' }}{{ 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>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -21,9 +21,13 @@ ...@@ -21,9 +21,13 @@
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.orderId') }}</span> <span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.orderId') }}</span>
<span class="font-medium text-gray-900 dark:text-white">#{{ orderId }}</span> <span class="font-medium text-gray-900 dark:text-white">#{{ orderId }}</span>
</div> </div>
<div class="flex justify-between"> <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="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</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">{{ orderType === 'balance' ? '$' : '¥' }}{{ 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>
</div> </div>
</div> </div>
</div> </div>
...@@ -36,7 +40,7 @@ ...@@ -36,7 +40,7 @@
<div class="card overflow-hidden"> <div class="card overflow-hidden">
<div class="bg-gradient-to-br from-[#635bff] to-[#4f46e5] px-6 py-5 text-center"> <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="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">¥{{ payAmount.toFixed(2) }}</p>
</div> </div>
</div> </div>
<!-- Stripe Payment Element --> <!-- Stripe Payment Element -->
...@@ -75,7 +79,9 @@ const POPUP_METHODS = new Set(['alipay', 'wechat_pay']) ...@@ -75,7 +79,9 @@ const POPUP_METHODS = new Set(['alipay', 'wechat_pay'])
const props = defineProps<{ const props = defineProps<{
orderId: number orderId: number
amount: number
clientSecret: string clientSecret: string
orderType?: 'balance' | 'subscription'
publishableKey: string publishableKey: string
payAmount: number payAmount: number
}>() }>()
......
...@@ -4566,6 +4566,9 @@ export default { ...@@ -4566,6 +4566,9 @@ export default {
minAmount: 'Minimum Amount', minAmount: 'Minimum Amount',
maxAmount: 'Maximum Amount', maxAmount: 'Maximum Amount',
dailyLimit: 'Daily Limit', dailyLimit: 'Daily Limit',
balanceRechargeMultiplier: 'Balance Recharge Multiplier',
balanceRechargeMultiplierHint: 'How many USD balance the user receives for each 1 CNY paid',
balanceRechargePreview: 'Preview: 1 CNY = {usd} USD',
orderTimeout: 'Order Timeout', orderTimeout: 'Order Timeout',
orderTimeoutHint: 'In minutes, minimum 1', orderTimeoutHint: 'In minutes, minimum 1',
maxPendingOrders: 'Max Pending Orders', maxPendingOrders: 'Max Pending Orders',
...@@ -5324,6 +5327,8 @@ export default { ...@@ -5324,6 +5327,8 @@ export default {
payment: { payment: {
title: 'Recharge / Subscription', title: 'Recharge / Subscription',
amountLabel: 'Amount', amountLabel: 'Amount',
paymentAmount: 'Payment Amount',
creditedBalance: 'Credited Balance',
quickAmounts: 'Quick Amounts', quickAmounts: 'Quick Amounts',
customAmount: 'Custom Amount', customAmount: 'Custom Amount',
enterAmount: 'Enter amount', enterAmount: 'Enter amount',
...@@ -5408,6 +5413,7 @@ export default { ...@@ -5408,6 +5413,7 @@ export default {
amountTooLow: 'Minimum amount is {min}', amountTooLow: 'Minimum amount is {min}',
amountTooHigh: 'Maximum amount is {max}', amountTooHigh: 'Maximum amount is {max}',
amountNoMethod: 'No payment method available for this amount', amountNoMethod: 'No payment method available for this amount',
rechargeRatePreview: 'Current rate: 1 CNY = {usd} USD',
refundReason: 'Refund Reason', refundReason: 'Refund Reason',
refundReasonPlaceholder: 'Please describe your refund reason', refundReasonPlaceholder: 'Please describe your refund reason',
stripeLoadFailed: 'Failed to load payment component. Please refresh and try again.', stripeLoadFailed: 'Failed to load payment component. Please refresh and try again.',
......
...@@ -4730,6 +4730,9 @@ export default { ...@@ -4730,6 +4730,9 @@ export default {
minAmount: '最低金额', minAmount: '最低金额',
maxAmount: '最高金额', maxAmount: '最高金额',
dailyLimit: '每日限额', dailyLimit: '每日限额',
balanceRechargeMultiplier: '余额充值倍率',
balanceRechargeMultiplierHint: '用户每支付 1 CNY 可获得多少 USD 余额',
balanceRechargePreview: '预览:1 CNY = {usd} USD',
orderTimeout: '订单超时时间', orderTimeout: '订单超时时间',
orderTimeoutHint: '单位:分钟,至少 1 分钟', orderTimeoutHint: '单位:分钟,至少 1 分钟',
maxPendingOrders: '最大待支付订单数', maxPendingOrders: '最大待支付订单数',
...@@ -5512,6 +5515,8 @@ export default { ...@@ -5512,6 +5515,8 @@ export default {
payment: { payment: {
title: '充值/订阅', title: '充值/订阅',
amountLabel: '充值金额', amountLabel: '充值金额',
paymentAmount: '支付金额',
creditedBalance: '到账余额',
quickAmounts: '快捷金额', quickAmounts: '快捷金额',
customAmount: '自定义金额', customAmount: '自定义金额',
enterAmount: '输入金额', enterAmount: '输入金额',
...@@ -5596,6 +5601,7 @@ export default { ...@@ -5596,6 +5601,7 @@ export default {
amountTooLow: '最低金额为 {min}', amountTooLow: '最低金额为 {min}',
amountTooHigh: '最高金额为 {max}', amountTooHigh: '最高金额为 {max}',
amountNoMethod: '该金额没有可用的支付方式', amountNoMethod: '该金额没有可用的支付方式',
rechargeRatePreview: '当前倍率:1 CNY = {usd} USD',
refundReason: '退款原因', refundReason: '退款原因',
refundReasonPlaceholder: '请描述您的退款原因', refundReasonPlaceholder: '请描述您的退款原因',
stripeLoadFailed: '支付组件加载失败,请刷新页面重试', stripeLoadFailed: '支付组件加载失败,请刷新页面重试',
......
...@@ -32,6 +32,7 @@ export interface PaymentConfig { ...@@ -32,6 +32,7 @@ export interface PaymentConfig {
max_pending_orders: number max_pending_orders: number
order_timeout_minutes: number order_timeout_minutes: number
balance_disabled: boolean balance_disabled: boolean
balance_recharge_multiplier: number
enabled_payment_types: PaymentType[] enabled_payment_types: PaymentType[]
help_image_url: string help_image_url: string
help_text: string help_text: string
...@@ -62,6 +63,7 @@ export interface CheckoutInfoResponse { ...@@ -62,6 +63,7 @@ export interface CheckoutInfoResponse {
global_max: number global_max: number
plans: SubscriptionPlan[] plans: SubscriptionPlan[]
balance_disabled: boolean balance_disabled: boolean
balance_recharge_multiplier: number
help_text: string help_text: string
help_image_url: string help_image_url: string
stripe_publishable_key: string stripe_publishable_key: string
...@@ -155,6 +157,7 @@ export interface CreateOrderRequest { ...@@ -155,6 +157,7 @@ export interface CreateOrderRequest {
export interface CreateOrderResult { export interface CreateOrderResult {
order_id: number order_id: number
amount: number
pay_url?: string pay_url?: string
qr_code?: string qr_code?: string
client_secret?: string client_secret?: string
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment