package common import ( "math" "testing" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" ) func TestSpendWalletBalanceClosesWithdrawableGap(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), Balance: 10_000, WithdrawalBalance: 10_000, } if err := SpendWalletBalance(&wallet, 8_000); err != nil { t.Fatal(err) } if wallet.Balance != 2_000 || wallet.WithdrawalBalance != 2_000 { t.Fatalf("balances after expense = (%d, %d), want (2000, 2000)", wallet.Balance, wallet.WithdrawalBalance) } if err := ReserveWalletWithdrawal(&wallet, 10_000); err != ErrWalletBalance { t.Fatalf("withdrawal after expense error = %v, want %v", err, ErrWalletBalance) } } func TestSpendWalletBalanceConsumesNonWithdrawableBalanceFirst(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), Balance: 15_000, WithdrawalBalance: 10_000, } if err := SpendWalletBalance(&wallet, 4_000); err != nil { t.Fatal(err) } if wallet.Balance != 11_000 || wallet.WithdrawalBalance != 10_000 { t.Fatalf("balances after first expense = (%d, %d), want (11000, 10000)", wallet.Balance, wallet.WithdrawalBalance) } if err := SpendWalletBalance(&wallet, 2_000); err != nil { t.Fatal(err) } if wallet.Balance != 9_000 || wallet.WithdrawalBalance != 9_000 { t.Fatalf("balances after second expense = (%d, %d), want (9000, 9000)", wallet.Balance, wallet.WithdrawalBalance) } } func TestWithdrawalReserveAndRejectAreExactInverse(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), Balance: 10_000, WithdrawalBalance: 10_000, } if err := ReserveWalletWithdrawal(&wallet, 10_000); err != nil { t.Fatal(err) } if wallet.Balance != 0 || wallet.WithdrawalBalance != 0 { t.Fatalf("reserved balances = (%d, %d), want (0, 0)", wallet.Balance, wallet.WithdrawalBalance) } if err := ReleaseWalletWithdrawal(&wallet, 10_000); err != nil { t.Fatal(err) } if wallet.Balance != 10_000 || wallet.WithdrawalBalance != 10_000 { t.Fatalf("released balances = (%d, %d), want (10000, 10000)", wallet.Balance, wallet.WithdrawalBalance) } } func TestLegacyWithdrawalReleaseCannotExceedRemainingBalance(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), Balance: 2_000, WithdrawalBalance: 2_000, } if err := ReleaseLegacyWithdrawalBalance(&wallet, 8_000); err != nil { t.Fatal(err) } if wallet.Balance != 2_000 || wallet.WithdrawalBalance != 2_000 { t.Fatalf("legacy released balances = (%d, %d), want (2000, 2000)", wallet.Balance, wallet.WithdrawalBalance) } } func TestFrozenWalletRejectsNewDebits(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusFrozen), Balance: 10_000, WithdrawalBalance: 10_000, } if err := SpendWalletBalance(&wallet, 1); err != ErrWalletUnavailable { t.Fatalf("expense error = %v, want %v", err, ErrWalletUnavailable) } if err := ReserveWalletWithdrawal(&wallet, 1); err != ErrWalletUnavailable { t.Fatalf("withdrawal error = %v, want %v", err, ErrWalletUnavailable) } } func TestWithdrawalReleaseRejectsOverflow(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), Balance: math.MaxInt64, WithdrawalBalance: math.MaxInt64, } if err := ReleaseWalletWithdrawal(&wallet, 1); err != ErrWalletOverflow { t.Fatalf("release error = %v, want %v", err, ErrWalletOverflow) } } func TestNewWithdrawalRejectionRestoresBothBalances(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), OwnerType: "delivery", Balance: 2_000, WithdrawalBalance: 2_000, } application := models.WalletApplyCash{Amount: 8_000, BalanceReserved: true} released, err := ReleaseWithdrawalApplication(&wallet, application) if err != nil || !released { t.Fatalf("release = (%v, %v), want (true, nil)", released, err) } if wallet.Balance != 10_000 || wallet.WithdrawalBalance != 10_000 { t.Fatalf("released balances = (%d, %d), want (10000, 10000)", wallet.Balance, wallet.WithdrawalBalance) } } func TestLegacyOrganizationWithdrawalRejectionDoesNotMintBalance(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), OwnerType: "delivery", Balance: 10_000, WithdrawalBalance: 10_000, } application := models.WalletApplyCash{Amount: 8_000} released, err := ReleaseWithdrawalApplication(&wallet, application) if err != nil || released { t.Fatalf("release = (%v, %v), want (false, nil)", released, err) } if wallet.Balance != 10_000 || wallet.WithdrawalBalance != 10_000 { t.Fatalf("legacy organization balances changed to (%d, %d)", wallet.Balance, wallet.WithdrawalBalance) } } func TestLegacyWithdrawalCompletionDebitsMissingBalances(t *testing.T) { tests := []struct { name string ownerType string withdrawalBalance int64 wantBalance int64 wantWithdrawal int64 }{ { name: "client already reserved withdrawable balance", ownerType: "user", withdrawalBalance: 2_000, wantBalance: 2_000, wantWithdrawal: 2_000, }, { name: "organization reserved neither balance", ownerType: "delivery", withdrawalBalance: 10_000, wantBalance: 2_000, wantWithdrawal: 2_000, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), OwnerType: test.ownerType, Balance: 10_000, WithdrawalBalance: test.withdrawalBalance, } changed, err := SettleLegacyWithdrawal(&wallet, models.WalletApplyCash{Amount: 8_000}) if err != nil || !changed { t.Fatalf("settle = (%v, %v), want (true, nil)", changed, err) } if wallet.Balance != test.wantBalance || wallet.WithdrawalBalance != test.wantWithdrawal { t.Fatalf( "settled balances = (%d, %d), want (%d, %d)", wallet.Balance, wallet.WithdrawalBalance, test.wantBalance, test.wantWithdrawal, ) } }) } } func TestReservedWithdrawalCompletionDoesNotDebitAgain(t *testing.T) { wallet := models.WalletBasic{ Entity: NewEntity(StatusEnable), OwnerType: "user", Balance: 2_000, WithdrawalBalance: 2_000, } application := models.WalletApplyCash{Amount: 8_000, BalanceReserved: true} changed, err := SettleLegacyWithdrawal(&wallet, application) if err != nil || changed { t.Fatalf("settle = (%v, %v), want (false, nil)", changed, err) } if wallet.Balance != 2_000 || wallet.WithdrawalBalance != 2_000 { t.Fatalf("reserved completion changed balances to (%d, %d)", wallet.Balance, wallet.WithdrawalBalance) } }