init: 1.0.0
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/coupon"
|
||||
)
|
||||
|
||||
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
|
||||
if couponInfo.Type == 1 {
|
||||
return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100)))
|
||||
} else {
|
||||
return min(couponInfo.Discount, amount)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package order
|
||||
|
||||
import "github.com/perfect-panel/ppanel-server/internal/model/payment"
|
||||
|
||||
func calculateFee(amount int64, config *payment.Payment) int64 {
|
||||
var fee float64
|
||||
switch config.FeeMode {
|
||||
case 0:
|
||||
return 0
|
||||
case 1:
|
||||
fee = float64(amount) * (float64(config.FeePercent) / float64(100))
|
||||
case 2:
|
||||
if amount > 0 {
|
||||
fee = float64(config.FeeAmount)
|
||||
}
|
||||
case 3:
|
||||
fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount)
|
||||
}
|
||||
return int64(fee)
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/constant"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/order"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/payment"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/user"
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/exchangeRate"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/payment/alipay"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/payment/epay"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/payment/stripe"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/tool"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/xerr"
|
||||
queueType "github.com/perfect-panel/ppanel-server/queue/types"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CheckoutOrderLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
type CurrencyConfig struct {
|
||||
CurrencyUnit string
|
||||
CurrencySymbol string
|
||||
AccessKey string
|
||||
}
|
||||
|
||||
const (
|
||||
Stripe = "Stripe"
|
||||
QR = "qr"
|
||||
Link = "link"
|
||||
)
|
||||
|
||||
// NewCheckoutOrderLogic Checkout order
|
||||
func NewCheckoutOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckoutOrderLogic {
|
||||
return &CheckoutOrderLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CheckoutOrderLogic) CheckoutOrder(req *types.CheckoutOrderRequest, requestHost string) (resp *types.CheckoutOrderResponse, err error) {
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Error("[CheckoutOrderLogic] Invalid access")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid access")
|
||||
}
|
||||
// find order
|
||||
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] FindOneByOrderNo error", logger.Field("orderNo", req.OrderNo), logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByOrderNo error: %s", err.Error())
|
||||
}
|
||||
|
||||
if orderInfo.Status != 1 {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Order status error")
|
||||
}
|
||||
|
||||
paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] FindOneByPaymentMark error", logger.Field("paymentMark", orderInfo.Method), logger.Field("PaymentID", orderInfo.PaymentId), logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByPaymentMark error: %s", err.Error())
|
||||
}
|
||||
var stripePayment *types.StripePayment = nil
|
||||
var url, t string
|
||||
|
||||
// switch payment method
|
||||
switch paymentPlatform.ParsePlatform(paymentConfig.Platform) {
|
||||
case paymentPlatform.Stripe:
|
||||
result, err := l.stripePayment(paymentConfig.Config, orderInfo, u)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] stripePayment error", logger.Field("error", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
stripePayment = result
|
||||
t = Stripe
|
||||
case paymentPlatform.EPay:
|
||||
// epay
|
||||
url, err = l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl, requestHost)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] epayPayment error", logger.Field("error", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
t = Link
|
||||
case paymentPlatform.AlipayF2F:
|
||||
// alipay f2f
|
||||
url, err = l.alipayF2fPayment(paymentConfig, orderInfo, requestHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t = QR
|
||||
case paymentPlatform.Balance:
|
||||
// balance
|
||||
if err = l.balancePayment(u, orderInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t = paymentPlatform.Balance.String()
|
||||
default:
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Payment method not supported")
|
||||
}
|
||||
return &types.CheckoutOrderResponse{
|
||||
Type: t,
|
||||
CheckoutUrl: url,
|
||||
Stripe: stripePayment,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Query exchange rate
|
||||
func (l *CheckoutOrderLogic) queryExchangeRate(to string, src int64) (amount float64, err error) {
|
||||
amount = float64(src) / float64(100)
|
||||
// query system currency
|
||||
currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] GetCurrencyConfig error", logger.Field("error", err.Error()))
|
||||
return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %s", err.Error())
|
||||
}
|
||||
configs := &CurrencyConfig{}
|
||||
tool.SystemConfigSliceReflectToStruct(currency, configs)
|
||||
if configs.AccessKey == "" {
|
||||
return amount, nil
|
||||
}
|
||||
if configs.CurrencyUnit != to {
|
||||
// query exchange rate
|
||||
result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, to, configs.AccessKey, 1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
amount = result * amount
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
// Stripe Payment
|
||||
func (l *CheckoutOrderLogic) stripePayment(config string, info *order.Order, u *user.User) (*types.StripePayment, error) {
|
||||
// stripe WeChat pay or stripe alipay
|
||||
stripeConfig := payment.StripeConfig{}
|
||||
if err := json.Unmarshal([]byte(config), &stripeConfig); err != nil {
|
||||
l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
|
||||
}
|
||||
client := stripe.NewClient(stripe.Config{
|
||||
SecretKey: stripeConfig.SecretKey,
|
||||
PublicKey: stripeConfig.PublicKey,
|
||||
WebhookSecret: stripeConfig.WebhookSecret,
|
||||
})
|
||||
// Calculate the amount with exchange rate
|
||||
amount, err := l.queryExchangeRate("CNY", info.Amount)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error())
|
||||
}
|
||||
convertAmount := int64(amount * 100)
|
||||
// create payment
|
||||
result, err := client.CreatePaymentSheet(&stripe.Order{
|
||||
OrderNo: info.OrderNo,
|
||||
Subscribe: strconv.FormatInt(info.SubscribeId, 10),
|
||||
Amount: convertAmount,
|
||||
Currency: "cny",
|
||||
Payment: stripeConfig.Payment,
|
||||
},
|
||||
&stripe.User{
|
||||
UserId: u.Id,
|
||||
})
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] CreatePaymentSheet error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "CreatePaymentSheet error: %s", err.Error())
|
||||
}
|
||||
tradeNo := result.TradeNo
|
||||
stripePayment := &types.StripePayment{
|
||||
PublishableKey: stripeConfig.PublicKey,
|
||||
ClientSecret: result.ClientSecret,
|
||||
Method: stripeConfig.Payment,
|
||||
}
|
||||
// save payment
|
||||
info.TradeNo = tradeNo
|
||||
err = l.svcCtx.OrderModel.Update(l.ctx, info)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] Update error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update error: %s", err.Error())
|
||||
}
|
||||
return stripePayment, nil
|
||||
}
|
||||
|
||||
// epay payment
|
||||
func (l *CheckoutOrderLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl, requestHost string) (string, error) {
|
||||
epayConfig := payment.EPayConfig{}
|
||||
if err := json.Unmarshal([]byte(config.Config), &epayConfig); err != nil {
|
||||
l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error()))
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
|
||||
}
|
||||
client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key)
|
||||
// Calculate the amount with exchange rate
|
||||
amount, err := l.queryExchangeRate("CNY", info.Amount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var domain string
|
||||
if config.Domain != "" {
|
||||
domain = config.Domain
|
||||
} else {
|
||||
domain = fmt.Sprintf("http://%s", requestHost)
|
||||
}
|
||||
// create payment
|
||||
url := client.CreatePayUrl(epay.Order{
|
||||
Name: l.svcCtx.Config.Site.SiteName,
|
||||
Amount: amount,
|
||||
OrderNo: info.OrderNo,
|
||||
SignType: "MD5",
|
||||
NotifyUrl: domain + "/v1/notify/epay",
|
||||
ReturnUrl: returnUrl,
|
||||
})
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// alipay f2f payment
|
||||
func (l *CheckoutOrderLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order, requestHost string) (string, error) {
|
||||
f2FConfig := payment.AlipayF2FConfig{}
|
||||
if err := json.Unmarshal([]byte(pay.Config), &f2FConfig); err != nil {
|
||||
l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error()))
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
|
||||
}
|
||||
var domain string
|
||||
if pay.Domain != "" {
|
||||
domain = pay.Domain
|
||||
} else {
|
||||
domain = fmt.Sprintf("http://%s", requestHost)
|
||||
}
|
||||
client := alipay.NewClient(alipay.Config{
|
||||
AppId: f2FConfig.AppId,
|
||||
PrivateKey: f2FConfig.PrivateKey,
|
||||
PublicKey: f2FConfig.PublicKey,
|
||||
InvoiceName: f2FConfig.InvoiceName,
|
||||
NotifyURL: domain + "/notify/alipay",
|
||||
})
|
||||
// Calculate the amount with exchange rate
|
||||
amount, err := l.queryExchangeRate("CNY", info.Amount)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error()))
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error())
|
||||
}
|
||||
convertAmount := int64(amount * 100)
|
||||
// create payment
|
||||
QRCode, err := client.PreCreateTrade(l.ctx, alipay.Order{
|
||||
OrderNo: info.OrderNo,
|
||||
Amount: convertAmount,
|
||||
})
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] PreCreateTrade error", logger.Field("error", err.Error()))
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "PreCreateTrade error: %s", err.Error())
|
||||
}
|
||||
return QRCode, nil
|
||||
}
|
||||
|
||||
// Balance payment
|
||||
func (l *CheckoutOrderLogic) balancePayment(u *user.User, o *order.Order) error {
|
||||
var userInfo user.User
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userInfo.Balance < o.Amount {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InsufficientBalance), "Insufficient balance")
|
||||
}
|
||||
// deduct balance
|
||||
userInfo.Balance -= o.Amount
|
||||
err = l.svcCtx.UserModel.Update(l.ctx, &userInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// create balance log
|
||||
balanceLog := &user.BalanceLog{
|
||||
Id: 0,
|
||||
UserId: u.Id,
|
||||
Amount: o.Amount,
|
||||
Type: 3,
|
||||
OrderId: o.Id,
|
||||
Balance: userInfo.Balance,
|
||||
}
|
||||
err = db.Create(balanceLog).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2)
|
||||
})
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] Transaction error", logger.Field("error", err.Error()), logger.Field("orderNo", o.OrderNo))
|
||||
return err
|
||||
}
|
||||
// create activity order task
|
||||
payload := queueType.ForthwithActivateOrderPayload{
|
||||
OrderNo: o.OrderNo,
|
||||
}
|
||||
bytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] Marshal error", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
|
||||
_, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
||||
if err != nil {
|
||||
l.Error("[CheckoutOrderLogic] Enqueue error", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
l.Logger.Info("[CheckoutOrderLogic] Enqueue success", logger.Field("orderNo", o.OrderNo))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/user"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/payment/stripe"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/order"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/payment"
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/payment/alipay"
|
||||
)
|
||||
|
||||
type CloseOrderLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewCloseOrderLogic Close order
|
||||
func NewCloseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CloseOrderLogic {
|
||||
return &CloseOrderLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error {
|
||||
// Find order information by order number
|
||||
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
|
||||
if err != nil {
|
||||
l.Error("[CloseOrder] Find order info failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("orderNo", req.OrderNo),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
// If the order status is not 1, it means that the order has been closed or paid
|
||||
if orderInfo.Status != 1 {
|
||||
l.Info("[CloseOrder] Order status is not 1",
|
||||
logger.Field("orderNo", req.OrderNo),
|
||||
logger.Field("status", orderInfo.Status),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if l.confirmationPayment(orderInfo) {
|
||||
l.Info("[CloseOrder] Order has been paid",
|
||||
logger.Field("orderNo", req.OrderNo),
|
||||
logger.Field("status", orderInfo.Status),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// update order status
|
||||
err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error
|
||||
if err != nil {
|
||||
l.Error("[CloseOrder] Update order status failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("orderNo", req.OrderNo),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// refund deduction amount to user deduction balance
|
||||
if orderInfo.GiftAmount > 0 {
|
||||
userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId)
|
||||
if err != nil {
|
||||
l.Error("[CloseOrder] Find user info failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", orderInfo.UserId),
|
||||
)
|
||||
return err
|
||||
}
|
||||
deduction := userInfo.GiftAmount + orderInfo.GiftAmount
|
||||
err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("deduction", deduction).Error
|
||||
if err != nil {
|
||||
l.Error("[CloseOrder] Refund deduction amount failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("uid", orderInfo.UserId),
|
||||
logger.Field("deduction", orderInfo.GiftAmount),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// Record the deduction refund log
|
||||
giftAmountLog := &user.GiftAmountLog{
|
||||
UserId: orderInfo.UserId,
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
Amount: orderInfo.GiftAmount,
|
||||
Type: 1,
|
||||
Balance: deduction,
|
||||
Remark: "Order cancellation refund",
|
||||
}
|
||||
err = tx.Model(&user.GiftAmountLog{}).Create(giftAmountLog).Error
|
||||
if err != nil {
|
||||
l.Error("[CloseOrder] Record cancellation refund log failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("uid", orderInfo.UserId),
|
||||
logger.Field("deduction", orderInfo.GiftAmount),
|
||||
)
|
||||
return err
|
||||
}
|
||||
// update user cache
|
||||
return l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// confirmationPayment Determine whether the payment is successful
|
||||
//
|
||||
//nolint:unused
|
||||
func (l *CloseOrderLogic) confirmationPayment(order *order.Order) bool {
|
||||
paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, order.PaymentId)
|
||||
if err != nil {
|
||||
l.Error("[CloseOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method))
|
||||
return false
|
||||
}
|
||||
switch paymentPlatform.ParsePlatform(order.Method) {
|
||||
case paymentPlatform.AlipayF2F:
|
||||
if l.queryAlipay(paymentConfig, order.TradeNo) {
|
||||
return true
|
||||
}
|
||||
case paymentPlatform.Stripe:
|
||||
if l.queryStripe(paymentConfig, order.TradeNo) {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
l.Info("[CloseOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// queryAlipay Query Alipay payment status
|
||||
func (l *CloseOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool {
|
||||
config := payment.AlipayF2FConfig{}
|
||||
if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
|
||||
l.Error("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config))
|
||||
return false
|
||||
}
|
||||
client := alipay.NewClient(alipay.Config{
|
||||
AppId: config.AppId,
|
||||
PrivateKey: config.PrivateKey,
|
||||
PublicKey: config.PublicKey,
|
||||
InvoiceName: config.InvoiceName,
|
||||
})
|
||||
status, err := client.QueryTrade(l.ctx, TradeNo)
|
||||
if err != nil {
|
||||
l.Error("[CloseOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo))
|
||||
return false
|
||||
}
|
||||
if status == alipay.Success || status == alipay.Finished {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// queryStripe Query Stripe payment status
|
||||
func (l *CloseOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool {
|
||||
config := payment.StripeConfig{}
|
||||
if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil {
|
||||
l.Error("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config))
|
||||
return false
|
||||
}
|
||||
client := stripe.NewClient(stripe.Config{
|
||||
PublicKey: config.PublicKey,
|
||||
SecretKey: config.SecretKey,
|
||||
WebhookSecret: config.WebhookSecret,
|
||||
})
|
||||
status, err := client.QueryOrderStatus(TradeNo)
|
||||
if err != nil {
|
||||
l.Error("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo))
|
||||
return false
|
||||
}
|
||||
return status
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package order
|
||||
|
||||
import "github.com/perfect-panel/ppanel-server/internal/types"
|
||||
|
||||
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
||||
var finalDiscount int64 = 100
|
||||
|
||||
for _, discount := range discounts {
|
||||
if inputMonths >= discount.Quantity && discount.Discount < finalDiscount {
|
||||
finalDiscount = discount.Discount
|
||||
}
|
||||
}
|
||||
return float64(finalDiscount) / float64(100)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/order"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/tool"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/constant"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/user"
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PreCreateOrderLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Pre create order
|
||||
func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic {
|
||||
return &PreCreateOrderLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
// find subscribe plan
|
||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
|
||||
if err != nil {
|
||||
l.Error("[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())
|
||||
}
|
||||
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 coupon 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 <= couponInfo.UsedCount {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used")
|
||||
}
|
||||
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")
|
||||
}
|
||||
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("[Purchase] 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 count >= couponInfo.UserLimit {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded")
|
||||
}
|
||||
coupon = calculateCoupon(amount, couponInfo)
|
||||
}
|
||||
amount -= coupon
|
||||
|
||||
var deductionAmount int64
|
||||
// Check user deduction amount
|
||||
if u.GiftAmount > 0 {
|
||||
if u.GiftAmount >= amount {
|
||||
deductionAmount = amount
|
||||
amount = 0
|
||||
} else {
|
||||
deductionAmount = u.GiftAmount
|
||||
amount -= u.GiftAmount
|
||||
}
|
||||
}
|
||||
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
l.Logger.Error("[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())
|
||||
}
|
||||
var feeAmount int64
|
||||
// Calculate the handling fee
|
||||
if amount > 0 {
|
||||
feeAmount = calculateFee(amount, payment)
|
||||
}
|
||||
amount += feeAmount
|
||||
|
||||
resp = &types.PreOrderResponse{
|
||||
Price: price,
|
||||
Amount: amount,
|
||||
Discount: discountAmount,
|
||||
GiftAmount: deductionAmount,
|
||||
Coupon: req.Coupon,
|
||||
CouponDiscount: coupon,
|
||||
FeeAmount: feeAmount,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/constant"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/order"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/user"
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/tool"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/xerr"
|
||||
queue "github.com/perfect-panel/ppanel-server/queue/types"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
const CloseOrderTimeMinutes = 15
|
||||
|
||||
// purchase Subscription
|
||||
func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic {
|
||||
return &PurchaseLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, 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")
|
||||
}
|
||||
// find user subscription
|
||||
|
||||
if l.svcCtx.Config.Subscribe.SingleModel {
|
||||
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
|
||||
if err != nil {
|
||||
l.Error("[Purchase] 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())
|
||||
}
|
||||
if len(userSub) > 0 {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserSubscribeExist), "user has subscription")
|
||||
}
|
||||
}
|
||||
|
||||
// find subscribe plan
|
||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
|
||||
|
||||
if err != nil {
|
||||
l.Error("[Purchase] 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 status
|
||||
if !*sub.Sell {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell")
|
||||
}
|
||||
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
|
||||
// discount amount
|
||||
amount := int64(float64(price) * discount)
|
||||
discountAmount := price - amount
|
||||
var coupon int64 = 0
|
||||
// Calculate the coupon deduction
|
||||
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 <= couponInfo.UsedCount {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used")
|
||||
}
|
||||
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")
|
||||
}
|
||||
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("[Purchase] 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 count >= couponInfo.UserLimit {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded")
|
||||
}
|
||||
coupon = calculateCoupon(amount, couponInfo)
|
||||
}
|
||||
// Calculate the handling fee
|
||||
amount -= coupon
|
||||
var deductionAmount int64
|
||||
// Check user deduction amount
|
||||
if u.GiftAmount > 0 {
|
||||
if u.GiftAmount >= amount {
|
||||
deductionAmount = amount
|
||||
amount = 0
|
||||
u.GiftAmount -= amount
|
||||
} else {
|
||||
deductionAmount = u.GiftAmount
|
||||
amount -= u.GiftAmount
|
||||
u.GiftAmount = 0
|
||||
}
|
||||
}
|
||||
// find payment method
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
l.Logger.Error("[Purchase] 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())
|
||||
}
|
||||
var feeAmount int64
|
||||
// Calculate the handling fee
|
||||
if amount > 0 {
|
||||
feeAmount = calculateFee(amount, payment)
|
||||
}
|
||||
// query user is new purchase or renewal
|
||||
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
|
||||
if err != nil {
|
||||
l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error())
|
||||
}
|
||||
// create order
|
||||
orderInfo := &order.Order{
|
||||
UserId: u.Id,
|
||||
OrderNo: tool.GenerateTradeNo(),
|
||||
Type: 1,
|
||||
Quantity: req.Quantity,
|
||||
Price: price,
|
||||
Amount: amount,
|
||||
Discount: discountAmount,
|
||||
GiftAmount: deductionAmount,
|
||||
Coupon: req.Coupon,
|
||||
CouponDiscount: coupon,
|
||||
PaymentId: req.Payment,
|
||||
Method: payment.Platform,
|
||||
FeeAmount: feeAmount,
|
||||
Status: 1,
|
||||
IsNew: isNew,
|
||||
SubscribeId: req.SubscribeId,
|
||||
}
|
||||
// Database transaction
|
||||
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if orderInfo.GiftAmount > 0 {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
|
||||
l.Error("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
|
||||
return e
|
||||
}
|
||||
// create deduction record
|
||||
giftAmountLog := user.GiftAmountLog{
|
||||
UserId: orderInfo.UserId,
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
Amount: orderInfo.GiftAmount,
|
||||
Type: 2,
|
||||
Balance: u.GiftAmount,
|
||||
Remark: "Purchase order deduction",
|
||||
}
|
||||
if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil {
|
||||
l.Error("[Purchase] Database insert error",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("deductionLog", giftAmountLog),
|
||||
)
|
||||
return e
|
||||
}
|
||||
}
|
||||
// insert order
|
||||
return db.Model(&order.Order{}).Create(&orderInfo).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error())
|
||||
}
|
||||
// Deferred task
|
||||
payload := queue.DeferCloseOrderPayload{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
}
|
||||
val, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
l.Error("[CreateOrder] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
|
||||
}
|
||||
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
|
||||
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
|
||||
if err != nil {
|
||||
l.Error("[CreateOrder] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
|
||||
} else {
|
||||
l.Info("[CreateOrder] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
|
||||
}
|
||||
|
||||
return &types.PurchaseOrderResponse{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/tool"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type QueryOrderDetailLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get order
|
||||
func NewQueryOrderDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderDetailLogic {
|
||||
return &QueryOrderDetailLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *QueryOrderDetailLogic) QueryOrderDetail(req *types.QueryOrderDetailRequest) (resp *types.OrderDetail, err error) {
|
||||
orderInfo, err := l.svcCtx.OrderModel.FindOneDetailsByOrderNo(l.ctx, req.OrderNo)
|
||||
if err != nil {
|
||||
l.Error("[QueryOrderDetail] Database query error", logger.Field("error", err.Error()), logger.Field("order_no", req.OrderNo))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error())
|
||||
}
|
||||
resp = &types.OrderDetail{}
|
||||
tool.DeepCopy(resp, orderInfo)
|
||||
// Prevent commission amount leakage
|
||||
resp.Commission = 0
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/constant"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/user"
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/tool"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type QueryOrderListLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get order list
|
||||
func NewQueryOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderListLogic {
|
||||
return &QueryOrderListLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *QueryOrderListLogic) QueryOrderList(req *types.QueryOrderListRequest) (resp *types.QueryOrderListResponse, 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")
|
||||
}
|
||||
total, data, err := l.svcCtx.OrderModel.QueryOrderListByPage(l.ctx, req.Page, req.Size, 0, u.Id, 0, "")
|
||||
if err != nil {
|
||||
l.Error("[QueryOrderListLogic] Query order list failed", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query order list failed")
|
||||
}
|
||||
resp = &types.QueryOrderListResponse{
|
||||
Total: total,
|
||||
List: make([]types.OrderDetail, 0),
|
||||
}
|
||||
for _, item := range data {
|
||||
var orderInfo types.OrderDetail
|
||||
tool.DeepCopy(&orderInfo, item)
|
||||
// Prevent commission amount leakage
|
||||
orderInfo.Commission = 0
|
||||
resp.List = append(resp.List, orderInfo)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/constant"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/xerr"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/order"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/user"
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/tool"
|
||||
queue "github.com/perfect-panel/ppanel-server/queue/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type RechargeLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewRechargeLogic Recharge
|
||||
func NewRechargeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RechargeLogic {
|
||||
return &RechargeLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.RechargeOrderResponse, 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")
|
||||
}
|
||||
// find payment method
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
l.Error("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
||||
return nil, errors.Wrapf(err, "find payment error: %v", err.Error())
|
||||
}
|
||||
// Calculate the handling fee
|
||||
feeAmount := calculateFee(req.Amount, payment)
|
||||
// query user is new purchase or renewal
|
||||
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
|
||||
if err != nil {
|
||||
l.Error("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, errors.Wrapf(err, "query user error: %v", err.Error())
|
||||
}
|
||||
orderInfo := order.Order{
|
||||
UserId: u.Id,
|
||||
OrderNo: tool.GenerateTradeNo(),
|
||||
Type: 4,
|
||||
Price: req.Amount,
|
||||
Amount: req.Amount + feeAmount,
|
||||
FeeAmount: feeAmount,
|
||||
PaymentId: req.Payment,
|
||||
Method: payment.Platform,
|
||||
Status: 1,
|
||||
IsNew: isNew,
|
||||
}
|
||||
err = l.svcCtx.OrderModel.Insert(l.ctx, &orderInfo)
|
||||
if err != nil {
|
||||
l.Error("[Recharge] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
|
||||
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
|
||||
}
|
||||
// Deferred task
|
||||
payload := queue.DeferCloseOrderPayload{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
}
|
||||
val, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
l.Error("[Recharge] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
|
||||
}
|
||||
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
|
||||
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
|
||||
if err != nil {
|
||||
l.Error("[Recharge] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
|
||||
} else {
|
||||
l.Info("[Recharge] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
|
||||
}
|
||||
return &types.RechargeOrderResponse{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/constant"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/order"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/user"
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/tool"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/xerr"
|
||||
queue "github.com/perfect-panel/ppanel-server/queue/types"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RenewalLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Renewal Subscription
|
||||
func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic {
|
||||
return &RenewalLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, 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")
|
||||
}
|
||||
orderNo := tool.GenerateTradeNo()
|
||||
// find user subscribe
|
||||
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error())
|
||||
}
|
||||
// find subscription
|
||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId)
|
||||
if err != nil {
|
||||
l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", userSubscribe.SubscribeId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
|
||||
}
|
||||
// check subscribe plan status
|
||||
if !*sub.Sell {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell")
|
||||
}
|
||||
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 coupon int64 = 0
|
||||
if req.Coupon != "" {
|
||||
couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon)
|
||||
if err != nil {
|
||||
l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("coupon", req.Coupon))
|
||||
return nil, errors.Wrapf(err, "find coupon error: %v", err.Error())
|
||||
}
|
||||
if couponInfo.Count <= couponInfo.UsedCount {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used")
|
||||
}
|
||||
coupon = calculateCoupon(amount, couponInfo)
|
||||
}
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
||||
return nil, errors.Wrapf(err, "find payment error: %v", err.Error())
|
||||
}
|
||||
amount -= coupon
|
||||
|
||||
var deductionAmount int64
|
||||
// Check user deduction amount
|
||||
if u.GiftAmount > 0 {
|
||||
if u.GiftAmount >= amount {
|
||||
deductionAmount = amount
|
||||
amount = 0
|
||||
u.GiftAmount -= amount
|
||||
} else {
|
||||
deductionAmount = u.GiftAmount
|
||||
amount -= u.GiftAmount
|
||||
u.GiftAmount = 0
|
||||
}
|
||||
}
|
||||
|
||||
var feeAmount int64
|
||||
// Calculate the handling fee
|
||||
if amount > 0 {
|
||||
feeAmount = calculateFee(amount, payment)
|
||||
}
|
||||
|
||||
amount += feeAmount
|
||||
|
||||
// create order
|
||||
orderInfo := order.Order{
|
||||
UserId: u.Id,
|
||||
ParentId: userSubscribe.OrderId,
|
||||
OrderNo: orderNo,
|
||||
Type: 2,
|
||||
Quantity: req.Quantity,
|
||||
Price: price,
|
||||
Amount: amount,
|
||||
GiftAmount: deductionAmount,
|
||||
Discount: discountAmount,
|
||||
Coupon: req.Coupon,
|
||||
CouponDiscount: coupon,
|
||||
PaymentId: payment.Id,
|
||||
Method: payment.Platform,
|
||||
FeeAmount: feeAmount,
|
||||
Status: 1,
|
||||
SubscribeId: userSubscribe.SubscribeId,
|
||||
SubscribeToken: userSubscribe.Token,
|
||||
}
|
||||
// Database transaction
|
||||
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if orderInfo.GiftAmount > 0 {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
|
||||
l.Error("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
|
||||
return err
|
||||
}
|
||||
// create deduction record
|
||||
deductionLog := user.GiftAmountLog{
|
||||
UserId: orderInfo.UserId,
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
Amount: orderInfo.GiftAmount,
|
||||
Type: 2,
|
||||
Balance: u.GiftAmount,
|
||||
Remark: "Renewal order deduction",
|
||||
}
|
||||
if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil {
|
||||
l.Error("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog))
|
||||
return err
|
||||
}
|
||||
}
|
||||
// insert order
|
||||
return db.Model(&order.Order{}).Create(&orderInfo).Error
|
||||
})
|
||||
if err != nil {
|
||||
l.Error("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
|
||||
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
|
||||
}
|
||||
// Deferred task
|
||||
payload := queue.DeferCloseOrderPayload{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
}
|
||||
val, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
l.Error("[Renewal] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
|
||||
}
|
||||
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
|
||||
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
|
||||
if err != nil {
|
||||
l.Error("[Renewal] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
|
||||
} else {
|
||||
l.Info("[Renewal] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
|
||||
}
|
||||
return &types.RenewalOrderResponse{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/constant"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/xerr"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/order"
|
||||
"github.com/perfect-panel/ppanel-server/internal/model/user"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/tool"
|
||||
queue "github.com/perfect-panel/ppanel-server/queue/types"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/internal/svc"
|
||||
"github.com/perfect-panel/ppanel-server/internal/types"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
)
|
||||
|
||||
type ResetTrafficLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Reset traffic
|
||||
func NewResetTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetTrafficLogic {
|
||||
return &ResetTrafficLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ResetTrafficLogic) ResetTraffic(req *types.ResetTrafficOrderRequest) (resp *types.ResetTrafficOrderResponse, 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")
|
||||
}
|
||||
// find user subscription
|
||||
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
|
||||
if err != nil {
|
||||
l.Error("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("UserSubscribeID", req.UserSubscribeID))
|
||||
return nil, errors.Wrapf(err, "find user subscribe error: %v", err.Error())
|
||||
}
|
||||
if userSubscribe.Subscribe == nil {
|
||||
l.Error("[ResetTraffic] subscribe not found", logger.Field("UserSubscribeID", req.UserSubscribeID))
|
||||
return nil, errors.New("subscribe not found")
|
||||
}
|
||||
amount := userSubscribe.Subscribe.Replacement
|
||||
var deductionAmount int64
|
||||
// Check user deduction amount
|
||||
if u.GiftAmount > 0 {
|
||||
if u.GiftAmount >= amount {
|
||||
deductionAmount = amount
|
||||
amount = 0
|
||||
u.GiftAmount -= amount
|
||||
} else {
|
||||
deductionAmount = u.GiftAmount
|
||||
amount -= u.GiftAmount
|
||||
u.GiftAmount = 0
|
||||
}
|
||||
}
|
||||
// find payment method
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
l.Error("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment))
|
||||
return nil, errors.Wrapf(err, "find payment error: %v", err.Error())
|
||||
}
|
||||
var feeAmount int64
|
||||
// Calculate the handling fee
|
||||
if amount > 0 {
|
||||
feeAmount = calculateFee(amount, payment)
|
||||
}
|
||||
// create order
|
||||
orderInfo := order.Order{
|
||||
Id: 0,
|
||||
ParentId: userSubscribe.OrderId,
|
||||
UserId: u.Id,
|
||||
OrderNo: tool.GenerateTradeNo(),
|
||||
Type: 3,
|
||||
Price: userSubscribe.Subscribe.Replacement,
|
||||
Amount: amount + feeAmount,
|
||||
GiftAmount: deductionAmount,
|
||||
FeeAmount: feeAmount,
|
||||
PaymentId: req.Payment,
|
||||
Method: payment.Platform,
|
||||
Status: 1,
|
||||
SubscribeId: userSubscribe.SubscribeId,
|
||||
SubscribeToken: userSubscribe.Token,
|
||||
}
|
||||
// Database transaction
|
||||
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if orderInfo.GiftAmount > 0 {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil {
|
||||
l.Error("[ResetTraffic] Database update error", logger.Field("error", err.Error()), logger.Field("user", u))
|
||||
return err
|
||||
}
|
||||
// create deduction record
|
||||
deductionLog := user.GiftAmountLog{
|
||||
UserId: orderInfo.UserId,
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
Amount: orderInfo.GiftAmount,
|
||||
Type: 2,
|
||||
Balance: u.GiftAmount,
|
||||
Remark: "ResetTraffic order deduction",
|
||||
}
|
||||
if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil {
|
||||
l.Error("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog))
|
||||
return err
|
||||
}
|
||||
}
|
||||
// insert order
|
||||
return db.Model(&order.Order{}).Create(&orderInfo).Error
|
||||
})
|
||||
if err != nil {
|
||||
l.Error("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
|
||||
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
|
||||
}
|
||||
// Deferred task
|
||||
payload := queue.DeferCloseOrderPayload{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
}
|
||||
val, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
l.Error("[ResetTraffic] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload))
|
||||
}
|
||||
task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3))
|
||||
taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute))
|
||||
if err != nil {
|
||||
l.Error("[ResetTraffic] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task))
|
||||
} else {
|
||||
l.Info("[ResetTraffic] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
|
||||
}
|
||||
return &types.ResetTrafficOrderResponse{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user