unified gateway interface
This commit is contained in:
83
api/pkg/payments/rail/gateway.go
Normal file
83
api/pkg/payments/rail/gateway.go
Normal 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)
|
||||
}
|
||||
38
api/pkg/payments/rail/ledger.go
Normal file
38
api/pkg/payments/rail/ledger.go
Normal 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
|
||||
}
|
||||
49
api/pkg/payments/types/chain.go
Normal file
49
api/pkg/payments/types/chain.go
Normal 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
|
||||
}
|
||||
94
api/pkg/payments/types/fees.go
Normal file
94
api/pkg/payments/types/fees.go
Normal 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"`
|
||||
}
|
||||
107
api/pkg/payments/types/fx.go
Normal file
107
api/pkg/payments/types/fx.go
Normal 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
|
||||
}
|
||||
33
api/pkg/payments/types/money.go
Normal file
33
api/pkg/payments/types/money.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user