package order import ( "testing" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/rentergrowth" "hfb_sys/backend/internal/timeutil" "gorm.io/datatypes" ) func pureCoinDiscountOrder() model.RentalOrder { return model.RentalOrder{ RentAmountCent: 38037, OwnerRentAmountCent: 35300, DepositAmountCent: 15000, RentOriginalAmountCent: 38300, RentDiscountAmountCent: 263, PureCoinOriginalAmountCent: 26300, ExtraItemOriginalAmountCent: 12000, RenterDiscountBps: 9900, 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 } } }`)), } } func TestCalculateCheckoutSettlementUsesOnlyActualPureCoinForDiscount(t *testing.T) { tests := []struct { name string consumableAmountCent int64 coinConsumedM float64 wantPureCoinCent int64 wantDiscountCent int64 wantActualRentCent int64 wantOvershootCent int64 }{ { name: "全量纯币和额外物品", consumableAmountCent: 12000, coinConsumedM: 100, wantPureCoinCent: 26300, wantDiscountCent: 263, wantActualRentCent: 38037, wantOvershootCent: 0, }, { name: "部分纯币和全量额外物品", consumableAmountCent: 12000, coinConsumedM: 50, wantPureCoinCent: 13150, wantDiscountCent: 132, wantActualRentCent: 25018, wantOvershootCent: 0, }, { name: "只使用额外物品", consumableAmountCent: 12000, coinConsumedM: 0, wantPureCoinCent: 0, wantDiscountCent: 0, wantActualRentCent: 12000, wantOvershootCent: 0, }, { name: "纯币打超仍参与优惠", consumableAmountCent: 12000, coinConsumedM: 110, wantPureCoinCent: 28930, wantDiscountCent: 290, wantActualRentCent: 40640, wantOvershootCent: 2603, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { settlement := calculateCheckoutSettlement(pureCoinDiscountOrder(), test.consumableAmountCent, test.coinConsumedM, 0) if settlement.PureCoinAmountCent != test.wantPureCoinCent || settlement.PureCoinDiscountCent != test.wantDiscountCent { t.Fatalf("纯币原价/优惠 = %d/%d, want %d/%d", settlement.PureCoinAmountCent, settlement.PureCoinDiscountCent, test.wantPureCoinCent, test.wantDiscountCent) } if settlement.ActualRentAmountCent != test.wantActualRentCent || settlement.OvershootAmountCent != test.wantOvershootCent { t.Fatalf("实际租金/打超 = %d/%d, want %d/%d", settlement.ActualRentAmountCent, settlement.OvershootAmountCent, test.wantActualRentCent, test.wantOvershootCent) } }) } } func TestBuildOrderPricingDoesNotDiscountAllExtraItemOrder(t *testing.T) { pricing := buildOrderPricing( model.RentalListing{PriceCent: 12000}, model.GameAccount{AssetSummary: datatypes.JSON([]byte(`{ "price_breakdown": { "buyer_coin_base_price": 0, "seller_coin_base_price": 0, "consumable_price": 120, "seller_total_price": 120 } }`))}, ) if pricing.PureCoinAmountCent != 0 || pricing.ExtraItemAmountCent != 12000 || pricing.PureCoinPlatformFeeCent != 0 { t.Fatalf("全额外物品订单纯币/额外物品/纯币差价 = %d/%d/%d, want 0/12000/0", pricing.PureCoinAmountCent, pricing.ExtraItemAmountCent, pricing.PureCoinPlatformFeeCent) } if discount := rentergrowth.CalculateDiscountCent(pricing.PureCoinAmountCent, pricing.PureCoinPlatformFeeCent, 9900); discount != 0 { t.Fatalf("全额外物品订单优惠 = %d, want 0", discount) } } func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) { order := model.RentalOrder{ RentAmountCent: 38300, OwnerRentAmountCent: 35300, DepositAmountCent: 15000, 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, 700, 90, 0) // 角精度:243.7 = roundMoney(236.7 + 7) if settlement.ActualRentAmountCent != 24370 { t.Fatalf("ActualRentAmountCent = %d, want 24370", settlement.ActualRentAmountCent) } // 角精度:216.7 = roundMoney(209.7 + 7) 卖家币价+消耗品 if settlement.OwnerRentIncomeCent != 21670 { t.Fatalf("OwnerRentIncomeCent = %d, want 21670", settlement.OwnerRentIncomeCent) } // 角精度:27.0 = roundMoney(27) 平台费 if settlement.PlatformFeeCent != 2700 { t.Fatalf("PlatformFeeCent = %d, want 2700", settlement.PlatformFeeCent) } // 角精度:139.3 = roundMoney(383 - 243.7) if settlement.RentRefundCent != 13930 { t.Fatalf("RentRefundCent = %d, want 13930", settlement.RentRefundCent) } if settlement.DepositRefundCent != 15000 { t.Fatalf("DepositRefundCent = %d, want 15000", settlement.DepositRefundCent) } // 角精度:289.3 = roundMoney(139.3 + 150) if settlement.RenterRefundCent != 28930 { t.Fatalf("RenterRefundCent = %d, want 28930", settlement.RenterRefundCent) } } func TestCalculateCheckoutSettlementUsesBuyerAndSellerRatiosSeparately(t *testing.T) { order := model.RentalOrder{ RentAmountCent: 38300, OwnerRentAmountCent: 35300, DepositAmountCent: 15000, 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) // 角精度:131.5 = roundMoney(131.5) if settlement.ActualRentAmountCent != 13150 { t.Fatalf("租客侧实际租金 = %d, want 13150", settlement.ActualRentAmountCent) } // 角精度:116.5 = roundMoney(116.5) if settlement.OwnerRentIncomeCent != 11650 { t.Fatalf("卖家侧租金收入 = %d, want 11650", settlement.OwnerRentIncomeCent) } // 角精度:15.0 = roundMoney(15) if settlement.PlatformFeeCent != 1500 { t.Fatalf("平台差价 = %d, want 1500", settlement.PlatformFeeCent) } } func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) { order := model.RentalOrder{ RentAmountCent: 38300, OwnerRentAmountCent: 35300, DepositAmountCent: 15000, 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, 12000, 100, 3000) if settlement.ActualRentAmountCent != 38300 { t.Fatalf("ActualRentAmountCent = %d, want 38300", settlement.ActualRentAmountCent) } if settlement.OwnerRentIncomeCent != 35300 { t.Fatalf("OwnerRentIncomeCent = %d, want 35300", settlement.OwnerRentIncomeCent) } if settlement.DepositCompensationCent != 3000 { t.Fatalf("DepositCompensationCent = %d, want 3000", settlement.DepositCompensationCent) } if settlement.OwnerIncomeCent != 38300 { t.Fatalf("OwnerIncomeCent = %d, want 38300", settlement.OwnerIncomeCent) } if settlement.RenterRefundCent != 12000 { t.Fatalf("RenterRefundCent = %d, want 12000", settlement.RenterRefundCent) } } func TestCalculateCheckoutSettlementAllowsOvershootFromDeposit(t *testing.T) { // 发布 100M,实际消耗 180M;按基价线性外推,打超从押金扣 order := model.RentalOrder{ RentAmountCent: 26300, // 仅币预收(无消耗品) OwnerRentAmountCent: 23300, DepositAmountCent: 30000, AccountSnapshot: datatypes.JSON([]byte(`{ "haf_coin_amount": 100000000, "asset_summary": { "price_breakdown": { "buyer_coin_base_price": 263, "seller_coin_base_price": 233, "consumable_price": 0 } } }`)), } settlement := calculateCheckoutSettlement(order, 0, 180, 0) // 180/100 * 26300 = 47340 if settlement.ActualRentAmountCent != 47340 { t.Fatalf("ActualRentAmountCent = %d, want 47340", settlement.ActualRentAmountCent) } if settlement.OwnerRentIncomeCent != 41940 { // 180/100 * 23300 t.Fatalf("OwnerRentIncomeCent = %d, want 41940", settlement.OwnerRentIncomeCent) } if settlement.OvershootAmountCent != 21040 { // 47340 - 26300 t.Fatalf("OvershootAmountCent = %d, want 21040", settlement.OvershootAmountCent) } if settlement.ShortfallCent != 0 { t.Fatalf("ShortfallCent = %d, want 0", settlement.ShortfallCent) } if settlement.RentRefundCent != 0 { t.Fatalf("RentRefundCent = %d, want 0", settlement.RentRefundCent) } // 押金 30000 - 打超 21040 = 8960 if settlement.DepositRefundCent != 8960 { t.Fatalf("DepositRefundCent = %d, want 8960", settlement.DepositRefundCent) } } func TestCalculateCheckoutSettlementShortfallWhenDepositInsufficient(t *testing.T) { order := model.RentalOrder{ RentAmountCent: 26300, OwnerRentAmountCent: 23300, DepositAmountCent: 5000, AccountSnapshot: datatypes.JSON([]byte(`{ "haf_coin_amount": 100000000, "asset_summary": { "price_breakdown": { "buyer_coin_base_price": 263, "seller_coin_base_price": 233, "consumable_price": 0 } } }`)), } settlement := calculateCheckoutSettlement(order, 0, 180, 0) // overshoot 21040, deposit 5000 → shortfall 16040 if settlement.OvershootAmountCent != 21040 { t.Fatalf("OvershootAmountCent = %d, want 21040", settlement.OvershootAmountCent) } if settlement.ShortfallCent != 16040 { t.Fatalf("ShortfallCent = %d, want 16040", settlement.ShortfallCent) } if settlement.DepositRefundCent != 0 { t.Fatalf("DepositRefundCent = %d, want 0", settlement.DepositRefundCent) } } func TestCalculateCheckoutSettlementUsesExplicitRatiosForActualConsume(t *testing.T) { // buyer 1:40, seller 1:45;消耗 180M // 租客: 180*100/40 = 450 元;号主: 180*100/45 = 400 元 order := model.RentalOrder{ RentAmountCent: 25000, // 100M 预收 OwnerRentAmountCent: 22222, DepositAmountCent: 50000, AccountSnapshot: datatypes.JSON([]byte(`{ "haf_coin_amount": 100000000, "asset_summary": { "price_breakdown": { "buyer_ratio": 40, "seller_ratio": 45, "buyer_coin_base_price": 250, "seller_coin_base_price": 222.22, "consumable_price": 0 } } }`)), } settlement := calculateCheckoutSettlement(order, 0, 180, 0) if settlement.ActualRentAmountCent != 45000 { t.Fatalf("ActualRentAmountCent = %d, want 45000", settlement.ActualRentAmountCent) } if settlement.OwnerRentIncomeCent != 40000 { t.Fatalf("OwnerRentIncomeCent = %d, want 40000", settlement.OwnerRentIncomeCent) } if settlement.PlatformFeeCent != 5000 { t.Fatalf("PlatformFeeCent = %d, want 5000", settlement.PlatformFeeCent) } if settlement.OvershootAmountCent != 20000 { t.Fatalf("OvershootAmountCent = %d, want 20000", settlement.OvershootAmountCent) } if settlement.ShortfallCent != 0 { t.Fatalf("ShortfallCent = %d, want 0", settlement.ShortfallCent) } } func TestCalculateCheckoutSettlementCapsFullUseRatioRounding(t *testing.T) { // 发布量刚好用完时,比例换算的四舍五入不能把线下待打款抬高。 order := model.RentalOrder{ RentAmountCent: 37210, OwnerRentAmountCent: 34000, PlatformFeeCent: 3210, DepositAmountCent: 50000, AccountSnapshot: datatypes.JSON([]byte(`{ "haf_coin_amount": 160000000, "asset_summary": { "price_breakdown": { "buyer_ratio": 43, "seller_ratio": 47, "buyer_coin_base_price": 372.1, "seller_coin_base_price": 340, "consumable_price": 0 } } }`)), } settlement := calculateCheckoutSettlement(order, 0, 160, 0) if settlement.ActualRentAmountCent != order.RentAmountCent { t.Fatalf("ActualRentAmountCent = %d, want %d", settlement.ActualRentAmountCent, order.RentAmountCent) } if settlement.OwnerRentIncomeCent != order.OwnerRentAmountCent { t.Fatalf("OwnerRentIncomeCent = %d, want %d", settlement.OwnerRentIncomeCent, order.OwnerRentAmountCent) } if settlement.PlatformFeeCent != order.PlatformFeeCent { t.Fatalf("PlatformFeeCent = %d, want %d", settlement.PlatformFeeCent, order.PlatformFeeCent) } if settlement.OvershootAmountCent != 0 { t.Fatalf("OvershootAmountCent = %d, want 0", settlement.OvershootAmountCent) } } func TestCalculateDepositWaiverUsesSharedRemainingQuota(t *testing.T) { paid, waived := calculateDepositWaiver(500, 300, 0) if paid != 200 || waived != 300 { t.Fatalf("paid = %d, waived = %d, want 200, 300", paid, waived) } paid, waived = calculateDepositWaiver(500, 300, 200) if paid != 400 || waived != 100 { t.Fatalf("paid = %d, waived = %d, want 400, 100", paid, waived) } paid, waived = calculateDepositWaiver(500, 300, 300) if paid != 500 || waived != 0 { t.Fatalf("paid = %d, waived = %d, want 500, 0", paid, waived) } } func TestArchiveAssetsMovesListingOffline(t *testing.T) { now := time.Now() listing := model.RentalListing{ Status: listingStatusRented, InTransaction: true, PublishedAt: &now, } account := model.GameAccount{Status: accountStatusRented} if err := archiveAssets(nil, &listing, &account); err != nil { t.Fatalf("archiveAssets: %v", err) } if listing.Status != listingStatusOffline { t.Fatalf("listing.Status = %q, want %q", listing.Status, listingStatusOffline) } if listing.InTransaction { t.Fatal("listing.InTransaction = true, want false") } if listing.PublishedAt != nil { t.Fatal("listing.PublishedAt is not nil") } if account.Status != accountStatusOffline { t.Fatalf("account.Status = %q, want %q", account.Status, accountStatusOffline) } } func TestCompleteAssetsMovesListingCompleted(t *testing.T) { now := time.Now() listing := model.RentalListing{ Status: listingStatusRented, InTransaction: true, PublishedAt: &now, } account := model.GameAccount{Status: accountStatusRented} if err := completeAssets(nil, &listing, &account); err != nil { t.Fatalf("completeAssets: %v", err) } if listing.Status != listingStatusCompleted { t.Fatalf("listing.Status = %q, want %q", listing.Status, listingStatusCompleted) } if listing.InTransaction { t.Fatal("listing.InTransaction = true, want false") } if listing.PublishedAt != nil { t.Fatal("listing.PublishedAt is not nil") } if account.Status != accountStatusOffline { t.Fatalf("account.Status = %q, want %q", account.Status, accountStatusOffline) } } func TestNewOrderNoUsesShanghaiTimeWhenLocalIsUTC(t *testing.T) { oldLocal := time.Local time.Local = time.UTC defer func() { time.Local = oldLocal }() loc := timeutil.ShanghaiLocation() before := time.Now().In(loc) orderNo, err := newOrderNo() after := time.Now().In(loc) if err != nil { t.Fatalf("newOrderNo() error = %v", err) } if len(orderNo) != len("RO20260603123456789") { t.Fatalf("orderNo length = %d, want %d: %s", len(orderNo), len("RO20260603123456789"), orderNo) } parsed, err := time.ParseInLocation("20060102150405", orderNo[2:16], loc) if err != nil { t.Fatalf("parse order no time: %v", err) } if parsed.Before(before.Add(-2*time.Second)) || parsed.After(after.Add(2*time.Second)) { t.Fatalf("order no time = %s, want between %s and %s", parsed, before, after) } }