|
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"log"
|
|
|
|
|
"math"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
@@ -20,9 +21,20 @@ import (
|
|
|
|
|
"gorm.io/gorm/clause"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RefundFunc 由 payment 模块注入,避免 order 与 payment 形成循环依赖。
|
|
|
|
|
type RefundFunc func(orderID uint64, refundAmountCent int64, bizType string, remark string) (status string, err error)
|
|
|
|
|
|
|
|
|
|
type refundAction struct {
|
|
|
|
|
OrderID uint64
|
|
|
|
|
RefundAmountCent int64
|
|
|
|
|
BizType string
|
|
|
|
|
Remark string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Repository struct {
|
|
|
|
|
db *gorm.DB
|
|
|
|
|
chatRepo *chat.Repository
|
|
|
|
|
db *gorm.DB
|
|
|
|
|
chatRepo *chat.Repository
|
|
|
|
|
refundFunc RefundFunc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultPendingPaymentTimeoutMinutes = 15
|
|
|
|
@@ -35,6 +47,10 @@ func (r *Repository) SetChatRepo(cr *chat.Repository) {
|
|
|
|
|
r.chatRepo = cr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) SetRefundFunc(fn RefundFunc) {
|
|
|
|
|
r.refundFunc = fn
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type orderPricing struct {
|
|
|
|
|
RentAmount float64
|
|
|
|
|
OwnerRentAmount float64
|
|
|
|
@@ -201,112 +217,12 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
|
|
|
|
|
return r.FindForUser(renterID, createdID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pay 保留旧接口兼容,但真实付款必须走 payment 模块的渠道支付入口。
|
|
|
|
|
func (r *Repository) Pay(userID uint64, orderID uint64) error {
|
|
|
|
|
var newConvID uint64
|
|
|
|
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var order model.RentalOrder
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
|
|
|
Where("id = ? AND renter_id = ?", orderID, userID).
|
|
|
|
|
First(&order).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if order.Status != "pending_payment" {
|
|
|
|
|
return ErrOrderCannotPay
|
|
|
|
|
}
|
|
|
|
|
timeoutMinutes := pendingPaymentTimeoutMinutes(tx)
|
|
|
|
|
if timeoutMinutes > 0 && order.CreatedAt.Before(time.Now().Add(-time.Duration(timeoutMinutes)*time.Minute)) {
|
|
|
|
|
return ErrOrderCannotPay
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var listing model.RentalListing
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if listing.Status != "published" || listing.ReviewStatus != "approved" || !listing.InTransaction {
|
|
|
|
|
return ErrListingUnavailable
|
|
|
|
|
}
|
|
|
|
|
var account model.GameAccount
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
total := order.RentAmount + order.DepositAmount
|
|
|
|
|
if err := wallet.AppendEntries(tx,
|
|
|
|
|
wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &order.ID,
|
|
|
|
|
Direction: "out",
|
|
|
|
|
Amount: total,
|
|
|
|
|
BalanceType: "available",
|
|
|
|
|
BizType: "order_pay",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "订单支付扣减可用余额",
|
|
|
|
|
},
|
|
|
|
|
wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &order.ID,
|
|
|
|
|
Direction: "in",
|
|
|
|
|
Amount: total,
|
|
|
|
|
BalanceType: "frozen",
|
|
|
|
|
BizType: "order_lock",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "订单支付冻结租金和押金",
|
|
|
|
|
},
|
|
|
|
|
); err != nil {
|
|
|
|
|
if errors.Is(err, wallet.ErrInsufficientBalance) {
|
|
|
|
|
return ErrInsufficientBalance
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
order.Status = "pending_handoff"
|
|
|
|
|
order.HandoffStatus = "pending_owner"
|
|
|
|
|
listing.Status = "rented"
|
|
|
|
|
account.Status = "rented"
|
|
|
|
|
conv, err := chat.EnsureOrderConversation(tx, order)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
newConvID = conv.ID
|
|
|
|
|
orderID := order.ID
|
|
|
|
|
if err := notification.Append(tx,
|
|
|
|
|
notification.Entry{
|
|
|
|
|
UserID: order.OwnerID,
|
|
|
|
|
Type: "order",
|
|
|
|
|
Title: "收到新的租号订单",
|
|
|
|
|
Content: "租客已完成支付,请尽快提交交接说明。",
|
|
|
|
|
BizType: "order",
|
|
|
|
|
BizID: &orderID,
|
|
|
|
|
},
|
|
|
|
|
notification.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
Type: "order",
|
|
|
|
|
Title: "订单支付成功",
|
|
|
|
|
Content: "支付金额已冻结,等待号主提交交接说明。",
|
|
|
|
|
BizType: "order",
|
|
|
|
|
BizID: &orderID,
|
|
|
|
|
},
|
|
|
|
|
); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Save(&order).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Save(&listing).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return tx.Save(&account).Error
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
// 事务成功后推送群聊创建事件
|
|
|
|
|
if newConvID > 0 && r.chatRepo != nil {
|
|
|
|
|
r.chatRepo.NotifyNewConversation(newConvID)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
return ErrChannelPaymentRequired
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ConfirmPaidFromChannel 在乐刷确认支付后推进订单状态;租客资金不进入站内钱包。
|
|
|
|
|
func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string) error {
|
|
|
|
|
var newConvID uint64
|
|
|
|
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
@@ -333,21 +249,8 @@ func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 租客已通过外部渠道付款,这里不写租客钱包流水。
|
|
|
|
|
orderID := order.ID
|
|
|
|
|
total := order.RentAmount + order.DepositAmount
|
|
|
|
|
if err := wallet.AppendEntries(tx, wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "in",
|
|
|
|
|
Amount: total,
|
|
|
|
|
BalanceType: "frozen",
|
|
|
|
|
BizType: "channel_order_lock",
|
|
|
|
|
BizNo: firstNonEmpty(providerBizNo, order.OrderNo),
|
|
|
|
|
Remark: "渠道支付成功冻结租金和押金",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
order.Status = "pending_handoff"
|
|
|
|
|
order.HandoffStatus = "pending_owner"
|
|
|
|
|
listing.Status = "rented"
|
|
|
|
@@ -370,7 +273,7 @@ func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
Type: "order",
|
|
|
|
|
Title: "订单支付成功",
|
|
|
|
|
Content: "支付金额已冻结,等待号主提交交接说明。",
|
|
|
|
|
Content: "支付已完成,等待号主提交交接说明。",
|
|
|
|
|
BizType: "order",
|
|
|
|
|
BizID: &orderID,
|
|
|
|
|
},
|
|
|
|
@@ -395,7 +298,8 @@ func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
|
|
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var refund *refundAction
|
|
|
|
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var order model.RentalOrder
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
|
|
|
Where("id = ? AND renter_id = ?", orderID, userID).
|
|
|
|
@@ -419,31 +323,12 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
|
|
|
|
order.HandoffStatus = "cancelled"
|
|
|
|
|
orderID := order.ID
|
|
|
|
|
if beforeStatus == "pending_handoff" {
|
|
|
|
|
total := order.RentAmount + order.DepositAmount
|
|
|
|
|
if err := wallet.AppendEntries(tx,
|
|
|
|
|
wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "out",
|
|
|
|
|
Amount: total,
|
|
|
|
|
BalanceType: "frozen",
|
|
|
|
|
BizType: "order_cancel",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "取消订单释放冻结金额",
|
|
|
|
|
},
|
|
|
|
|
wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "in",
|
|
|
|
|
Amount: total,
|
|
|
|
|
BalanceType: "available",
|
|
|
|
|
BizType: "order_cancel_refund",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "取消订单退回可用余额",
|
|
|
|
|
},
|
|
|
|
|
); err != nil {
|
|
|
|
|
totalCent := int64(math.Round((order.RentAmount + order.DepositAmount) * 100))
|
|
|
|
|
action, err := r.prepareRefund(&order, totalCent, "cancel_refund", "取消订单原路退款")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
refund = action
|
|
|
|
|
}
|
|
|
|
|
if err := notification.Append(tx,
|
|
|
|
|
notification.Entry{
|
|
|
|
@@ -458,7 +343,7 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
Type: "order",
|
|
|
|
|
Title: "订单取消成功",
|
|
|
|
|
Content: "订单已取消,相关金额已释放。",
|
|
|
|
|
Content: "订单已取消,退款将原路退回您的支付账户。",
|
|
|
|
|
BizType: "order",
|
|
|
|
|
BizID: &orderID,
|
|
|
|
|
},
|
|
|
|
@@ -476,6 +361,11 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
|
|
|
|
}
|
|
|
|
|
return tx.Save(&account).Error
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
r.startRefundBestEffort(refund)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) SubmitHandoff(userID uint64, orderID uint64, req SubmitHandoffRequest) (*HandoffRecordDTO, error) {
|
|
|
|
@@ -651,7 +541,8 @@ func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) ConfirmCheckout(userID uint64, orderID uint64) error {
|
|
|
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var refund *refundAction
|
|
|
|
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var order model.RentalOrder
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
@@ -677,8 +568,15 @@ func (r *Repository) ConfirmCheckout(userID uint64, orderID uint64) error {
|
|
|
|
|
}
|
|
|
|
|
checkout.Status = "accepted"
|
|
|
|
|
checkout.OwnerAdjustedAt = &now
|
|
|
|
|
return r.finalizeCheckout(tx, &order, &checkout, "号主已确认结账,订单完成。")
|
|
|
|
|
action, err := r.finalizeCheckout(tx, &order, &checkout, "号主已确认结账,订单完成。")
|
|
|
|
|
refund = action
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
r.startRefundBestEffort(refund)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterCheckoutRequest) (*CheckoutDTO, error) {
|
|
|
|
@@ -756,7 +654,8 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) AcceptCheckout(userID uint64, orderID uint64) error {
|
|
|
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var refund *refundAction
|
|
|
|
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var order model.RentalOrder
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
@@ -777,8 +676,15 @@ func (r *Repository) AcceptCheckout(userID uint64, orderID uint64) error {
|
|
|
|
|
now := time.Now()
|
|
|
|
|
checkout.Status = "accepted"
|
|
|
|
|
checkout.RenterConfirmedAt = &now
|
|
|
|
|
return r.finalizeCheckout(tx, &order, &checkout, "租客已确认修正结账,订单完成。")
|
|
|
|
|
action, err := r.finalizeCheckout(tx, &order, &checkout, "租客已确认修正结账,订单完成。")
|
|
|
|
|
refund = action
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
r.startRefundBestEffort(refund)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) ListForUser(userID uint64) ([]OrderDTO, error) {
|
|
|
|
@@ -840,7 +746,8 @@ func (r *Repository) HandoffRecordsAdmin(orderID uint64) ([]HandoffRecordDTO, er
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
|
|
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var refund *refundAction
|
|
|
|
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
@@ -862,38 +769,19 @@ func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionR
|
|
|
|
|
listing.InTransaction = false
|
|
|
|
|
account.Status = "offline"
|
|
|
|
|
if beforeOrderStatus != "pending_payment" {
|
|
|
|
|
total := order.RentAmount + order.DepositAmount
|
|
|
|
|
if err := wallet.AppendEntries(tx,
|
|
|
|
|
wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &order.ID,
|
|
|
|
|
Direction: "out",
|
|
|
|
|
Amount: total,
|
|
|
|
|
BalanceType: "frozen",
|
|
|
|
|
BizType: "admin_order_close",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "后台关闭订单释放冻结金额",
|
|
|
|
|
},
|
|
|
|
|
wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &order.ID,
|
|
|
|
|
Direction: "in",
|
|
|
|
|
Amount: total,
|
|
|
|
|
BalanceType: "available",
|
|
|
|
|
BizType: "admin_order_close_refund",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "后台关闭订单退回可用余额",
|
|
|
|
|
},
|
|
|
|
|
); err != nil {
|
|
|
|
|
totalCent := int64(math.Round((order.RentAmount + order.DepositAmount) * 100))
|
|
|
|
|
action, err := r.prepareRefund(order, totalCent, "admin_close_refund", "客服关闭订单原路退款")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
refund = action
|
|
|
|
|
}
|
|
|
|
|
if err := notification.Append(tx,
|
|
|
|
|
notification.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
Type: "order_admin",
|
|
|
|
|
Title: "订单已由客服关闭",
|
|
|
|
|
Content: "客服已关闭订单,模拟冻结金额已释放。原因:" + req.Reason,
|
|
|
|
|
Content: "客服已关闭订单,退款将原路退回您的支付账户。原因:" + req.Reason,
|
|
|
|
|
BizType: "order",
|
|
|
|
|
BizID: &order.ID,
|
|
|
|
|
},
|
|
|
|
@@ -935,6 +823,11 @@ func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionR
|
|
|
|
|
}
|
|
|
|
|
return tx.Save(account).Error
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
r.startRefundBestEffort(refund)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
|
|
|
@@ -1002,6 +895,57 @@ func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req Admin
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AdminRefund 触发后台人工退款,退款由 payment 模块走渠道原路退回。
|
|
|
|
|
func (r *Repository) AdminRefund(orderID uint64) (*RefundStatusDTO, error) {
|
|
|
|
|
var order model.RentalOrder
|
|
|
|
|
if err := r.db.First(&order, orderID).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if order.RefundStatus == "refunded" {
|
|
|
|
|
return r.buildRefundStatusDTO(&order), nil
|
|
|
|
|
}
|
|
|
|
|
if r.refundFunc == nil {
|
|
|
|
|
return nil, ErrDependencyUnavailable
|
|
|
|
|
}
|
|
|
|
|
totalCent := int64(math.Round((order.RentAmount + order.DepositAmount) * 100))
|
|
|
|
|
if totalCent <= 0 {
|
|
|
|
|
return nil, ErrInvalidCheckoutAmount
|
|
|
|
|
}
|
|
|
|
|
status, err := r.refundFunc(orderID, totalCent, "admin_refund", "后台人工退款")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
// 重新读取订单,拿到 payment 模块更新后的退款字段。
|
|
|
|
|
if err := r.db.First(&order, orderID).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
dto := r.buildRefundStatusDTO(&order)
|
|
|
|
|
if status != "" {
|
|
|
|
|
dto.RefundStatus = status
|
|
|
|
|
}
|
|
|
|
|
return dto, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AdminRefundStatus 查询订单退款状态。
|
|
|
|
|
func (r *Repository) AdminRefundStatus(orderID uint64) (*RefundStatusDTO, error) {
|
|
|
|
|
var order model.RentalOrder
|
|
|
|
|
if err := r.db.First(&order, orderID).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return r.buildRefundStatusDTO(&order), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) buildRefundStatusDTO(order *model.RentalOrder) *RefundStatusDTO {
|
|
|
|
|
return &RefundStatusDTO{
|
|
|
|
|
OrderID: order.ID,
|
|
|
|
|
OrderNo: order.OrderNo,
|
|
|
|
|
RefundStatus: order.RefundStatus,
|
|
|
|
|
RefundAmountCent: order.RefundAmountCent,
|
|
|
|
|
RefundedAt: order.RefundedAt,
|
|
|
|
|
TotalAmount: order.RentAmount + order.DepositAmount,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, error) {
|
|
|
|
|
var row orderRow
|
|
|
|
|
if err := r.baseQuery().
|
|
|
|
@@ -1014,14 +958,14 @@ func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, erro
|
|
|
|
|
return &dto, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, checkout *model.OrderCheckout, renterContent string) error {
|
|
|
|
|
func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, checkout *model.OrderCheckout, renterContent string) (*refundAction, error) {
|
|
|
|
|
var listing model.RentalListing
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
var account model.GameAccount
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
now := time.Now()
|
|
|
|
|
order.Status = "completed"
|
|
|
|
@@ -1034,20 +978,11 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
|
|
|
|
account.Status = "published"
|
|
|
|
|
orderID := order.ID
|
|
|
|
|
settlement := buildCheckoutSettlement(*order, checkout)
|
|
|
|
|
entries := []wallet.Entry{
|
|
|
|
|
wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "out",
|
|
|
|
|
Amount: order.RentAmount + order.DepositAmount,
|
|
|
|
|
BalanceType: "frozen",
|
|
|
|
|
BizType: "order_settle",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "订单结账释放冻结金额",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 卖家收入进入站内钱包;租客资金不进入站内钱包。
|
|
|
|
|
var ownerEntries []wallet.Entry
|
|
|
|
|
if settlement.OwnerRentIncome > 0 {
|
|
|
|
|
entries = append(entries, wallet.Entry{
|
|
|
|
|
ownerEntries = append(ownerEntries, wallet.Entry{
|
|
|
|
|
UserID: order.OwnerID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "in",
|
|
|
|
@@ -1059,7 +994,7 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if settlement.DepositCompensation > 0 {
|
|
|
|
|
entries = append(entries, wallet.Entry{
|
|
|
|
|
ownerEntries = append(ownerEntries, wallet.Entry{
|
|
|
|
|
UserID: order.OwnerID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "in",
|
|
|
|
@@ -1070,33 +1005,23 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
|
|
|
|
Remark: "订单结账押金赔付",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if settlement.RentRefund > 0 {
|
|
|
|
|
entries = append(entries, wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "in",
|
|
|
|
|
Amount: settlement.RentRefund,
|
|
|
|
|
BalanceType: "available",
|
|
|
|
|
BizType: "rent_refund",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "订单结账退回未使用租金",
|
|
|
|
|
})
|
|
|
|
|
if len(ownerEntries) > 0 {
|
|
|
|
|
if err := wallet.AppendEntries(tx, ownerEntries...); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if settlement.DepositRefund > 0 {
|
|
|
|
|
entries = append(entries, wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "in",
|
|
|
|
|
Amount: settlement.DepositRefund,
|
|
|
|
|
BalanceType: "available",
|
|
|
|
|
BizType: "deposit_release",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "订单结账退回押金",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if err := wallet.AppendEntries(tx, entries...); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
var refund *refundAction
|
|
|
|
|
renterRefundTotal := settlement.RentRefund + settlement.DepositRefund
|
|
|
|
|
if renterRefundTotal > 0 {
|
|
|
|
|
refundCent := int64(math.Round(renterRefundTotal * 100))
|
|
|
|
|
action, err := r.prepareRefund(order, refundCent, "checkout_refund", "结账退款原路退还")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
refund = action
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
checkout.RentAmount = settlement.ActualRentAmount
|
|
|
|
|
checkout.OwnerRentAmount = settlement.OwnerRentIncome
|
|
|
|
|
checkout.PlatformFee = settlement.PlatformFee
|
|
|
|
@@ -1120,18 +1045,48 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
|
|
|
|
BizID: &orderID,
|
|
|
|
|
},
|
|
|
|
|
); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Save(order).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Save(checkout).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Save(&listing).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Save(&account).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return refund, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) prepareRefund(order *model.RentalOrder, amountCent int64, bizType string, remark string) (*refundAction, error) {
|
|
|
|
|
if amountCent <= 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if r.refundFunc == nil {
|
|
|
|
|
return nil, ErrDependencyUnavailable
|
|
|
|
|
}
|
|
|
|
|
order.RefundStatus = "pending"
|
|
|
|
|
order.RefundAmountCent = amountCent
|
|
|
|
|
order.RefundedAt = nil
|
|
|
|
|
return &refundAction{
|
|
|
|
|
OrderID: order.ID,
|
|
|
|
|
RefundAmountCent: amountCent,
|
|
|
|
|
BizType: bizType,
|
|
|
|
|
Remark: remark,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) startRefundBestEffort(action *refundAction) {
|
|
|
|
|
if action == nil || r.refundFunc == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if _, err := r.refundFunc(action.OrderID, action.RefundAmountCent, action.BizType, action.Remark); err != nil {
|
|
|
|
|
log.Printf("[order] start refund failed order_id=%d biz_type=%s amount_cent=%d err=%v", action.OrderID, action.BizType, action.RefundAmountCent, err)
|
|
|
|
|
}
|
|
|
|
|
return tx.Save(&account).Error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) findOrderAssetsForAdminUpdate(tx *gorm.DB, orderID uint64) (*model.RentalOrder, *model.RentalListing, *model.GameAccount, error) {
|
|
|
|
|