修复结账金额和后台类型
This commit is contained in:
@@ -40,6 +40,17 @@ type orderPricing struct {
|
||||
PlatformFee float64
|
||||
}
|
||||
|
||||
type checkoutSettlement struct {
|
||||
OwnerRentIncome float64
|
||||
DepositCompensation float64
|
||||
OwnerIncome float64
|
||||
RentRefund float64
|
||||
DepositRefund float64
|
||||
RenterRefund float64
|
||||
PlatformFee float64
|
||||
ActualRentAmount float64
|
||||
}
|
||||
|
||||
func orderDurationHours(order model.RentalOrder) int {
|
||||
if order.EstimatedDurationHours > 0 {
|
||||
return order.EstimatedDurationHours
|
||||
@@ -68,14 +79,21 @@ func buildOrderPricing(listing model.RentalListing, account model.GameAccount) o
|
||||
}
|
||||
|
||||
func readSnapshotPrice(raw datatypes.JSON, key string) float64 {
|
||||
return readOrderSnapshotPrice(raw, key)
|
||||
}
|
||||
|
||||
func readOrderSnapshotPrice(raw datatypes.JSON, key string) float64 {
|
||||
if len(raw) == 0 {
|
||||
return 0
|
||||
}
|
||||
var summary map[string]any
|
||||
if err := json.Unmarshal(raw, &summary); err != nil {
|
||||
var snapshot map[string]any
|
||||
if err := json.Unmarshal(raw, &snapshot); err != nil {
|
||||
return 0
|
||||
}
|
||||
breakdown, ok := summary["price_breakdown"].(map[string]any)
|
||||
if assetSummary, ok := snapshot["asset_summary"].(map[string]any); ok {
|
||||
snapshot = assetSummary
|
||||
}
|
||||
breakdown, ok := snapshot["price_breakdown"].(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
@@ -927,7 +945,8 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
||||
listing.InTransaction = false
|
||||
account.Status = "published"
|
||||
orderID := order.ID
|
||||
if err := wallet.AppendEntries(tx,
|
||||
settlement := buildCheckoutSettlement(*order, checkout)
|
||||
entries := []wallet.Entry{
|
||||
wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
@@ -938,29 +957,63 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账释放冻结金额",
|
||||
},
|
||||
wallet.Entry{
|
||||
}
|
||||
if settlement.OwnerRentIncome > 0 {
|
||||
entries = append(entries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
Amount: checkout.OwnerIncomeAmount,
|
||||
Amount: settlement.OwnerRentIncome,
|
||||
BalanceType: "available",
|
||||
BizType: "owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账收入",
|
||||
},
|
||||
wallet.Entry{
|
||||
Remark: "订单结账租金收入",
|
||||
})
|
||||
}
|
||||
if settlement.DepositCompensation > 0 {
|
||||
entries = append(entries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
Amount: settlement.DepositCompensation,
|
||||
BalanceType: "available",
|
||||
BizType: "deposit_compensation",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账押金赔付",
|
||||
})
|
||||
}
|
||||
if settlement.RentRefund > 0 {
|
||||
entries = append(entries, wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
Amount: checkout.RenterRefundAmount,
|
||||
Amount: settlement.RentRefund,
|
||||
BalanceType: "available",
|
||||
BizType: "rent_refund",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账退回未使用租金",
|
||||
})
|
||||
}
|
||||
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: "订单结账退回押金",
|
||||
},
|
||||
); err != nil {
|
||||
})
|
||||
}
|
||||
if err := wallet.AppendEntries(tx, entries...); err != nil {
|
||||
return err
|
||||
}
|
||||
checkout.RentAmount = settlement.ActualRentAmount
|
||||
checkout.OwnerRentAmount = settlement.OwnerRentIncome
|
||||
checkout.PlatformFee = settlement.PlatformFee
|
||||
checkout.RenterRefundAmount = settlement.RenterRefund
|
||||
checkout.OwnerIncomeAmount = settlement.OwnerIncome
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
@@ -1049,7 +1102,7 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
|
||||
if deductAmount > order.DepositAmount {
|
||||
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
|
||||
}
|
||||
renterRefund := order.DepositAmount - deductAmount
|
||||
settlement := calculateCheckoutSettlement(order, consumableAmount, roundQuantity(coinConsumedM), deductAmount)
|
||||
evidence, err := marshalStringList(evidenceURLS)
|
||||
if err != nil {
|
||||
return model.OrderCheckout{}, err
|
||||
@@ -1058,21 +1111,80 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
|
||||
OrderID: order.ID,
|
||||
InitiatedBy: initiatedBy,
|
||||
Status: status,
|
||||
RentAmount: order.RentAmount,
|
||||
OwnerRentAmount: order.OwnerRentAmount,
|
||||
PlatformFee: order.PlatformFee,
|
||||
RentAmount: settlement.ActualRentAmount,
|
||||
OwnerRentAmount: settlement.OwnerRentIncome,
|
||||
PlatformFee: settlement.PlatformFee,
|
||||
DepositAmount: order.DepositAmount,
|
||||
ConsumableAmount: consumableAmount,
|
||||
CoinConsumedM: roundQuantity(coinConsumedM),
|
||||
OtherAmount: otherAmount,
|
||||
DepositDeductAmount: roundMoney(deductAmount),
|
||||
RenterRefundAmount: roundMoney(renterRefund),
|
||||
OwnerIncomeAmount: roundMoney(order.OwnerRentAmount + deductAmount),
|
||||
RenterRefundAmount: settlement.RenterRefund,
|
||||
OwnerIncomeAmount: settlement.OwnerIncome,
|
||||
Content: content,
|
||||
EvidenceURLS: evidence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement {
|
||||
return calculateCheckoutSettlement(order, checkout.ConsumableAmount, checkout.CoinConsumedM, checkout.DepositDeductAmount)
|
||||
}
|
||||
|
||||
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmount float64, coinConsumedM float64, depositDeductAmount float64) checkoutSettlement {
|
||||
buyerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price")
|
||||
if buyerCoinBasePrice <= 0 || buyerCoinBasePrice > order.RentAmount {
|
||||
buyerCoinBasePrice = order.RentAmount
|
||||
}
|
||||
sellerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price")
|
||||
if sellerCoinBasePrice <= 0 || sellerCoinBasePrice > order.OwnerRentAmount {
|
||||
sellerCoinBasePrice = order.OwnerRentAmount
|
||||
}
|
||||
prepaidConsumablePrice := readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price")
|
||||
if prepaidConsumablePrice <= 0 || prepaidConsumablePrice > order.RentAmount-buyerCoinBasePrice {
|
||||
prepaidConsumablePrice = maxMoney(order.RentAmount-buyerCoinBasePrice, 0)
|
||||
}
|
||||
prepaidOwnerConsumablePrice := maxMoney(order.OwnerRentAmount-sellerCoinBasePrice, 0)
|
||||
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
|
||||
coinUseRatio := 1.0
|
||||
if totalCoinM > 0 {
|
||||
coinUseRatio = minRatio(maxRatio(roundQuantity(coinConsumedM)/totalCoinM, 0), 1)
|
||||
}
|
||||
usedBuyerCoinPrice := roundMoney(buyerCoinBasePrice * coinUseRatio)
|
||||
usedOwnerCoinPrice := roundMoney(sellerCoinBasePrice * coinUseRatio)
|
||||
usedBuyerConsumablePrice := minMoney(roundMoney(consumableAmount), prepaidConsumablePrice)
|
||||
consumableUseRatio := 1.0
|
||||
if prepaidConsumablePrice > 0 {
|
||||
consumableUseRatio = minRatio(maxRatio(usedBuyerConsumablePrice/prepaidConsumablePrice, 0), 1)
|
||||
}
|
||||
usedOwnerConsumablePrice := roundMoney(prepaidOwnerConsumablePrice * consumableUseRatio)
|
||||
actualRentAmount := minMoney(roundMoney(usedBuyerCoinPrice+usedBuyerConsumablePrice), order.RentAmount)
|
||||
ownerRentIncome := minMoney(roundMoney(usedOwnerCoinPrice+usedOwnerConsumablePrice), order.OwnerRentAmount)
|
||||
depositCompensation := minMoney(roundMoney(depositDeductAmount), order.DepositAmount)
|
||||
rentRefund := maxMoney(order.RentAmount-actualRentAmount, 0)
|
||||
depositRefund := maxMoney(order.DepositAmount-depositCompensation, 0)
|
||||
return checkoutSettlement{
|
||||
OwnerRentIncome: ownerRentIncome,
|
||||
DepositCompensation: depositCompensation,
|
||||
OwnerIncome: roundMoney(ownerRentIncome + depositCompensation),
|
||||
RentRefund: rentRefund,
|
||||
DepositRefund: depositRefund,
|
||||
RenterRefund: roundMoney(rentRefund + depositRefund),
|
||||
PlatformFee: maxMoney(actualRentAmount-ownerRentIncome, 0),
|
||||
ActualRentAmount: actualRentAmount,
|
||||
}
|
||||
}
|
||||
|
||||
func readOrderSnapshotCoinM(raw datatypes.JSON) float64 {
|
||||
if len(raw) == 0 {
|
||||
return 0
|
||||
}
|
||||
var snapshot map[string]any
|
||||
if err := json.Unmarshal(raw, &snapshot); err != nil {
|
||||
return 0
|
||||
}
|
||||
return roundQuantity(readJSONNumber(snapshot["haf_coin_amount"]) / 1000000)
|
||||
}
|
||||
|
||||
func marshalStringList(items []string) (datatypes.JSON, error) {
|
||||
if items == nil {
|
||||
items = []string{}
|
||||
@@ -1100,6 +1212,34 @@ func roundQuantity(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
func minMoney(a float64, b float64) float64 {
|
||||
if a < b {
|
||||
return roundMoney(a)
|
||||
}
|
||||
return roundMoney(b)
|
||||
}
|
||||
|
||||
func maxMoney(a float64, b float64) float64 {
|
||||
if a > b {
|
||||
return roundMoney(a)
|
||||
}
|
||||
return roundMoney(b)
|
||||
}
|
||||
|
||||
func minRatio(a float64, b float64) float64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func maxRatio(a float64, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func hasOpenCheckout(tx *gorm.DB, orderID uint64) (bool, error) {
|
||||
var count int64
|
||||
err := tx.Model(&model.OrderCheckout{}).
|
||||
@@ -1288,27 +1428,33 @@ func applyCheckoutPriceView(dto *CheckoutDTO, order model.RentalOrder, userID ui
|
||||
if dto == nil {
|
||||
return
|
||||
}
|
||||
ownerAmount := 0.0
|
||||
if dto.OwnerRentAmount != nil {
|
||||
ownerAmount = *dto.OwnerRentAmount
|
||||
}
|
||||
ownerIncomeAmount := 0.0
|
||||
if dto.OwnerIncomeAmount != nil {
|
||||
ownerIncomeAmount = *dto.OwnerIncomeAmount
|
||||
}
|
||||
rentAmount := 0.0
|
||||
if dto.RentAmount != nil {
|
||||
rentAmount = *dto.RentAmount
|
||||
}
|
||||
renterRefundAmount := 0.0
|
||||
if dto.RenterRefundAmount != nil {
|
||||
renterRefundAmount = *dto.RenterRefundAmount
|
||||
}
|
||||
dto.PlatformFee = nil
|
||||
dto.RenterRefundAmount = nil
|
||||
dto.OwnerIncomeAmount = nil
|
||||
switch {
|
||||
case userID == order.OwnerID:
|
||||
ownerAmount := order.OwnerRentAmount
|
||||
if ownerAmount <= 0 {
|
||||
ownerAmount = order.RentAmount
|
||||
}
|
||||
ownerIncomeAmount := dto.DepositDeductAmount + ownerAmount
|
||||
dto.PriceRole = "owner"
|
||||
dto.DisplayAmount = ownerAmount
|
||||
dto.RentAmount = nil
|
||||
dto.OwnerRentAmount = &ownerAmount
|
||||
dto.OwnerIncomeAmount = &ownerIncomeAmount
|
||||
case userID == order.RenterID:
|
||||
rentAmount := order.RentAmount
|
||||
renterRefundAmount := order.DepositAmount - dto.DepositDeductAmount
|
||||
if renterRefundAmount < 0 {
|
||||
renterRefundAmount = 0
|
||||
}
|
||||
dto.PriceRole = "renter"
|
||||
dto.DisplayAmount = rentAmount
|
||||
dto.RentAmount = &rentAmount
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
RentAmount: 383,
|
||||
OwnerRentAmount: 353,
|
||||
DepositAmount: 150,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 100000000,
|
||||
"asset_summary": {
|
||||
"price_breakdown": {
|
||||
"buyer_coin_base_price": 263,
|
||||
"seller_coin_base_price": 233,
|
||||
"consumable_price": 120
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
}
|
||||
|
||||
settlement := calculateCheckoutSettlement(order, 7, 90, 0)
|
||||
|
||||
if settlement.ActualRentAmount != 244 {
|
||||
t.Fatalf("ActualRentAmount = %.2f, want 244.00", settlement.ActualRentAmount)
|
||||
}
|
||||
if settlement.OwnerRentIncome != 217 {
|
||||
t.Fatalf("OwnerRentIncome = %.2f, want 217.00", settlement.OwnerRentIncome)
|
||||
}
|
||||
if settlement.PlatformFee != 27 {
|
||||
t.Fatalf("PlatformFee = %.2f, want 27.00", settlement.PlatformFee)
|
||||
}
|
||||
if settlement.RentRefund != 139 {
|
||||
t.Fatalf("RentRefund = %.2f, want 139.00", settlement.RentRefund)
|
||||
}
|
||||
if settlement.DepositRefund != 150 {
|
||||
t.Fatalf("DepositRefund = %.2f, want 150.00", settlement.DepositRefund)
|
||||
}
|
||||
if settlement.RenterRefund != 289 {
|
||||
t.Fatalf("RenterRefund = %.2f, want 289.00", settlement.RenterRefund)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateCheckoutSettlementUsesBuyerAndSellerRatiosSeparately(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
RentAmount: 383,
|
||||
OwnerRentAmount: 353,
|
||||
DepositAmount: 150,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 100000000,
|
||||
"asset_summary": {
|
||||
"price_breakdown": {
|
||||
"buyer_coin_base_price": 263,
|
||||
"seller_coin_base_price": 233,
|
||||
"consumable_price": 120
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
}
|
||||
|
||||
settlement := calculateCheckoutSettlement(order, 0, 50, 0)
|
||||
|
||||
if settlement.ActualRentAmount != 132 {
|
||||
t.Fatalf("租客侧实际租金 = %.2f, want 132.00", settlement.ActualRentAmount)
|
||||
}
|
||||
if settlement.OwnerRentIncome != 117 {
|
||||
t.Fatalf("卖家侧租金收入 = %.2f, want 117.00", settlement.OwnerRentIncome)
|
||||
}
|
||||
if settlement.PlatformFee != 15 {
|
||||
t.Fatalf("平台差价 = %.2f, want 15.00", settlement.PlatformFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
RentAmount: 383,
|
||||
OwnerRentAmount: 353,
|
||||
DepositAmount: 150,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 100000000,
|
||||
"asset_summary": {
|
||||
"price_breakdown": {
|
||||
"buyer_coin_base_price": 263,
|
||||
"seller_coin_base_price": 233,
|
||||
"consumable_price": 120
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
}
|
||||
|
||||
settlement := calculateCheckoutSettlement(order, 120, 100, 30)
|
||||
|
||||
if settlement.ActualRentAmount != 383 {
|
||||
t.Fatalf("ActualRentAmount = %.2f, want 383.00", settlement.ActualRentAmount)
|
||||
}
|
||||
if settlement.OwnerRentIncome != 353 {
|
||||
t.Fatalf("OwnerRentIncome = %.2f, want 353.00", settlement.OwnerRentIncome)
|
||||
}
|
||||
if settlement.DepositCompensation != 30 {
|
||||
t.Fatalf("DepositCompensation = %.2f, want 30.00", settlement.DepositCompensation)
|
||||
}
|
||||
if settlement.OwnerIncome != 383 {
|
||||
t.Fatalf("OwnerIncome = %.2f, want 383.00", settlement.OwnerIncome)
|
||||
}
|
||||
if settlement.RenterRefund != 120 {
|
||||
t.Fatalf("RenterRefund = %.2f, want 120.00", settlement.RenterRefund)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user