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
This commit is contained in:
@@ -188,6 +188,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders,
|
||||
PaymentEnabledTypes: paymentCfg.EnabledTypes,
|
||||
PaymentBalanceDisabled: paymentCfg.BalanceDisabled,
|
||||
PaymentBalanceRechargeMultiplier: paymentCfg.BalanceRechargeMultiplier,
|
||||
PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy,
|
||||
PaymentProductNamePrefix: paymentCfg.ProductNamePrefix,
|
||||
PaymentProductNameSuffix: paymentCfg.ProductNameSuffix,
|
||||
@@ -323,9 +324,10 @@ type UpdateSettingsRequest struct {
|
||||
PaymentDailyLimit *float64 `json:"payment_daily_limit"`
|
||||
PaymentOrderTimeoutMin *int `json:"payment_order_timeout_minutes"`
|
||||
PaymentMaxPendingOrders *int `json:"payment_max_pending_orders"`
|
||||
PaymentEnabledTypes []string `json:"payment_enabled_types"`
|
||||
PaymentBalanceDisabled *bool `json:"payment_balance_disabled"`
|
||||
PaymentLoadBalanceStrat *string `json:"payment_load_balance_strategy"`
|
||||
PaymentEnabledTypes []string `json:"payment_enabled_types"`
|
||||
PaymentBalanceDisabled *bool `json:"payment_balance_disabled"`
|
||||
PaymentBalanceRechargeMultiplier *float64 `json:"payment_balance_recharge_multiplier"`
|
||||
PaymentLoadBalanceStrat *string `json:"payment_load_balance_strategy"`
|
||||
PaymentProductNamePrefix *string `json:"payment_product_name_prefix"`
|
||||
PaymentProductNameSuffix *string `json:"payment_product_name_suffix"`
|
||||
PaymentHelpImageURL *string `json:"payment_help_image_url"`
|
||||
@@ -934,24 +936,25 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
// Skip if no payment fields were provided (prevents accidental wipe).
|
||||
if h.paymentConfigService != nil && hasPaymentFields(req) {
|
||||
paymentReq := service.UpdatePaymentConfigRequest{
|
||||
Enabled: req.PaymentEnabled,
|
||||
MinAmount: req.PaymentMinAmount,
|
||||
MaxAmount: req.PaymentMaxAmount,
|
||||
DailyLimit: req.PaymentDailyLimit,
|
||||
OrderTimeoutMin: req.PaymentOrderTimeoutMin,
|
||||
MaxPendingOrders: req.PaymentMaxPendingOrders,
|
||||
EnabledTypes: req.PaymentEnabledTypes,
|
||||
BalanceDisabled: req.PaymentBalanceDisabled,
|
||||
LoadBalanceStrategy: req.PaymentLoadBalanceStrat,
|
||||
ProductNamePrefix: req.PaymentProductNamePrefix,
|
||||
ProductNameSuffix: req.PaymentProductNameSuffix,
|
||||
HelpImageURL: req.PaymentHelpImageURL,
|
||||
HelpText: req.PaymentHelpText,
|
||||
CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled,
|
||||
CancelRateLimitMax: req.PaymentCancelRateLimitMax,
|
||||
CancelRateLimitWindow: req.PaymentCancelRateLimitWindow,
|
||||
CancelRateLimitUnit: req.PaymentCancelRateLimitUnit,
|
||||
CancelRateLimitMode: req.PaymentCancelRateLimitMode,
|
||||
Enabled: req.PaymentEnabled,
|
||||
MinAmount: req.PaymentMinAmount,
|
||||
MaxAmount: req.PaymentMaxAmount,
|
||||
DailyLimit: req.PaymentDailyLimit,
|
||||
OrderTimeoutMin: req.PaymentOrderTimeoutMin,
|
||||
MaxPendingOrders: req.PaymentMaxPendingOrders,
|
||||
EnabledTypes: req.PaymentEnabledTypes,
|
||||
BalanceDisabled: req.PaymentBalanceDisabled,
|
||||
BalanceRechargeMultiplier: req.PaymentBalanceRechargeMultiplier,
|
||||
LoadBalanceStrategy: req.PaymentLoadBalanceStrat,
|
||||
ProductNamePrefix: req.PaymentProductNamePrefix,
|
||||
ProductNameSuffix: req.PaymentProductNameSuffix,
|
||||
HelpImageURL: req.PaymentHelpImageURL,
|
||||
HelpText: req.PaymentHelpText,
|
||||
CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled,
|
||||
CancelRateLimitMax: req.PaymentCancelRateLimitMax,
|
||||
CancelRateLimitWindow: req.PaymentCancelRateLimitWindow,
|
||||
CancelRateLimitUnit: req.PaymentCancelRateLimitUnit,
|
||||
CancelRateLimitMode: req.PaymentCancelRateLimitMode,
|
||||
}
|
||||
if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
@@ -1082,6 +1085,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders,
|
||||
PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes,
|
||||
PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled,
|
||||
PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier,
|
||||
PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy,
|
||||
PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix,
|
||||
PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix,
|
||||
@@ -1101,6 +1105,7 @@ func hasPaymentFields(req UpdateSettingsRequest) bool {
|
||||
req.PaymentMaxAmount != nil || req.PaymentDailyLimit != nil ||
|
||||
req.PaymentOrderTimeoutMin != nil || req.PaymentMaxPendingOrders != nil ||
|
||||
req.PaymentEnabledTypes != nil || req.PaymentBalanceDisabled != nil ||
|
||||
req.PaymentBalanceRechargeMultiplier != nil ||
|
||||
req.PaymentLoadBalanceStrat != nil || req.PaymentProductNamePrefix != nil ||
|
||||
req.PaymentProductNameSuffix != nil || req.PaymentHelpImageURL != nil ||
|
||||
req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil ||
|
||||
|
||||
@@ -134,9 +134,10 @@ type SystemSettings struct {
|
||||
PaymentDailyLimit float64 `json:"payment_daily_limit"`
|
||||
PaymentOrderTimeoutMin int `json:"payment_order_timeout_minutes"`
|
||||
PaymentMaxPendingOrders int `json:"payment_max_pending_orders"`
|
||||
PaymentEnabledTypes []string `json:"payment_enabled_types"`
|
||||
PaymentBalanceDisabled bool `json:"payment_balance_disabled"`
|
||||
PaymentLoadBalanceStrat string `json:"payment_load_balance_strategy"`
|
||||
PaymentEnabledTypes []string `json:"payment_enabled_types"`
|
||||
PaymentBalanceDisabled bool `json:"payment_balance_disabled"`
|
||||
PaymentBalanceRechargeMultiplier float64 `json:"payment_balance_recharge_multiplier"`
|
||||
PaymentLoadBalanceStrat string `json:"payment_load_balance_strategy"`
|
||||
PaymentProductNamePrefix string `json:"payment_product_name_prefix"`
|
||||
PaymentProductNameSuffix string `json:"payment_product_name_suffix"`
|
||||
PaymentHelpImageURL string `json:"payment_help_image_url"`
|
||||
|
||||
@@ -126,26 +126,28 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
|
||||
}
|
||||
|
||||
response.Success(c, checkoutInfoResponse{
|
||||
Methods: limitsResp.Methods,
|
||||
GlobalMin: limitsResp.GlobalMin,
|
||||
GlobalMax: limitsResp.GlobalMax,
|
||||
Plans: planList,
|
||||
BalanceDisabled: cfg.BalanceDisabled,
|
||||
HelpText: cfg.HelpText,
|
||||
HelpImageURL: cfg.HelpImageURL,
|
||||
StripePublishableKey: cfg.StripePublishableKey,
|
||||
Methods: limitsResp.Methods,
|
||||
GlobalMin: limitsResp.GlobalMin,
|
||||
GlobalMax: limitsResp.GlobalMax,
|
||||
Plans: planList,
|
||||
BalanceDisabled: cfg.BalanceDisabled,
|
||||
BalanceRechargeMultiplier: cfg.BalanceRechargeMultiplier,
|
||||
HelpText: cfg.HelpText,
|
||||
HelpImageURL: cfg.HelpImageURL,
|
||||
StripePublishableKey: cfg.StripePublishableKey,
|
||||
})
|
||||
}
|
||||
|
||||
type checkoutInfoResponse struct {
|
||||
Methods map[string]service.MethodLimits `json:"methods"`
|
||||
GlobalMin float64 `json:"global_min"`
|
||||
GlobalMax float64 `json:"global_max"`
|
||||
Plans []checkoutPlan `json:"plans"`
|
||||
BalanceDisabled bool `json:"balance_disabled"`
|
||||
HelpText string `json:"help_text"`
|
||||
HelpImageURL string `json:"help_image_url"`
|
||||
StripePublishableKey string `json:"stripe_publishable_key"`
|
||||
Methods map[string]service.MethodLimits `json:"methods"`
|
||||
GlobalMin float64 `json:"global_min"`
|
||||
GlobalMax float64 `json:"global_max"`
|
||||
Plans []checkoutPlan `json:"plans"`
|
||||
BalanceDisabled bool `json:"balance_disabled"`
|
||||
BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"`
|
||||
HelpText string `json:"help_text"`
|
||||
HelpImageURL string `json:"help_image_url"`
|
||||
StripePublishableKey string `json:"stripe_publishable_key"`
|
||||
}
|
||||
|
||||
type checkoutPlan struct {
|
||||
@@ -381,6 +383,7 @@ type PublicOrderResult struct {
|
||||
Amount float64 `json:"amount"`
|
||||
PayAmount float64 `json:"pay_amount"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
OrderType string `json:"order_type"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
@@ -404,6 +407,7 @@ func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) {
|
||||
Amount: order.Amount,
|
||||
PayAmount: order.PayAmount,
|
||||
PaymentType: order.PaymentType,
|
||||
OrderType: order.OrderType,
|
||||
Status: order.Status,
|
||||
})
|
||||
}
|
||||
|
||||
37
backend/internal/service/payment_amounts.go
Normal file
37
backend/internal/service/payment_amounts.go
Normal file
@@ -0,0 +1,37 @@
|
||||
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
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,6 +23,7 @@ const (
|
||||
SettingEnabledPaymentTypes = "ENABLED_PAYMENT_TYPES"
|
||||
SettingLoadBalanceStrategy = "LOAD_BALANCE_STRATEGY"
|
||||
SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED"
|
||||
SettingBalanceRechargeMult = "BALANCE_RECHARGE_MULTIPLIER"
|
||||
SettingProductNamePrefix = "PRODUCT_NAME_PREFIX"
|
||||
SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX"
|
||||
SettingHelpImageURL = "PAYMENT_HELP_IMAGE_URL"
|
||||
@@ -46,9 +49,10 @@ type PaymentConfig struct {
|
||||
DailyLimit float64 `json:"daily_limit"`
|
||||
OrderTimeoutMin int `json:"order_timeout_minutes"`
|
||||
MaxPendingOrders int `json:"max_pending_orders"`
|
||||
EnabledTypes []string `json:"enabled_payment_types"`
|
||||
BalanceDisabled bool `json:"balance_disabled"`
|
||||
LoadBalanceStrategy string `json:"load_balance_strategy"`
|
||||
EnabledTypes []string `json:"enabled_payment_types"`
|
||||
BalanceDisabled bool `json:"balance_disabled"`
|
||||
BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"`
|
||||
LoadBalanceStrategy string `json:"load_balance_strategy"`
|
||||
ProductNamePrefix string `json:"product_name_prefix"`
|
||||
ProductNameSuffix string `json:"product_name_suffix"`
|
||||
HelpImageURL string `json:"help_image_url"`
|
||||
@@ -71,9 +75,10 @@ type UpdatePaymentConfigRequest struct {
|
||||
DailyLimit *float64 `json:"daily_limit"`
|
||||
OrderTimeoutMin *int `json:"order_timeout_minutes"`
|
||||
MaxPendingOrders *int `json:"max_pending_orders"`
|
||||
EnabledTypes []string `json:"enabled_payment_types"`
|
||||
BalanceDisabled *bool `json:"balance_disabled"`
|
||||
LoadBalanceStrategy *string `json:"load_balance_strategy"`
|
||||
EnabledTypes []string `json:"enabled_payment_types"`
|
||||
BalanceDisabled *bool `json:"balance_disabled"`
|
||||
BalanceRechargeMultiplier *float64 `json:"balance_recharge_multiplier"`
|
||||
LoadBalanceStrategy *string `json:"load_balance_strategy"`
|
||||
ProductNamePrefix *string `json:"product_name_prefix"`
|
||||
ProductNameSuffix *string `json:"product_name_suffix"`
|
||||
HelpImageURL *string `json:"help_image_url"`
|
||||
@@ -183,7 +188,7 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo
|
||||
keys := []string{
|
||||
SettingPaymentEnabled, SettingMinRechargeAmount, SettingMaxRechargeAmount,
|
||||
SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders,
|
||||
SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingLoadBalanceStrategy,
|
||||
SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingBalanceRechargeMult, SettingLoadBalanceStrategy,
|
||||
SettingProductNamePrefix, SettingProductNameSuffix,
|
||||
SettingHelpImageURL, SettingHelpText,
|
||||
SettingCancelRateLimitOn, SettingCancelRateLimitMax,
|
||||
@@ -207,8 +212,9 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme
|
||||
DailyLimit: pcParseFloat(vals[SettingDailyRechargeLimit], 0),
|
||||
OrderTimeoutMin: pcParseInt(vals[SettingOrderTimeoutMinutes], defaultOrderTimeoutMin),
|
||||
MaxPendingOrders: pcParseInt(vals[SettingMaxPendingOrders], defaultMaxPendingOrders),
|
||||
BalanceDisabled: vals[SettingBalancePayDisabled] == "true",
|
||||
LoadBalanceStrategy: vals[SettingLoadBalanceStrategy],
|
||||
BalanceDisabled: vals[SettingBalancePayDisabled] == "true",
|
||||
BalanceRechargeMultiplier: normalizeBalanceRechargeMultiplier(pcParseFloat(vals[SettingBalanceRechargeMult], defaultBalanceRechargeMultiplier)),
|
||||
LoadBalanceStrategy: vals[SettingLoadBalanceStrategy],
|
||||
ProductNamePrefix: vals[SettingProductNamePrefix],
|
||||
ProductNameSuffix: vals[SettingProductNameSuffix],
|
||||
HelpImageURL: vals[SettingHelpImageURL],
|
||||
@@ -256,6 +262,11 @@ func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) stri
|
||||
// nil-check before serialisation — this is inherent to patch-style update patterns
|
||||
// and cannot be meaningfully decomposed without introducing unnecessary abstraction.
|
||||
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{
|
||||
SettingPaymentEnabled: formatBoolOrEmpty(req.Enabled),
|
||||
SettingMinRechargeAmount: formatPositiveFloat(req.MinAmount),
|
||||
@@ -264,6 +275,7 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
|
||||
SettingOrderTimeoutMinutes: formatPositiveInt(req.OrderTimeoutMin),
|
||||
SettingMaxPendingOrders: formatPositiveInt(req.MaxPendingOrders),
|
||||
SettingBalancePayDisabled: formatBoolOrEmpty(req.BalanceDisabled),
|
||||
SettingBalanceRechargeMult: formatPositiveFloat(req.BalanceRechargeMultiplier),
|
||||
SettingLoadBalanceStrategy: derefStr(req.LoadBalanceStrategy),
|
||||
SettingProductNamePrefix: derefStr(req.ProductNamePrefix),
|
||||
SettingProductNameSuffix: derefStr(req.ProductNameSuffix),
|
||||
|
||||
@@ -216,7 +216,11 @@ func (s *PaymentService) markCompleted(ctx context.Context, o *dbent.PaymentOrde
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -43,14 +43,18 @@ func (s *PaymentService) CreateOrder(ctx context.Context, req CreateOrderRequest
|
||||
if user.Status != payment.EntityStatusActive {
|
||||
return nil, infraerrors.Forbidden("USER_INACTIVE", "user account is disabled")
|
||||
}
|
||||
amount := req.Amount
|
||||
orderAmount := req.Amount
|
||||
limitAmount := req.Amount
|
||||
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)
|
||||
payAmountStr := payment.CalculatePayAmount(amount, feeRate)
|
||||
payAmountStr := payment.CalculatePayAmount(limitAmount, feeRate)
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,7 +103,7 @@ func (s *PaymentService) validateSubOrder(ctx context.Context, req CreateOrderRe
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin transaction: %w", err)
|
||||
@@ -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 {
|
||||
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
|
||||
}
|
||||
tm := cfg.OrderTimeoutMin
|
||||
@@ -121,7 +125,7 @@ func (s *PaymentService) createOrderInTx(ctx context.Context, req CreateOrderReq
|
||||
SetUserEmail(user.Email).
|
||||
SetUserName(user.Username).
|
||||
SetNillableUserNotes(psNilIfEmpty(user.Notes)).
|
||||
SetAmount(amount).
|
||||
SetAmount(orderAmount).
|
||||
SetPayAmount(payAmount).
|
||||
SetFeeRate(feeRate).
|
||||
SetRechargeCode("").
|
||||
@@ -180,6 +184,10 @@ func (s *PaymentService) checkDailyLimit(ctx context.Context, tx *dbent.Tx, user
|
||||
}
|
||||
var used float64
|
||||
for _, o := range orders {
|
||||
if o.OrderType == payment.OrderTypeBalance {
|
||||
used += o.PayAmount
|
||||
continue
|
||||
}
|
||||
used += o.Amount
|
||||
}
|
||||
if used+amount > limit {
|
||||
@@ -213,7 +221,13 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -113,11 +113,7 @@ func (s *PaymentService) PrepareRefund(ctx context.Context, oid int64, amt float
|
||||
if amt-o.Amount > amountToleranceCNY {
|
||||
return nil, nil, infraerrors.BadRequest("REFUND_AMOUNT_EXCEEDED", "refund amount exceeds recharge")
|
||||
}
|
||||
// Full refund: use actual pay_amount for gateway (includes fees)
|
||||
ga := amt
|
||||
if math.Abs(amt-o.Amount) <= amountToleranceCNY {
|
||||
ga = o.PayAmount
|
||||
}
|
||||
ga := calculateGatewayRefundAmount(o.Amount, o.PayAmount, amt)
|
||||
rr := strings.TrimSpace(reason)
|
||||
if rr == "" && o.RefundRequestReason != nil {
|
||||
rr = *o.RefundRequestReason
|
||||
|
||||
Reference in New Issue
Block a user