| package routingrules_test |
|
|
| import ( |
| "testing" |
| "time" |
|
|
| "github.com/alpacahq/alpacadecimal" |
| "github.com/samber/lo" |
| "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/routingrules" |
| transactionstestutils "github.com/openmeterio/openmeter/openmeter/ledger/transactions/testutils" |
| "github.com/openmeterio/openmeter/pkg/currencyx" |
| ) |
|
|
| func TestDefaultValidator_AllowsFBOToAccrued(t *testing.T) { |
| validator := routingrules.DefaultValidator |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_AllowsAccruedToFBO(t *testing.T) { |
| validator := routingrules.DefaultValidator |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_AllowsFBOToReceivableReverse(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| openStatus := ledger.TransactionAuthorizationStatusOpen |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-open", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TransactionAuthorizationStatus: &openStatus, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_RejectsForbiddenAccountCombination(t *testing.T) { |
| validator := routingrules.DefaultValidator |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeEarnings, "sub-earnings", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_RejectsDuplicateSubAccountEntries(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| costBasis := alpacadecimal.NewFromInt(1) |
| accruedAddress := addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-dup", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| }) |
| earningsAddress := addressForRoute(t, ledger.AccountTypeEarnings, "sub-dup", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| }) |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: accruedAddress, |
| AmountValue: alpacadecimal.NewFromInt(-20), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: earningsAddress, |
| AmountValue: alpacadecimal.NewFromInt(20), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_AllowsDuplicateSubAccountEntriesWithUniqueIdentities(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| costBasis := alpacadecimal.NewFromInt(1) |
| accruedAddress := addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-source", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| }) |
| earningsAddress := addressForRoute(t, ledger.AccountTypeEarnings, "sub-earnings", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| }) |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: accruedAddress, |
| AmountValue: alpacadecimal.NewFromInt(-20), |
| IdentityKeyValue: "source:0", |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: accruedAddress, |
| AmountValue: alpacadecimal.NewFromInt(-10), |
| IdentityKeyValue: "source:1", |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: earningsAddress, |
| AmountValue: alpacadecimal.NewFromInt(30), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_AllowsDuplicateSubAccountEntriesWithUniqueProvenanceIdentities(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| sourceChargeID1 := "01JABCDEF0123456789ABCDEFG" |
| sourceChargeID2 := "01JBCDEFG0123456789ABCDEFG" |
| spendChargeID := "01JCDEFGH0123456789ABCDEFG" |
| collectionSource := "collection-source" |
| identityKey1, _ := ledger.EntryIdentityParts{ |
| CollectionSource: &collectionSource, |
| SourceChargeID: &sourceChargeID1, |
| SpendChargeID: &spendChargeID, |
| }.Text() |
| identityKey2, _ := ledger.EntryIdentityParts{ |
| CollectionSource: &collectionSource, |
| SourceChargeID: &sourceChargeID2, |
| SpendChargeID: &spendChargeID, |
| }.Text() |
| costBasis := alpacadecimal.NewFromInt(1) |
| accruedAddress := addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-source", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| }) |
| earningsAddress := addressForRoute(t, ledger.AccountTypeEarnings, "sub-earnings", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| }) |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: accruedAddress, |
| AmountValue: alpacadecimal.NewFromInt(-20), |
| IdentityKeyValue: string(identityKey1), |
| SourceChargeIDValue: &sourceChargeID1, |
| SpendChargeIDValue: &spendChargeID, |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: accruedAddress, |
| AmountValue: alpacadecimal.NewFromInt(-10), |
| IdentityKeyValue: string(identityKey2), |
| SourceChargeIDValue: &sourceChargeID2, |
| SpendChargeIDValue: &spendChargeID, |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: earningsAddress, |
| AmountValue: alpacadecimal.NewFromInt(30), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_RejectsTaxBehaviorOutsideAccruedOrEarnings(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| taxBehavior := ledger.TaxBehaviorExclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo-tax-behavior", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_RejectsTaxCodeOnFBO(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| taxCode := "tax_A" |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo-tax-code", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &taxCode, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_RejectsMismatchedReceivableAndFBORoute(t *testing.T) { |
| validator := routingrules.DefaultValidator |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo", ledger.Route{ |
| Currency: currencyx.Code("EUR"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_AllowsFBOCostBasisTranslationBothDirections(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| priority := ledger.DefaultCustomerFBOPriority |
| costBasis := alpacadecimal.NewFromInt(1) |
|
|
| unknownFBO := addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo-unknown", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CreditPriority: &priority, |
| }) |
| knownFBO := addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo-known", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| CreditPriority: &priority, |
| }) |
|
|
| t.Run("attribute", func(t *testing.T) { |
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: unknownFBO, |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: knownFBO, |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| }) |
|
|
| t.Run("correction", func(t *testing.T) { |
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: knownFBO, |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: unknownFBO, |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| }) |
| } |
|
|
| func TestDefaultValidator_RejectsFBOCostBasisTranslationWithMismatchedPriority(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| unknownPriority := ledger.DefaultCustomerFBOPriority |
| knownPriority := ledger.DefaultCustomerFBOPriority + 1 |
| costBasis := alpacadecimal.NewFromInt(1) |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo-unknown", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CreditPriority: &unknownPriority, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo-known", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| CreditPriority: &knownPriority, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_RejectsFBOCostBasisTranslationWithMismatchedTaxBehavior(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| priority := ledger.DefaultCustomerFBOPriority |
| costBasis := alpacadecimal.NewFromInt(1) |
| inclusive := ledger.TaxBehaviorInclusive |
| exclusive := ledger.TaxBehaviorExclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo-unknown", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CreditPriority: &priority, |
| TaxBehavior: &inclusive, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo-known", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| CreditPriority: &priority, |
| TaxBehavior: &exclusive, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_AllowsReceivableCostBasisAttributionAcrossFeatures(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| openStatus := ledger.TransactionAuthorizationStatusOpen |
| costBasis := alpacadecimal.NewFromFloat(0.5) |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-advance", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| Features: []string{"api-calls"}, |
| TransactionAuthorizationStatus: &openStatus, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(20), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-attributed", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| CostBasis: &costBasis, |
| TransactionAuthorizationStatus: &openStatus, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-20), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_AllowsReceivableAuthorizationTransition(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| openStatus := ledger.TransactionAuthorizationStatusOpen |
| status := ledger.TransactionAuthorizationStatusAuthorized |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-authorized", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TransactionAuthorizationStatus: &status, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-open", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TransactionAuthorizationStatus: &openStatus, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_RejectsReceivableAuthorizationTransitionWithWrongDirection(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| openStatus := ledger.TransactionAuthorizationStatusOpen |
| status := ledger.TransactionAuthorizationStatusAuthorized |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-open", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TransactionAuthorizationStatus: &openStatus, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-authorized", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TransactionAuthorizationStatus: &status, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_AllowsWashToAuthorizedReceivable(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| status := ledger.TransactionAuthorizationStatusAuthorized |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeWash, "sub-wash", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-authorized", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TransactionAuthorizationStatus: &status, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_AllowsFBOToTaxedAccrued(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| tax := "tax_A" |
| taxBehavior := ledger.TaxBehaviorInclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerFBO, "sub-fbo", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-tax", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &tax, |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_RejectsTaxCodeOnReceivable(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| openStatus := ledger.TransactionAuthorizationStatusOpen |
| taxA := "tax_A" |
| taxBehavior := ledger.TaxBehaviorInclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-taxA", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &taxA, |
| TransactionAuthorizationStatus: &openStatus, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-taxA", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &taxA, |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_RejectsFeaturesOnAccrued(t *testing.T) { |
| validator := routingrules.DefaultValidator |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-feature", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| Features: []string{"api-calls"}, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_RejectsAccruedToEarningsTaxCodeMismatch(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| taxA := "tax_A" |
| taxB := "tax_B" |
| taxBehavior := ledger.TaxBehaviorInclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-taxA", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &taxA, |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeEarnings, "sub-earn-taxB", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &taxB, |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_RejectsAccruedToEarningsTaxBehaviorMismatch(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| inclusive := ledger.TaxBehaviorInclusive |
| exclusive := ledger.TaxBehaviorExclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-inclusive", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: lo.ToPtr("tax_A"), |
| TaxBehavior: &inclusive, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeEarnings, "sub-earn-exclusive", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: lo.ToPtr("tax_A"), |
| TaxBehavior: &exclusive, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_AllowsReceivableToAccruedMatchingTaxCode(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| openStatus := ledger.TransactionAuthorizationStatusOpen |
| tax := "tax_A" |
| taxBehavior := ledger.TaxBehaviorInclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TransactionAuthorizationStatus: &openStatus, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-tax", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &tax, |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_AllowsAccruedToEarningsMatchingTaxCode(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| tax := "tax_A" |
| taxBehavior := ledger.TaxBehaviorInclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-tax", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &tax, |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeEarnings, "sub-earn-tax", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &tax, |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_AllowsAccruedToEarningsTaxCodeWithoutTaxBehavior(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| tax := "tax_A" |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-tax-only", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &tax, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeEarnings, "sub-earn-tax-only", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxCode: &tax, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_RejectsTaxBehaviorWithoutTaxCode(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| taxBehavior := ledger.TaxBehaviorInclusive |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-behavior-only", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeEarnings, "sub-earn-behavior-only", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TaxBehavior: &taxBehavior, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func TestDefaultValidator_AllowsAccruedToEarningsNilTaxCodeBothSides(t *testing.T) { |
| validator := routingrules.DefaultValidator |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerAccrued, "sub-accrued-nil", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeEarnings, "sub-earn-nil", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.NoError(t, err) |
| } |
|
|
| func TestDefaultValidator_RejectsWashToOpenReceivable(t *testing.T) { |
| validator := routingrules.DefaultValidator |
| status := ledger.TransactionAuthorizationStatusOpen |
|
|
| err := validator.ValidateEntries([]ledger.EntryInput{ |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeWash, "sub-wash", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| }), |
| AmountValue: alpacadecimal.NewFromInt(-50), |
| }, |
| &transactionstestutils.AnyEntryInput{ |
| Address: addressForRoute(t, ledger.AccountTypeCustomerReceivable, "sub-rec-open", ledger.Route{ |
| Currency: currencyx.Code("USD"), |
| TransactionAuthorizationStatus: &status, |
| }), |
| AmountValue: alpacadecimal.NewFromInt(50), |
| }, |
| }) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "ledger routing rule violated") |
| } |
|
|
| func addressForRoute(t *testing.T, accountType ledger.AccountType, subAccountID string, route ledger.Route) ledger.PostingAddress { |
| t.Helper() |
|
|
| key, err := ledger.BuildRoutingKey(route) |
| require.NoError(t, err) |
|
|
| addr, err := ledgeraccount.NewAddressFromData(ledgeraccount.AddressData{ |
| SubAccountID: subAccountID, |
| AccountType: accountType, |
| Route: route, |
| RouteID: "route-" + subAccountID + "-" + time.Now().UTC().Format("150405.000000000"), |
| RoutingKey: key, |
| }) |
| require.NoError(t, err) |
|
|
| return addr |
| } |
|
|