hi-server/internal/logic/public/order/preCreateOrderLogic.go
EUForest 7e08a07e29 fix: resolve balance payment issue with coupon and fee calculation
This commit fixes the inconsistent calculation logic between order preview
and actual order creation, which caused balance payment failures when using
coupons.

Changes:
- Standardized fee calculation order in both preCreateOrderLogic and purchaseLogic
- Moved gift amount deduction after fee calculation to ensure correct total
- Removed premature gift amount deduction before transaction in purchaseLogic
- Gift amount is now only deducted within the database transaction

The calculation order is now unified:
1. Apply coupon discount
2. Calculate handling fee based on post-coupon amount
3. Deduct gift amount from total (including fee)

This ensures the preview amount matches the actual payment amount.
2026-02-06 23:14:04 +08:00

156 lines
5.4 KiB
Go

package order
import (
"context"
"encoding/json"
"github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type PreCreateOrderLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewPreCreateOrderLogic creates a new pre-create order logic instance for order preview operations.
// It initializes the logger with context and sets up the service context for database operations.
func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic {
return &PreCreateOrderLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// PreCreateOrder calculates order pricing preview including discounts, coupons, gift amounts, and fees
// without actually creating an order. It validates subscription plans, coupons, and payment methods
// to provide accurate pricing information for the frontend order preview.
func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
if req.Quantity <= 0 {
l.Debugf("[PreCreateOrder] Quantity is less than or equal to 0, setting to 1")
req.Quantity = 1
}
// find subscribe plan
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
if err != nil {
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
}
// check subscribe plan quota limit
if sub.Quota > 0 {
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
if err != nil {
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error())
}
var count int64
for _, v := range userSub {
if v.SubscribeId == req.SubscribeId {
count++
}
}
if count >= sub.Quota {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
}
}
var discount float64 = 1
if sub.Discount != "" {
var dis []types.SubscribeDiscount
_ = json.Unmarshal([]byte(sub.Discount), &dis)
discount = getDiscount(dis, req.Quantity)
}
price := sub.UnitPrice * req.Quantity
amount := int64(float64(price) * discount)
discountAmount := price - amount
var couponAmount int64
if req.Coupon != "" {
couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
}
if couponInfo.Count > 0 && couponInfo.Count <= couponInfo.UsedCount {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponAlreadyUsed), "coupon used")
}
var count int64
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error
})
if err != nil {
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error())
}
if couponInfo.UserLimit > 0 && count >= couponInfo.UserLimit {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded")
}
couponSub := tool.StringToInt64Slice(couponInfo.Subscribe)
if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match")
}
couponAmount = calculateCoupon(amount, couponInfo)
}
amount -= couponAmount
var feeAmount int64
if req.Payment != 0 {
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
if err != nil {
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
}
// Calculate the handling fee
if amount > 0 {
feeAmount = calculateFee(amount, payment)
amount += feeAmount
}
}
// Calculate gift amount deduction after fee calculation
var deductionAmount int64
if u.GiftAmount > 0 && amount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
}
}
resp = &types.PreOrderResponse{
Price: price,
Amount: amount,
Discount: discountAmount,
GiftAmount: deductionAmount,
Coupon: req.Coupon,
CouponDiscount: couponAmount,
FeeAmount: feeAmount,
}
return
}