From 809370bda865c83f5568191a4f753b473b112696 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 30 Jan 2026 16:07:28 +0100 Subject: [PATCH] pkg update --- api/payments/orchestrator/.gitignore | 1 + .../api/routers/internal/grpcimp/config.go | 19 +-- api/pkg/discovery/announcer.go | 2 +- api/pkg/discovery/logging.go | 4 +- api/pkg/go.mod | 2 +- api/pkg/go.sum | 4 +- api/pkg/ledgerconv/account.go | 40 ++++++ api/pkg/model/account_role.go | 117 ++++++++++++++++ api/pkg/model/chains.go | 92 +++++++++++- api/pkg/model/ledger.go | 131 ++++++++++++++++++ api/pkg/mservice/services.go | 3 +- api/pkg/payments/rail/gateway.go | 3 + api/pkg/payments/rail/ledger.go | 1 + 13 files changed, 389 insertions(+), 30 deletions(-) create mode 100644 api/pkg/model/account_role.go create mode 100644 api/pkg/model/ledger.go diff --git a/api/payments/orchestrator/.gitignore b/api/payments/orchestrator/.gitignore index c62beb6b..436d3e5e 100644 --- a/api/payments/orchestrator/.gitignore +++ b/api/payments/orchestrator/.gitignore @@ -1,3 +1,4 @@ internal/generated .gocache app +tmp diff --git a/api/pkg/api/routers/internal/grpcimp/config.go b/api/pkg/api/routers/internal/grpcimp/config.go index 45926665..a01a97e1 100644 --- a/api/pkg/api/routers/internal/grpcimp/config.go +++ b/api/pkg/api/routers/internal/grpcimp/config.go @@ -1,7 +1,6 @@ package grpcimp import ( - "fmt" "net" "os" "strings" @@ -59,23 +58,7 @@ func (c *Config) DiscoveryInvokeURI() string { return "" } - return fmt.Sprintf("%s://%s:%s", c.discoveryScheme(), host, addrPort) -} - -func (c *Config) discoveryScheme() string { - scheme := strings.TrimSpace(c.AdvertiseScheme) - if envKey := strings.TrimSpace(c.AdvertiseSchemeEnv); envKey != "" { - if value := strings.TrimSpace(os.Getenv(envKey)); value != "" { - scheme = value - } - } - if scheme != "" { - return scheme - } - if c != nil && c.TLS != nil && strings.TrimSpace(c.TLS.CertFile) != "" && strings.TrimSpace(c.TLS.KeyFile) != "" { - return "grpcs" - } - return "grpc" + return net.JoinHostPort(host, addrPort) } func splitHostPort(address string) (string, string) { diff --git a/api/pkg/discovery/announcer.go b/api/pkg/discovery/announcer.go index 6728c803..8c082939 100644 --- a/api/pkg/discovery/announcer.go +++ b/api/pkg/discovery/announcer.go @@ -166,5 +166,5 @@ func DefaultInvokeURI(service string) string { if clean == "" { return "" } - return "grpc://" + clean + return clean } diff --git a/api/pkg/discovery/logging.go b/api/pkg/discovery/logging.go index 4a1a94be..f750532a 100644 --- a/api/pkg/discovery/logging.go +++ b/api/pkg/discovery/logging.go @@ -40,10 +40,10 @@ func announcementFields(announce Announcement) []zap.Field { fields = append(fields, zap.Int("currencies", len(announce.Currencies))) } if announce.Health.IntervalSec > 0 { - fields = append(fields, zap.Int("interval_sec", announce.Health.IntervalSec)) + fields = append(fields, zap.Int("interval_seconds", announce.Health.IntervalSec)) } if announce.Health.TimeoutSec > 0 { - fields = append(fields, zap.Int("timeout_sec", announce.Health.TimeoutSec)) + fields = append(fields, zap.Int("timeout_seconds", announce.Health.TimeoutSec)) } return fields } diff --git a/api/pkg/go.mod b/api/pkg/go.mod index d8884bba..d9aeace4 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -93,6 +93,6 @@ require ( golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/api/pkg/go.sum b/api/pkg/go.sum index 6c726150..5ed5deaa 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -269,8 +269,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE= google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/pkg/ledgerconv/account.go b/api/pkg/ledgerconv/account.go index 71037da0..4f504d5e 100644 --- a/api/pkg/ledgerconv/account.go +++ b/api/pkg/ledgerconv/account.go @@ -36,6 +36,37 @@ func ParseAccountStatus(value string) (ledgerv1.AccountStatus, bool) { } } +func ParseAccountRole(value string) (ledgerv1.AccountRole, bool) { + switch strings.ToUpper(strings.TrimSpace(value)) { + case "ACCOUNT_ROLE_OPERATING", "OPERATING": + return ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, true + case "ACCOUNT_ROLE_HOLD", "HOLD": + return ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD, true + case "ACCOUNT_ROLE_TRANSIT", "TRANSIT": + return ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT, true + case "ACCOUNT_ROLE_SETTLEMENT", "SETTLEMENT": + return ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT, true + case "ACCOUNT_ROLE_CLEARING", "CLEARING": + return ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING, true + case "ACCOUNT_ROLE_PENDING", "PENDING": + return ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING, true + case "ACCOUNT_ROLE_RESERVE", "RESERVE": + return ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE, true + case "ACCOUNT_ROLE_LIQUIDITY", "LIQUIDITY": + return ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY, true + case "ACCOUNT_ROLE_FEE", "FEE": + return ledgerv1.AccountRole_ACCOUNT_ROLE_FEE, true + case "ACCOUNT_ROLE_CHARGEBACK", "CHARGEBACK": + return ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK, true + case "ACCOUNT_ROLE_ADJUSTMENT", "ADJUSTMENT": + return ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT, true + case "ACCOUNT_ROLE_UNSPECIFIED", "UNSPECIFIED", "": + return ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED, true + default: + return ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED, false + } +} + func IsAccountTypeUnspecified(value string) bool { switch strings.ToUpper(strings.TrimSpace(value)) { case "", "ACCOUNT_TYPE_UNSPECIFIED", "UNSPECIFIED": @@ -53,3 +84,12 @@ func IsAccountStatusUnspecified(value string) bool { return false } } + +func IsAccountRoleUnspecified(value string) bool { + switch strings.ToUpper(strings.TrimSpace(value)) { + case "", "ACCOUNT_ROLE_UNSPECIFIED", "UNSPECIFIED": + return true + default: + return false + } +} diff --git a/api/pkg/model/account_role.go b/api/pkg/model/account_role.go new file mode 100644 index 00000000..ede14894 --- /dev/null +++ b/api/pkg/model/account_role.go @@ -0,0 +1,117 @@ +package model + +import ( + "strings" + + accountrolev1 "github.com/tech/sendico/pkg/proto/common/account_role/v1" +) + +type AccountRole string + +const ( + AccountRoleOperating AccountRole = "operating" + AccountRoleHold AccountRole = "hold" + AccountRoleTransit AccountRole = "transit" + AccountRoleSettlement AccountRole = "settlement" + AccountRoleClearing AccountRole = "clearing" + AccountRolePending AccountRole = "pending" + AccountRoleReserve AccountRole = "reserve" + AccountRoleLiquidity AccountRole = "liquidity" + AccountRoleFee AccountRole = "fee" + AccountRoleChargeback AccountRole = "chargeback" + AccountRoleAdjustment AccountRole = "adjustment" +) + +const ( + MetadataKeyFromRole = "from_role" + MetadataKeyToRole = "to_role" +) + +func Parse(value string) (AccountRole, bool) { + switch strings.ToUpper(strings.TrimSpace(value)) { + case "ACCOUNT_ROLE_OPERATING", "OPERATING": + return AccountRoleOperating, true + case "ACCOUNT_ROLE_HOLD", "HOLD": + return AccountRoleHold, true + case "ACCOUNT_ROLE_TRANSIT", "TRANSIT": + return AccountRoleTransit, true + case "ACCOUNT_ROLE_SETTLEMENT", "SETTLEMENT": + return AccountRoleSettlement, true + case "ACCOUNT_ROLE_CLEARING", "CLEARING": + return AccountRoleClearing, true + case "ACCOUNT_ROLE_PENDING", "PENDING": + return AccountRolePending, true + case "ACCOUNT_ROLE_RESERVE", "RESERVE": + return AccountRoleReserve, true + case "ACCOUNT_ROLE_LIQUIDITY", "LIQUIDITY": + return AccountRoleLiquidity, true + case "ACCOUNT_ROLE_FEE", "FEE": + return AccountRoleFee, true + case "ACCOUNT_ROLE_CHARGEBACK", "CHARGEBACK": + return AccountRoleChargeback, true + case "ACCOUNT_ROLE_ADJUSTMENT", "ADJUSTMENT": + return AccountRoleAdjustment, true + case "ACCOUNT_ROLE_UNSPECIFIED", "UNSPECIFIED", "": + return "", true + default: + return "", false + } +} + +func ToProto(role AccountRole) accountrolev1.AccountRole { + switch role { + case AccountRoleOperating: + return accountrolev1.AccountRole_OPERATING + case AccountRoleHold: + return accountrolev1.AccountRole_HOLD + case AccountRoleTransit: + return accountrolev1.AccountRole_TRANSIT + case AccountRoleSettlement: + return accountrolev1.AccountRole_SETTLEMENT + case AccountRoleClearing: + return accountrolev1.AccountRole_CLEARING + case AccountRolePending: + return accountrolev1.AccountRole_PENDING + case AccountRoleReserve: + return accountrolev1.AccountRole_RESERVE + case AccountRoleLiquidity: + return accountrolev1.AccountRole_LIQUIDITY + case AccountRoleFee: + return accountrolev1.AccountRole_FEE + case AccountRoleChargeback: + return accountrolev1.AccountRole_CHARGEBACK + case AccountRoleAdjustment: + return accountrolev1.AccountRole_ADJUSTMENT + default: + return accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED + } +} + +func FromProto(role accountrolev1.AccountRole) AccountRole { + switch role { + case accountrolev1.AccountRole_OPERATING: + return AccountRoleOperating + case accountrolev1.AccountRole_HOLD: + return AccountRoleHold + case accountrolev1.AccountRole_TRANSIT: + return AccountRoleTransit + case accountrolev1.AccountRole_SETTLEMENT: + return AccountRoleSettlement + case accountrolev1.AccountRole_CLEARING: + return AccountRoleClearing + case accountrolev1.AccountRole_PENDING: + return AccountRolePending + case accountrolev1.AccountRole_RESERVE: + return AccountRoleReserve + case accountrolev1.AccountRole_LIQUIDITY: + return AccountRoleLiquidity + case accountrolev1.AccountRole_FEE: + return AccountRoleFee + case accountrolev1.AccountRole_CHARGEBACK: + return AccountRoleChargeback + case accountrolev1.AccountRole_ADJUSTMENT: + return AccountRoleAdjustment + default: + return "" + } +} diff --git a/api/pkg/model/chains.go b/api/pkg/model/chains.go index 000d9d09..903f5da4 100644 --- a/api/pkg/model/chains.go +++ b/api/pkg/model/chains.go @@ -1,11 +1,93 @@ package model +import "strings" + +// ChainNetwork identifies a blockchain network for routing and discovery. type ChainNetwork string const ( - ChainNetworkARB ChainNetwork = "arbitrum_one" - ChainNetworkEthMain ChainNetwork = "ethereum_mainnet" - ChainNetworkTronMain ChainNetwork = "tron_mainnet" - ChainNetworkTronNile ChainNetwork = "tron_nile" - ChainNetworkUnspecified ChainNetwork = "unspecified" + ChainNetworkArbitrumOne ChainNetwork = "arbitrum_one" + ChainNetworkArbitrumSepolia ChainNetwork = "arbitrum_sepolia" + ChainNetworkEthereumMainnet ChainNetwork = "ethereum_mainnet" + ChainNetworkTronMainnet ChainNetwork = "tron_mainnet" + ChainNetworkTronNile ChainNetwork = "tron_nile" + ChainNetworkUnspecified ChainNetwork = "" ) + +// AllChainNetworks returns all valid chain networks. +func AllChainNetworks() []ChainNetwork { + return []ChainNetwork{ + ChainNetworkArbitrumOne, + ChainNetworkEthereumMainnet, + ChainNetworkTronMainnet, + ChainNetworkTronNile, + } +} + +// ParseChainNetwork parses a string into a ChainNetwork. +// Returns the network and true if valid, or empty and false if invalid. +func ParseChainNetwork(value string) (ChainNetwork, bool) { + v := strings.ToLower(strings.TrimSpace(value)) + switch v { + case "arbitrum_one", "arbitrum", "arb": + return ChainNetworkArbitrumOne, true + case "ethereum_mainnet", "ethereum", "eth": + return ChainNetworkEthereumMainnet, true + case "tron_mainnet", "tron": + return ChainNetworkTronMainnet, true + case "tron_nile", "nile": + return ChainNetworkTronNile, true + case "unspecified", "": + return ChainNetworkUnspecified, true + default: + return ChainNetworkUnspecified, false + } +} + +// String returns the string representation of the network. +func (n ChainNetwork) String() string { + return string(n) +} + +// IsValid returns true if the network is a known valid network. +func (n ChainNetwork) IsValid() bool { + switch n { + case ChainNetworkArbitrumOne, + ChainNetworkEthereumMainnet, + ChainNetworkTronMainnet, + ChainNetworkTronNile: + return true + default: + return false + } +} + +// IsTron returns true if the network is a TRON network. +func (n ChainNetwork) IsTron() bool { + switch n { + case ChainNetworkTronMainnet, ChainNetworkTronNile: + return true + default: + return false + } +} + +// IsEVM returns true if the network is an EVM-compatible network. +func (n ChainNetwork) IsEVM() bool { + switch n { + case ChainNetworkArbitrumOne, ChainNetworkEthereumMainnet: + return true + default: + return false + } +} + +// IsTestnet returns true if the network is a testnet. +func (n ChainNetwork) IsTestnet() bool { + switch n { + case ChainNetworkTronNile: + return true + default: + return false + } +} diff --git a/api/pkg/model/ledger.go b/api/pkg/model/ledger.go new file mode 100644 index 00000000..11f6fb4a --- /dev/null +++ b/api/pkg/model/ledger.go @@ -0,0 +1,131 @@ +package model + +import ( + "strings" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AccountType defines the category of account (asset, liability, revenue, expense). +type LedgerAccountType string + +const ( + LedgerAccountTypeAsset LedgerAccountType = "asset" + LedgerAccountTypeLiability LedgerAccountType = "liability" + LedgerAccountTypeRevenue LedgerAccountType = "revenue" + LedgerAccountTypeExpense LedgerAccountType = "expense" +) + +// AccountStatus tracks the operational state of an account. +type LedgerAccountStatus string + +const ( + LedgerAccountStatusActive LedgerAccountStatus = "active" + LedgerAccountStatusFrozen LedgerAccountStatus = "frozen" + LedgerAccountStatusClosed LedgerAccountStatus = "closed" +) + +type LedgerAccountScope string + +const ( + LedgerAccountScopeOrganization LedgerAccountScope = "organization" + LedgerAccountScopeSystem LedgerAccountScope = "system" +) + +type SystemAccountPurpose string + +const ( + SystemAccountPurposeExternalSource SystemAccountPurpose = "external_source" + SystemAccountPurposeExternalSink SystemAccountPurpose = "external_sink" +) + +// Account represents a ledger account that holds balances for a specific currency. +type LedgerAccount struct { + storable.Base `bson:",inline" json:",inline"` + Describable `bson:",inline" json:",inline"` + + // Scope defines whether the account belongs to an organization + // or is a system-level ledger account used for internal accounting. + Scope LedgerAccountScope `bson:"scope" json:"scope"` + + // SystemPurpose specifies the role of a system-scoped account + // (e.g., external source or sink of funds). Must be set for system accounts + // and must be nil for organization accounts. + SystemPurpose *SystemAccountPurpose `bson:"systemPurpose,omitempty" json:"systemPurpose,omitempty"` + + // OrganizationRef links the account to an organization. + // Must be set for organization accounts and nil for system accounts. + OrganizationRef *primitive.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"` + + // Role defines the functional purpose of the account within an organization + // (e.g., pending, operating, settlement, hold, etc.). + // Must be set for organization accounts and omitted for system accounts. + Role AccountRole `bson:"role,omitempty" json:"role,omitempty"` + + // AccountCode is a logical classification code of the account + // (e.g., "asset:cash:usd") used for reporting and grouping. + AccountCode string `bson:"accountCode" json:"accountCode"` + + // Currency is the ISO 4217 currency code the account operates in. + Currency string `bson:"currency" json:"currency"` + + // AccountType defines the accounting category of the account + // (asset, liability, revenue, expense). + AccountType LedgerAccountType `bson:"accountType" json:"accountType"` + + // Status represents the operational state of the account. + Status LedgerAccountStatus `bson:"status" json:"status"` + + // AllowNegative defines whether the account is allowed to have + // a negative balance (used for system control accounts). + AllowNegative bool `bson:"allowNegative" json:"allowNegative"` + + // OwnerRef optionally links the account to a specific owner entity + // (e.g., user or sub-entity within the organization). + OwnerRef *primitive.ObjectID `bson:"ownerRef,omitempty" json:"ownerRef,omitempty"` + + // Metadata holds additional arbitrary key-value attributes. + Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` +} + +// Collection implements storable.Storable. +func (*LedgerAccount) Collection() string { + return mservice.LedgerAccounts +} + +// Validate enforces scope-specific invariants for ledger accounts. +func (a *LedgerAccount) Validate() error { + if a == nil { + return merrors.InvalidArgument("ledger account is required") + } + + switch a.Scope { + case LedgerAccountScopeOrganization: + if a.OrganizationRef == nil || a.OrganizationRef.IsZero() { + return merrors.InvalidArgument("organization_ref is required for organization accounts") + } + if strings.TrimSpace(string(a.Role)) == "" { + return merrors.InvalidArgument("role is required for organization accounts") + } + if a.SystemPurpose != nil { + return merrors.InvalidArgument("system_purpose must be nil for organization accounts") + } + case LedgerAccountScopeSystem: + if a.OrganizationRef != nil && !a.OrganizationRef.IsZero() { + return merrors.InvalidArgument("organization_ref must be nil for system accounts") + } + if strings.TrimSpace(string(a.Role)) != "" { + return merrors.InvalidArgument("role must be empty for system accounts") + } + if a.SystemPurpose == nil { + return merrors.InvalidArgument("system_purpose is required for system accounts") + } + default: + return merrors.InvalidArgument("scope is required") + } + + return nil +} diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go index ed6272db..e2790868 100644 --- a/api/pkg/mservice/services.go +++ b/api/pkg/mservice/services.go @@ -17,6 +17,7 @@ const ( PaymentGateway Type = "payment_gateway" // Represents payment gateway microservice FXOracle Type = "fx_oracle" // Represents FX oracle microservice FeePlans Type = "fee_plans" // Represents fee plans microservice + BillingDocuments Type = "billing_documents" // Represents billing documents microservice FilterProjects Type = "filter_projects" // Represents comments on tasks or other resources Invitations Type = "invitations" // Represents invitations sent to users Invoices Type = "invoices" // Represents invoices @@ -55,7 +56,7 @@ const ( func StringToSType(s string) (Type, error) { switch Type(s) { case Accounts, Confirmations, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, ChainWalletBalances, - ChainTransfers, ChainDeposits, MntxGateway, PaymentGateway, FXOracle, FeePlans, FilterProjects, Invitations, Invoices, Logo, Ledger, + ChainTransfers, ChainDeposits, MntxGateway, PaymentGateway, FXOracle, FeePlans, BillingDocuments, FilterProjects, Invitations, Invoices, Logo, Ledger, LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications, Organizations, Payments, PaymentRoutes, PaymentPlanTemplates, PaymentOrchestrator, Permissions, Policies, PolicyAssignements, RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery: diff --git a/api/pkg/payments/rail/gateway.go b/api/pkg/payments/rail/gateway.go index 66766609..3cef11d0 100644 --- a/api/pkg/payments/rail/gateway.go +++ b/api/pkg/payments/rail/gateway.go @@ -3,6 +3,7 @@ package rail import ( "context" + "github.com/tech/sendico/pkg/model" paymenttypes "github.com/tech/sendico/pkg/payments/types" ) @@ -49,6 +50,8 @@ type TransferRequest struct { Metadata map[string]string ClientReference string DestinationMemo string + FromRole model.AccountRole + ToRole model.AccountRole } // BlockRequest defines the inputs for reserving value through a rail gateway. diff --git a/api/pkg/payments/rail/ledger.go b/api/pkg/payments/rail/ledger.go index ea3dc9f3..44a4315e 100644 --- a/api/pkg/payments/rail/ledger.go +++ b/api/pkg/payments/rail/ledger.go @@ -25,6 +25,7 @@ type LedgerTx struct { FromRail string ToRail string ExternalReferenceID string + Operation string FXRateUsed string IdempotencyKey string CreatedAt time.Time