File size: 3,281 Bytes
cee2387 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | package ledger
import (
"context"
"github.com/alpacahq/alpacadecimal"
"github.com/samber/lo"
"github.com/openmeterio/openmeter/pkg/currencyx"
"github.com/openmeterio/openmeter/pkg/models"
)
// ValidateInvariance validates that Debit - Credit = 0 for the given entries.
func ValidateInvariance(ctx context.Context, entries []EntryInput) error {
total := alpacadecimal.NewFromInt(0)
for _, entry := range entries {
total = total.Add(entry.Amount())
}
if total.IsZero() {
return nil
}
return ErrInvalidTransactionTotal.WithAttrs(models.Attributes{
"total": total,
"entries": entries,
})
}
func ValidateRouting(ctx context.Context, entries []EntryInput) error {
// Routing validation is implementation-specific and can be injected by the concrete ledger.
return nil
}
func ValidateEntryInput(ctx context.Context, entry EntryInput) error {
if entry == nil {
return ErrEntryInvalid.WithAttrs(models.Attributes{
"reason": "entry_required",
})
}
// Let's validate the address
if err := ValidateAddress(ctx, entry.PostingAddress()); err != nil {
return ErrEntryInvalid.WithAttrs(models.Attributes{
"reason": "invalid_address",
"error": err,
})
}
if err := validateEntryAmountPrecision(entry); err != nil {
return err
}
if err := ValidateEntryIdentityKey(entry); err != nil {
return ErrEntryInvalid.WithAttrs(models.Attributes{
"reason": "invalid_identity_key",
"error": err,
})
}
return nil
}
func ValidateAddress(ctx context.Context, address PostingAddress) error {
if address == nil {
return ErrAddressInvalid.WithAttrs(models.Attributes{
"reason": "address_required",
})
}
return nil
}
func validateEntryAmountPrecision(entry EntryInput) error {
currency, err := currencyx.NewCurrencyBuilder(currencyx.CurrencyTypeFiat).
WithCode(entry.PostingAddress().Route().Route().Currency).
Build()
if err != nil {
return ErrCurrencyInvalid.WithAttrs(models.Attributes{
"currency": entry.PostingAddress().Route().Route().Currency,
"error": err,
})
}
amount := entry.Amount()
if currency.IsRoundedToPrecision(amount) {
return nil
}
return ErrTransactionAmountInvalid.WithAttrs(models.Attributes{
"reason": "amount_not_rounded_to_currency_precision",
"currency": currency.Details().Code,
"amount": amount.String(),
"rounded_amount": currency.RoundToPrecision(amount).String(),
})
}
func ValidateTransactionInput(ctx context.Context, transaction TransactionInput) error {
return ValidateTransactionInputWith(ctx, transaction, nil)
}
func ValidateTransactionInputWith(ctx context.Context, transaction TransactionInput, routingValidator RoutingValidator) error {
if transaction == nil {
return ErrTransactionInputRequired
}
// Let's validate that the entries add up
if err := ValidateInvariance(ctx, lo.Map(transaction.EntryInputs(), func(e EntryInput, _ int) EntryInput {
return e
})); err != nil {
return err
}
// Let's validate the entries themselves
for _, entry := range transaction.EntryInputs() {
if err := ValidateEntryInput(ctx, entry); err != nil {
return err
}
}
if routingValidator != nil {
if err := routingValidator.ValidateEntries(transaction.EntryInputs()); err != nil {
return err
}
}
return nil
}
|