unified gateway interface

This commit is contained in:
Stephan D
2025-12-31 17:47:32 +01:00
parent 19b7b69bd8
commit 97ba7500dc
104 changed files with 8228 additions and 1742 deletions

View File

@@ -0,0 +1,83 @@
package rail
import (
"context"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
// Money represents a currency amount using decimal-safe strings.
type Money = paymenttypes.Money
const (
TransferStatusUnspecified = "UNSPECIFIED"
TransferStatusSuccess = "SUCCESS"
TransferStatusFailed = "FAILED"
TransferStatusRejected = "REJECTED"
TransferStatusPending = "PENDING"
)
// RailCapabilities are declared per gateway instance.
type RailCapabilities struct {
CanPayIn bool
CanPayOut bool
CanReadBalance bool
CanSendFee bool
RequiresObserveConfirm bool
}
// FeeBreakdown provides a gateway-level fee description.
type FeeBreakdown struct {
FeeCode string
Amount *Money
Description string
}
// TransferRequest defines the inputs for sending value through a rail gateway.
type TransferRequest struct {
OrganizationRef string
FromAccountID string
ToAccountID string
Currency string
Network string
Amount string
Fee *Money
Fees []FeeBreakdown
IdempotencyKey string
Metadata map[string]string
ClientReference string
DestinationMemo string
}
// RailResult reports the outcome of a rail gateway operation.
type RailResult struct {
ReferenceID string
Status string
FinalAmount *Money
Error *RailError
}
// ObserveResult reports the outcome of a confirmation observation.
type ObserveResult struct {
ReferenceID string
Status string
FinalAmount *Money
Error *RailError
}
// RailError captures structured failure details from a gateway.
type RailError struct {
Code string
Message string
CanRetry bool
ShouldRollback bool
}
// RailGateway exposes unified gateway operations for external rails.
type RailGateway interface {
Rail() string
Network() string
Capabilities() RailCapabilities
Send(ctx context.Context, req TransferRequest) (RailResult, error)
Observe(ctx context.Context, referenceID string) (ObserveResult, error)
}

View File

@@ -0,0 +1,38 @@
package rail
import (
"context"
"time"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
)
// InternalLedger exposes unified ledger operations for orchestration.
type InternalLedger interface {
ReadBalance(ctx context.Context, accountID string) (*moneyv1.Money, error)
CreateTransaction(ctx context.Context, tx LedgerTx) (string, error)
HoldBalance(ctx context.Context, accountID string, amount string) error
}
// LedgerTx captures ledger posting context used by orchestration.
type LedgerTx struct {
PaymentPlanID string
Currency string
Amount string
FeeAmount string
FromRail string
ToRail string
ExternalReferenceID string
FXRateUsed string
IdempotencyKey string
CreatedAt time.Time
// Internal fields required to map into the ledger API.
OrganizationRef string
LedgerAccountRef string
ContraLedgerAccountRef string
Description string
Charges []*ledgerv1.PostingLine
Metadata map[string]string
}

View File

@@ -0,0 +1,49 @@
package types
// Asset captures a chain and token identifier.
type Asset struct {
Chain string `bson:"chain,omitempty" json:"chain,omitempty"`
TokenSymbol string `bson:"tokenSymbol,omitempty" json:"tokenSymbol,omitempty"`
ContractAddress string `bson:"contractAddress,omitempty" json:"contractAddress,omitempty"`
}
func (a *Asset) GetChain() string {
if a == nil {
return ""
}
return a.Chain
}
func (a *Asset) GetTokenSymbol() string {
if a == nil {
return ""
}
return a.TokenSymbol
}
func (a *Asset) GetContractAddress() string {
if a == nil {
return ""
}
return a.ContractAddress
}
// NetworkFeeEstimate captures network fee estimation output.
type NetworkFeeEstimate struct {
NetworkFee *Money `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
EstimationContext string `bson:"estimationContext,omitempty" json:"estimationContext,omitempty"`
}
func (n *NetworkFeeEstimate) GetNetworkFee() *Money {
if n == nil {
return nil
}
return n.NetworkFee
}
func (n *NetworkFeeEstimate) GetEstimationContext() string {
if n == nil {
return ""
}
return n.EstimationContext
}

View File

@@ -0,0 +1,94 @@
package types
// InsufficientNetPolicy indicates how to handle insufficient net funds for fees.
type InsufficientNetPolicy string
const (
InsufficientNetUnspecified InsufficientNetPolicy = "UNSPECIFIED"
InsufficientNetBlockPosting InsufficientNetPolicy = "BLOCK_POSTING"
InsufficientNetSweepOrgCash InsufficientNetPolicy = "SWEEP_ORG_CASH"
InsufficientNetInvoiceLater InsufficientNetPolicy = "INVOICE_LATER"
)
// FeePolicy captures optional fee policy overrides.
type FeePolicy struct {
InsufficientNet InsufficientNetPolicy `bson:"insufficientNet,omitempty" json:"insufficientNet,omitempty"`
}
// EntrySide captures debit/credit semantics for fee lines.
type EntrySide string
const (
EntrySideUnspecified EntrySide = "UNSPECIFIED"
EntrySideDebit EntrySide = "DEBIT"
EntrySideCredit EntrySide = "CREDIT"
)
// PostingLineType captures the semantic type of a fee line.
type PostingLineType string
const (
PostingLineTypeUnspecified PostingLineType = "UNSPECIFIED"
PostingLineTypeFee PostingLineType = "FEE"
PostingLineTypeTax PostingLineType = "TAX"
PostingLineTypeSpread PostingLineType = "SPREAD"
PostingLineTypeReversal PostingLineType = "REVERSAL"
)
// RoundingMode captures rounding behavior for fee rules.
type RoundingMode string
const (
RoundingModeUnspecified RoundingMode = "UNSPECIFIED"
RoundingModeHalfEven RoundingMode = "HALF_EVEN"
RoundingModeHalfUp RoundingMode = "HALF_UP"
RoundingModeDown RoundingMode = "DOWN"
)
// FeeLine stores derived fee posting data.
type FeeLine struct {
LedgerAccountRef string `bson:"ledgerAccountRef,omitempty" json:"ledgerAccountRef,omitempty"`
Money *Money `bson:"money,omitempty" json:"money,omitempty"`
LineType PostingLineType `bson:"lineType,omitempty" json:"lineType,omitempty"`
Side EntrySide `bson:"side,omitempty" json:"side,omitempty"`
Meta map[string]string `bson:"meta,omitempty" json:"meta,omitempty"`
}
func (l *FeeLine) GetMoney() *Money {
if l == nil {
return nil
}
return l.Money
}
func (l *FeeLine) GetSide() EntrySide {
if l == nil {
return EntrySideUnspecified
}
return l.Side
}
func (l *FeeLine) GetLineType() PostingLineType {
if l == nil {
return PostingLineTypeUnspecified
}
return l.LineType
}
func (l *FeeLine) GetLedgerAccountRef() string {
if l == nil {
return ""
}
return l.LedgerAccountRef
}
// AppliedRule stores fee rule audit data.
type AppliedRule struct {
RuleID string `bson:"ruleId,omitempty" json:"ruleId,omitempty"`
RuleVersion string `bson:"ruleVersion,omitempty" json:"ruleVersion,omitempty"`
Formula string `bson:"formula,omitempty" json:"formula,omitempty"`
Rounding RoundingMode `bson:"rounding,omitempty" json:"rounding,omitempty"`
TaxCode string `bson:"taxCode,omitempty" json:"taxCode,omitempty"`
TaxRate string `bson:"taxRate,omitempty" json:"taxRate,omitempty"`
Parameters map[string]string `bson:"parameters,omitempty" json:"parameters,omitempty"`
}

View File

@@ -0,0 +1,107 @@
package types
// CurrencyPair describes base/quote currencies.
type CurrencyPair struct {
Base string `bson:"base,omitempty" json:"base,omitempty"`
Quote string `bson:"quote,omitempty" json:"quote,omitempty"`
}
func (p *CurrencyPair) GetBase() string {
if p == nil {
return ""
}
return p.Base
}
func (p *CurrencyPair) GetQuote() string {
if p == nil {
return ""
}
return p.Quote
}
// FXSide indicates the direction of an FX trade.
type FXSide string
const (
FXSideUnspecified FXSide = "UNSPECIFIED"
FXSideBuyBaseSellQuote FXSide = "BUY_BASE_SELL_QUOTE"
FXSideSellBaseBuyQuote FXSide = "SELL_BASE_BUY_QUOTE"
)
// FXQuote captures a priced FX quote.
type FXQuote struct {
QuoteRef string `bson:"quoteRef,omitempty" json:"quoteRef,omitempty"`
Pair *CurrencyPair `bson:"pair,omitempty" json:"pair,omitempty"`
Side FXSide `bson:"side,omitempty" json:"side,omitempty"`
Price *Decimal `bson:"price,omitempty" json:"price,omitempty"`
BaseAmount *Money `bson:"baseAmount,omitempty" json:"baseAmount,omitempty"`
QuoteAmount *Money `bson:"quoteAmount,omitempty" json:"quoteAmount,omitempty"`
ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs,omitempty" json:"expiresAtUnixMs,omitempty"`
Provider string `bson:"provider,omitempty" json:"provider,omitempty"`
RateRef string `bson:"rateRef,omitempty" json:"rateRef,omitempty"`
Firm bool `bson:"firm,omitempty" json:"firm,omitempty"`
}
func (q *FXQuote) GetPair() *CurrencyPair {
if q == nil {
return nil
}
return q.Pair
}
func (q *FXQuote) GetSide() FXSide {
if q == nil {
return FXSideUnspecified
}
return q.Side
}
func (q *FXQuote) GetPrice() *Decimal {
if q == nil {
return nil
}
return q.Price
}
func (q *FXQuote) GetBaseAmount() *Money {
if q == nil {
return nil
}
return q.BaseAmount
}
func (q *FXQuote) GetQuoteAmount() *Money {
if q == nil {
return nil
}
return q.QuoteAmount
}
func (q *FXQuote) GetExpiresAtUnixMs() int64 {
if q == nil {
return 0
}
return q.ExpiresAtUnixMs
}
func (q *FXQuote) GetProvider() string {
if q == nil {
return ""
}
return q.Provider
}
func (q *FXQuote) GetRateRef() string {
if q == nil {
return ""
}
return q.RateRef
}
func (q *FXQuote) GetFirm() bool {
if q == nil {
return false
}
return q.Firm
}

View File

@@ -0,0 +1,33 @@
package types
// Decimal holds a decimal value as a string.
type Decimal struct {
Value string `bson:"value,omitempty" json:"value,omitempty"`
}
func (d *Decimal) GetValue() string {
if d == nil {
return ""
}
return d.Value
}
// Money represents a currency amount using decimal-safe strings.
type Money struct {
Amount string `bson:"amount,omitempty" json:"amount,omitempty"`
Currency string `bson:"currency,omitempty" json:"currency,omitempty"`
}
func (m *Money) GetAmount() string {
if m == nil {
return ""
}
return m.Amount
}
func (m *Money) GetCurrency() string {
if m == nil {
return ""
}
return m.Currency
}