pkg update #371

Merged
tech merged 1 commits from pkg-369 into main 2026-01-30 16:01:36 +00:00
13 changed files with 389 additions and 30 deletions
Showing only changes of commit 809370bda8 - Show all commits

View File

@@ -1,3 +1,4 @@
internal/generated internal/generated
.gocache .gocache
app app
tmp

View File

@@ -1,7 +1,6 @@
package grpcimp package grpcimp
import ( import (
"fmt"
"net" "net"
"os" "os"
"strings" "strings"
@@ -59,23 +58,7 @@ func (c *Config) DiscoveryInvokeURI() string {
return "" return ""
} }
return fmt.Sprintf("%s://%s:%s", c.discoveryScheme(), host, addrPort) return net.JoinHostPort(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"
} }
func splitHostPort(address string) (string, string) { func splitHostPort(address string) (string, string) {

View File

@@ -166,5 +166,5 @@ func DefaultInvokeURI(service string) string {
if clean == "" { if clean == "" {
return "" return ""
} }
return "grpc://" + clean return clean
} }

View File

@@ -40,10 +40,10 @@ func announcementFields(announce Announcement) []zap.Field {
fields = append(fields, zap.Int("currencies", len(announce.Currencies))) fields = append(fields, zap.Int("currencies", len(announce.Currencies)))
} }
if announce.Health.IntervalSec > 0 { 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 { 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 return fields
} }

View File

@@ -93,6 +93,6 @@ require (
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.5.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 gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -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= 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 h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= 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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
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/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= 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/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -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 { func IsAccountTypeUnspecified(value string) bool {
switch strings.ToUpper(strings.TrimSpace(value)) { switch strings.ToUpper(strings.TrimSpace(value)) {
case "", "ACCOUNT_TYPE_UNSPECIFIED", "UNSPECIFIED": case "", "ACCOUNT_TYPE_UNSPECIFIED", "UNSPECIFIED":
@@ -53,3 +84,12 @@ func IsAccountStatusUnspecified(value string) bool {
return false return false
} }
} }
func IsAccountRoleUnspecified(value string) bool {
switch strings.ToUpper(strings.TrimSpace(value)) {
case "", "ACCOUNT_ROLE_UNSPECIFIED", "UNSPECIFIED":
return true
default:
return false
}
}

View File

@@ -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 ""
}
}

View File

@@ -1,11 +1,93 @@
package model package model
import "strings"
// ChainNetwork identifies a blockchain network for routing and discovery.
type ChainNetwork string type ChainNetwork string
const ( const (
ChainNetworkARB ChainNetwork = "arbitrum_one" ChainNetworkArbitrumOne ChainNetwork = "arbitrum_one"
ChainNetworkEthMain ChainNetwork = "ethereum_mainnet" ChainNetworkArbitrumSepolia ChainNetwork = "arbitrum_sepolia"
ChainNetworkTronMain ChainNetwork = "tron_mainnet" ChainNetworkEthereumMainnet ChainNetwork = "ethereum_mainnet"
ChainNetworkTronNile ChainNetwork = "tron_nile" ChainNetworkTronMainnet ChainNetwork = "tron_mainnet"
ChainNetworkUnspecified ChainNetwork = "unspecified" 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
}
}

131
api/pkg/model/ledger.go Normal file
View File

@@ -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
}

View File

@@ -17,6 +17,7 @@ const (
PaymentGateway Type = "payment_gateway" // Represents payment gateway microservice PaymentGateway Type = "payment_gateway" // Represents payment gateway microservice
FXOracle Type = "fx_oracle" // Represents FX oracle microservice FXOracle Type = "fx_oracle" // Represents FX oracle microservice
FeePlans Type = "fee_plans" // Represents fee plans 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 FilterProjects Type = "filter_projects" // Represents comments on tasks or other resources
Invitations Type = "invitations" // Represents invitations sent to users Invitations Type = "invitations" // Represents invitations sent to users
Invoices Type = "invoices" // Represents invoices Invoices Type = "invoices" // Represents invoices
@@ -55,7 +56,7 @@ const (
func StringToSType(s string) (Type, error) { func StringToSType(s string) (Type, error) {
switch Type(s) { switch Type(s) {
case Accounts, Confirmations, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, ChainWalletBalances, 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, LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications,
Organizations, Payments, PaymentRoutes, PaymentPlanTemplates, PaymentOrchestrator, Permissions, Policies, PolicyAssignements, Organizations, Payments, PaymentRoutes, PaymentPlanTemplates, PaymentOrchestrator, Permissions, Policies, PolicyAssignements,
RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery: RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery:

View File

@@ -3,6 +3,7 @@ package rail
import ( import (
"context" "context"
"github.com/tech/sendico/pkg/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types" paymenttypes "github.com/tech/sendico/pkg/payments/types"
) )
@@ -49,6 +50,8 @@ type TransferRequest struct {
Metadata map[string]string Metadata map[string]string
ClientReference string ClientReference string
DestinationMemo string DestinationMemo string
FromRole model.AccountRole
ToRole model.AccountRole
} }
// BlockRequest defines the inputs for reserving value through a rail gateway. // BlockRequest defines the inputs for reserving value through a rail gateway.

View File

@@ -25,6 +25,7 @@ type LedgerTx struct {
FromRail string FromRail string
ToRail string ToRail string
ExternalReferenceID string ExternalReferenceID string
Operation string
FXRateUsed string FXRateUsed string
IdempotencyKey string IdempotencyKey string
CreatedAt time.Time CreatedAt time.Time