167 lines
6.7 KiB
Go
167 lines
6.7 KiB
Go
package payment
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type RefundItemInput struct {
|
|
Identity string `json:"identity"`
|
|
Quantity int `json:"quantity"`
|
|
}
|
|
type RefundInput struct {
|
|
RequestNo, Reason, Description string
|
|
Items []RefundItemInput
|
|
}
|
|
|
|
// CreateRefund 校验本人订单、退款窗口、履约状态、数量和累计金额后创建待审核退款。
|
|
func CreateRefund(userID uint64, userIdentity, businessType, businessIdentity string, input RefundInput) (models.PaymentRefund, error) {
|
|
var result models.PaymentRefund
|
|
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|
var pay models.PaymentOrder
|
|
if err := tx.Where("business_type = ? AND business_identity = ? AND user_identity = ? AND payment_status = ?", businessType, businessIdentity, userIdentity, StatusPaid).Order("paid_at desc").First(&pay).Error; err != nil {
|
|
return err
|
|
}
|
|
if pay.PaidAt == nil || time.Now().After(pay.PaidAt.AddDate(0, 0, config.Spec.Payment.RefundWindowDays)) {
|
|
return errors.New("refund window expired")
|
|
}
|
|
var wallet models.WalletBasic
|
|
if err := tx.Where("owner_type = ? AND owner_identity = ? AND owner_id = ?", "user", userIdentity, userID).First(&wallet).Error; err != nil {
|
|
return err
|
|
}
|
|
items, whole, err := refundItems(tx, businessType, businessIdentity, userID, input.Items)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var used int64
|
|
if err := tx.Model(&models.PaymentRefund{}).Where("payment_order_id = ? AND refund_status IN ?", pay.ID, []int{10, 20}).Select("COALESCE(SUM(amount),0)").Scan(&used).Error; err != nil {
|
|
return err
|
|
}
|
|
var amount int64
|
|
for _, item := range items {
|
|
amount += item.Amount
|
|
}
|
|
if whole {
|
|
amount = pay.Amount - used
|
|
}
|
|
if amount <= 0 || used+amount > pay.Amount {
|
|
return errors.New("refund amount exceeds payment")
|
|
}
|
|
result = models.PaymentRefund{Entity: models.Entity{Identity: models.NewIdentity(), Status: 1}, RefundStatus: 10, PaymentOrderID: pay.ID, WalletBasicID: wallet.ID,
|
|
RefundNo: "RF" + time.Now().Format("20060102150405.000000"), RequestNo: input.RequestNo, BusinessType: businessType, BusinessIdentity: businessIdentity,
|
|
UserIdentity: userIdentity, Amount: amount, Reason: input.Reason, Description: input.Description}
|
|
if err := tx.Create(&result).Error; err != nil {
|
|
return err
|
|
}
|
|
for index := range items {
|
|
items[index].PaymentRefundID = result.ID
|
|
items[index].Identity = models.NewIdentity()
|
|
if err := tx.Create(&items[index]).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
func refundItems(tx *gorm.DB, businessType, identity string, userID uint64, requested []RefundItemInput) ([]models.PaymentRefundItem, bool, error) {
|
|
if len(requested) == 0 {
|
|
return nil, false, errors.New("refund items are required")
|
|
}
|
|
result := make([]models.PaymentRefundItem, 0, len(requested))
|
|
whole := true
|
|
switch businessType {
|
|
case "ec_order":
|
|
var order models.EcOrder
|
|
if err := tx.Where("identity = ? AND user_account_id = ? AND order_status = ? AND logistics_status < ?", identity, userID, 18, 30).First(&order).Error; err != nil {
|
|
return nil, false, err
|
|
}
|
|
var all []models.EcOrderItem
|
|
if err := tx.Where("ec_order_id = ?", order.ID).Find(&all).Error; err != nil {
|
|
return nil, false, err
|
|
}
|
|
by := map[string]models.EcOrderItem{}
|
|
for _, value := range all {
|
|
by[value.Identity] = value
|
|
}
|
|
for _, request := range requested {
|
|
value, ok := by[request.Identity]
|
|
if !ok || request.Quantity <= 0 || request.Quantity > value.Quantity {
|
|
return nil, false, gorm.ErrInvalidData
|
|
}
|
|
if request.Quantity != value.Quantity {
|
|
whole = false
|
|
}
|
|
result = append(result, models.PaymentRefundItem{OrderItemIdentity: value.Identity, Quantity: request.Quantity, Amount: value.SaleAmount * int64(request.Quantity)})
|
|
}
|
|
if len(requested) != len(all) {
|
|
whole = false
|
|
}
|
|
case "gasorder":
|
|
var order models.GasorderBasic
|
|
if err := tx.Where("identity = ? AND user_account_id = ? AND order_status = ?", identity, userID, 35).First(&order).Error; err != nil {
|
|
return nil, false, err
|
|
}
|
|
var all []models.GasorderItem
|
|
if err := tx.Where("gasorder_basic_id = ?", order.ID).Find(&all).Error; err != nil {
|
|
return nil, false, err
|
|
}
|
|
by := map[string]models.GasorderItem{}
|
|
for _, value := range all {
|
|
by[value.Identity] = value
|
|
}
|
|
for _, request := range requested {
|
|
value, ok := by[request.Identity]
|
|
if !ok || request.Quantity != 1 {
|
|
return nil, false, gorm.ErrInvalidData
|
|
}
|
|
result = append(result, models.PaymentRefundItem{OrderItemIdentity: value.Identity, Quantity: 1, Amount: value.UnitPrice})
|
|
}
|
|
if len(requested) != len(all) {
|
|
whole = false
|
|
}
|
|
default:
|
|
return nil, false, errors.New("business does not support refund")
|
|
}
|
|
return result, whole, nil
|
|
}
|
|
|
|
// ReviewRefund 驳回时记录原因;通过时在同一事务中立即增加钱包与不可变流水。
|
|
func ReviewRefund(identity, reviewer, remark string, approve bool) error {
|
|
return impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|
var refund models.PaymentRefund
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND refund_status = ?", identity, 10).First(&refund).Error; err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
if !approve {
|
|
if remark == "" {
|
|
return errors.New("reject reason required")
|
|
}
|
|
return tx.Model(&refund).Updates(map[string]any{"refund_status": 30, "review_remark": remark, "reviewer_identity": reviewer, "reviewed_at": &now}).Error
|
|
}
|
|
var wallet models.WalletBasic
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&wallet, refund.WalletBasicID).Error; err != nil {
|
|
return err
|
|
}
|
|
wallet.Balance += refund.Amount
|
|
if err := tx.Model(&wallet).Update("balance", wallet.Balance).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&refund).Updates(map[string]any{"refund_status": 20, "review_remark": remark, "reviewer_identity": reviewer, "reviewed_at": &now, "completed_at": &now}).Error; err != nil {
|
|
return err
|
|
}
|
|
date := now.In(time.Local)
|
|
return tx.Create(&models.WalletRecord{Entity: models.Entity{Identity: models.NewIdentity(), Status: 1}, WalletBasicID: wallet.ID, RecordNo: models.NewIdentity(), RequestNo: "refund:" + refund.Identity,
|
|
Direction: "income", TradeType: "refund", Amount: refund.Amount, BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance, InTradeNo: refund.RefundNo, PayChannel: "wallet", OperatorIdentity: reviewer,
|
|
Ymd: int32(date.Year()*10000 + int(date.Month())*100 + date.Day()), Ym: int32(date.Year()*100 + int(date.Month())), Remark: "退款审核:" + remark}).Error
|
|
})
|
|
}
|