| package ledger |
|
|
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "strings" |
| "time" |
|
|
| "github.com/alpacahq/alpacadecimal" |
| "github.com/samber/mo" |
|
|
| "github.com/openmeterio/openmeter/pkg/currencyx" |
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| |
| |
| |
|
|
| |
| type PostingAddress interface { |
| models.Equaler[PostingAddress] |
|
|
| SubAccountID() string |
| AccountType() AccountType |
| Route() SubAccountRoute |
| } |
|
|
| type Balance = alpacadecimal.Decimal |
|
|
| |
| |
| type SubAccount interface { |
| |
| Address() PostingAddress |
|
|
| |
| AccountID() models.NamespacedID |
|
|
| |
| Route() Route |
| } |
|
|
| |
| type RouteFilter struct { |
| Currency currencyx.Code |
|
|
| |
| TaxCode mo.Option[*string] |
| TaxBehavior mo.Option[*TaxBehavior] |
| |
| Features mo.Option[[]string] |
| |
| MatchFeature string |
| CostBasis mo.Option[*alpacadecimal.Decimal] |
|
|
| |
| CreditPriority *int |
|
|
| |
| |
| TransactionAuthorizationStatus *TransactionAuthorizationStatus |
| } |
|
|
| |
| |
| type Account interface { |
| ID() models.NamespacedID |
| Type() AccountType |
| } |
|
|
| |
| |
| |
|
|
| type EntryInput interface { |
| PostingAddress() PostingAddress |
| Amount() alpacadecimal.Decimal |
| IdentityKey() string |
| SchemaVersion() EntrySchemaVersion |
| SourceChargeID() *string |
| SpendChargeID() *string |
| Annotations() models.Annotations |
| } |
|
|
| type EntrySchemaVersion int |
|
|
| const ( |
| EntrySchemaVersionLegacy EntrySchemaVersion = 1 |
| EntrySchemaVersionCurrent EntrySchemaVersion = 2 |
| ) |
|
|
| type Entry interface { |
| EntryInput |
| ID() models.NamespacedID |
| TransactionID() models.NamespacedID |
| } |
|
|
| type TransactionInput interface { |
| BookedAt() time.Time |
| EntryInputs() []EntryInput |
| Annotations() models.Annotations |
| AsGroupInput(namespace string, annotations models.Annotations) TransactionGroupInput |
| } |
|
|
| type ImpactFilter struct { |
| AccountType AccountType |
| Route RouteFilter |
| } |
|
|
| |
| type Transaction interface { |
| Cursor() TransactionCursor |
| BookedAt() time.Time |
| Entries() []Entry |
| ID() models.NamespacedID |
| Annotations() models.Annotations |
| } |
|
|
| type TransactionCursor struct { |
| BookedAt time.Time |
| CreatedAt time.Time |
| ID models.NamespacedID |
| } |
|
|
| |
| |
| func (c TransactionCursor) Compare(other TransactionCursor) int { |
| switch { |
| case c.BookedAt.Before(other.BookedAt): |
| return -1 |
| case c.BookedAt.After(other.BookedAt): |
| return 1 |
| } |
|
|
| switch { |
| case c.CreatedAt.Before(other.CreatedAt): |
| return -1 |
| case c.CreatedAt.After(other.CreatedAt): |
| return 1 |
| } |
|
|
| return strings.Compare(c.ID.ID, other.ID.ID) |
| } |
|
|
| func (c TransactionCursor) Validate() error { |
| var errs []error |
|
|
| if c.BookedAt.IsZero() { |
| errs = append(errs, fmt.Errorf("booked_at is zero")) |
| } |
|
|
| if c.CreatedAt.IsZero() { |
| errs = append(errs, fmt.Errorf("created_at is zero")) |
| } |
|
|
| if err := c.ID.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("id is invalid: %w", err)) |
| } |
|
|
| return errors.Join(errs...) |
| } |
|
|
| type TransactionGroupInput interface { |
| Namespace() string |
| Transactions() []TransactionInput |
| Annotations() models.Annotations |
| } |
|
|
| |
| type TransactionGroup interface { |
| ID() models.NamespacedID |
| Transactions() []Transaction |
| Annotations() models.Annotations |
| } |
|
|
| |
| |
| |
|
|
| type Ledger interface { |
| |
| CommitGroup(ctx context.Context, group TransactionGroupInput) (TransactionGroup, error) |
|
|
| |
| GetTransactionGroup(ctx context.Context, id models.NamespacedID) (TransactionGroup, error) |
|
|
| |
| |
| |
| ListTransactions(ctx context.Context, params ListTransactionsInput) (ListTransactionsResult, error) |
| } |
|
|
| type ListTransactionsInput struct { |
| Namespace string |
| Cursor *TransactionCursor |
| Before *TransactionCursor |
| Limit int |
|
|
| TransactionID *models.NamespacedID |
|
|
| |
| AccountIDs []string |
| Currency *currencyx.Code |
| AsOf *time.Time |
| Route RouteFilter |
|
|
| CreditMovement ListTransactionsCreditMovement |
|
|
| |
| AnnotationFilters map[string]string |
|
|
| |
| ExcludeAnnotationFilters map[string]string |
| } |
|
|
| type ListTransactionsResult struct { |
| Items []Transaction |
| NextCursor *TransactionCursor |
| } |
|
|
| func (i ListTransactionsInput) Validate() error { |
| if i.Limit < 1 { |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "limit_invalid", |
| "limit": i.Limit, |
| }) |
| } |
|
|
| if i.Cursor != nil { |
| if err := i.Cursor.Validate(); err != nil { |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "cursor_invalid", |
| "cursor": i.Cursor, |
| "error": err, |
| }) |
| } |
| } |
|
|
| if i.Before != nil { |
| if err := i.Before.Validate(); err != nil { |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "before_invalid", |
| "before": i.Before, |
| "error": err, |
| }) |
| } |
| } |
|
|
| if i.Cursor != nil && i.Before != nil { |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "after_before_both_set", |
| }) |
| } |
|
|
| if i.TransactionID != nil { |
| if err := i.TransactionID.Validate(); err != nil { |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "transaction_id_invalid", |
| "transaction_id": i.TransactionID, |
| "error": err, |
| }) |
| } |
| } |
|
|
| if i.Currency != nil { |
| if err := i.Currency.Validate(); err != nil { |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "currency_invalid", |
| "currency": i.Currency, |
| "error": err, |
| }) |
| } |
| } |
|
|
| if i.AsOf != nil && i.AsOf.IsZero() { |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "as_of_invalid", |
| "as_of": i.AsOf, |
| }) |
| } |
|
|
| if err := validateListTransactionsRouteFilter(i.Route); err != nil { |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "route_invalid", |
| "route": i.Route, |
| "error": err, |
| }) |
| } |
|
|
| switch i.CreditMovement { |
| case ListTransactionsCreditMovementUnspecified, ListTransactionsCreditMovementPositive, ListTransactionsCreditMovementNegative: |
| default: |
| return ErrListTransactionsInputInvalid.WithAttrs(models.Attributes{ |
| "reason": "credit_movement_invalid", |
| "credit_movement": i.CreditMovement, |
| }) |
| } |
|
|
| return nil |
| } |
|
|
| func validateListTransactionsRouteFilter(route RouteFilter) error { |
| var errs []error |
|
|
| if route.TaxCode.IsPresent() { |
| errs = append(errs, errors.New("tax code filter is not supported")) |
| } |
|
|
| if route.TaxBehavior.IsPresent() { |
| errs = append(errs, errors.New("tax behavior filter is not supported")) |
| } |
|
|
| if route.CostBasis.IsPresent() { |
| errs = append(errs, errors.New("cost basis filter is not supported")) |
| } |
|
|
| if route.CreditPriority != nil { |
| errs = append(errs, errors.New("credit priority filter is not supported")) |
| } |
|
|
| if route.TransactionAuthorizationStatus != nil { |
| errs = append(errs, errors.New("transaction authorization status filter is not supported")) |
| } |
|
|
| if route.Features.IsPresent() && route.MatchFeature != "" { |
| errs = append(errs, errors.New("features and match feature filters cannot be combined")) |
| } |
|
|
| if route.Features.IsPresent() { |
| features, _ := route.Features.Get() |
| if err := validateFeatures(features); err != nil { |
| errs = append(errs, fmt.Errorf("features: %w", err)) |
| } |
| } |
|
|
| if route.MatchFeature != "" { |
| if err := validateFeatures([]string{route.MatchFeature}); err != nil { |
| errs = append(errs, fmt.Errorf("match feature: %w", err)) |
| } |
| } |
|
|
| return errors.Join(errs...) |
| } |
|
|
| type ListTransactionsCreditMovement uint8 |
|
|
| const ( |
| ListTransactionsCreditMovementUnspecified ListTransactionsCreditMovement = iota |
| ListTransactionsCreditMovementPositive |
| ListTransactionsCreditMovementNegative |
| ) |
|
|