package ledger_test import ( "testing" "time" "github.com/alpacahq/alpacadecimal" "github.com/samber/mo" "github.com/stretchr/testify/require" "github.com/openmeterio/openmeter/openmeter/ledger" ledgeraccount "github.com/openmeterio/openmeter/openmeter/ledger/account" "github.com/openmeterio/openmeter/openmeter/ledger/transactions/testutils" "github.com/openmeterio/openmeter/pkg/currencyx" "github.com/openmeterio/openmeter/pkg/models" ) func TestValidateTransactionInputEntryAmountPrecision(t *testing.T) { tests := []struct { name string currency currencyx.Code amount string wantErr bool }{ { name: "USD accepts cents", currency: currencyx.Code("USD"), amount: "10.01", }, { name: "USD rejects sub-cent amount", currency: currencyx.Code("USD"), amount: "10.001", wantErr: true, }, { name: "USD accepts negative cents", currency: currencyx.Code("USD"), amount: "-10.01", }, { name: "JPY accepts whole amount", currency: currencyx.Code("JPY"), amount: "10", }, { name: "JPY rejects fractional amount", currency: currencyx.Code("JPY"), amount: "10.1", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { amount := mustDecimal(t, tt.amount) address := mustPostingAddress(t, tt.currency) txInput := &testutils.AnyTransactionInput{ BookedAtValue: time.Now(), EntryInputsValues: []*testutils.AnyEntryInput{ { Address: address, AmountValue: amount, }, { Address: address, AmountValue: amount.Neg(), }, }, } err := ledger.ValidateTransactionInput(t.Context(), txInput) if !tt.wantErr { require.NoError(t, err) return } require.Error(t, err) require.ErrorIs(t, err, ledger.ErrTransactionAmountInvalid) issues, issueErr := models.AsValidationIssues(err) require.NoError(t, issueErr) require.Len(t, issues, 1) require.Equal(t, ledger.ErrCodeTransactionAmountInvalid, issues[0].Code()) attrs := issues[0].Attributes() require.Equal(t, "amount_not_rounded_to_currency_precision", attrs["reason"]) currencyCode, ok := (attrs["currency"]).(currencyx.Code) require.True(t, ok, "expected currency to be a currencyx.Code, got %T", attrs["currency"]) require.Equal(t, tt.currency, currencyCode) require.Equal(t, amount.String(), attrs["amount"]) require.NotEmpty(t, attrs["rounded_amount"]) }) } } func TestListTransactionsInputValidateRouteFilter(t *testing.T) { costBasis := alpacadecimal.NewFromFloat(0.7) taxCode := "vat" taxBehavior := ledger.TaxBehaviorInclusive creditPriority := 1 authStatus := ledger.TransactionAuthorizationStatusAuthorized tests := []struct { name string route ledger.RouteFilter wantErr bool }{ { name: "currency route filter is supported", route: ledger.RouteFilter{ Currency: currencyx.Code("USD"), }, }, { name: "exact features route filter is supported", route: ledger.RouteFilter{ Features: mo.Some([]string{"feature-a"}), }, }, { name: "match feature route filter is supported", route: ledger.RouteFilter{ MatchFeature: "feature-a", }, }, { name: "cost basis route filter is rejected", route: ledger.RouteFilter{ CostBasis: mo.Some(&costBasis), }, wantErr: true, }, { name: "tax code route filter is rejected", route: ledger.RouteFilter{ TaxCode: mo.Some(&taxCode), }, wantErr: true, }, { name: "tax behavior route filter is rejected", route: ledger.RouteFilter{ TaxBehavior: mo.Some(&taxBehavior), }, wantErr: true, }, { name: "credit priority route filter is rejected", route: ledger.RouteFilter{ CreditPriority: &creditPriority, }, wantErr: true, }, { name: "transaction authorization route filter is rejected", route: ledger.RouteFilter{ TransactionAuthorizationStatus: &authStatus, }, wantErr: true, }, { name: "exact features and match feature cannot be combined", route: ledger.RouteFilter{ Features: mo.Some([]string{"feature-a"}), MatchFeature: "feature-a", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ledger.ListTransactionsInput{ Namespace: "ns-test", Limit: 1, Route: tt.route, }.Validate() if tt.wantErr { require.Error(t, err) require.ErrorIs(t, err, ledger.ErrListTransactionsInputInvalid) return } require.NoError(t, err) }) } } func mustPostingAddress(t *testing.T, currency currencyx.Code) ledger.PostingAddress { t.Helper() route := ledger.Route{Currency: currency} key, err := ledger.BuildRoutingKey(route) require.NoError(t, err) address, err := ledgeraccount.NewAddressFromData(ledgeraccount.AddressData{ SubAccountID: "sub_" + string(currency), AccountType: ledger.AccountTypeCustomerFBO, Route: route, RouteID: "route_" + string(currency), RoutingKey: key, }) require.NoError(t, err) return address } func mustDecimal(t *testing.T, raw string) alpacadecimal.Decimal { t.Helper() value, err := alpacadecimal.NewFromString(raw) require.NoError(t, err) return value }