package order import ( "testing" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/timeutil" "gorm.io/datatypes" ) 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 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) } }