Files
sendico/api/payments/orchestrator/storage/model/payment.go
2025-12-26 15:14:31 +01:00

330 lines
16 KiB
Go

package model
import (
"strings"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
)
// PaymentKind captures the orchestrator intent type.
type PaymentKind string
const (
PaymentKindUnspecified PaymentKind = "unspecified"
PaymentKindPayout PaymentKind = "payout"
PaymentKindInternalTransfer PaymentKind = "internal_transfer"
PaymentKindFXConversion PaymentKind = "fx_conversion"
)
// PaymentState enumerates lifecycle phases.
type PaymentState string
const (
PaymentStateUnspecified PaymentState = "unspecified"
PaymentStateAccepted PaymentState = "accepted"
PaymentStateFundsReserved PaymentState = "funds_reserved"
PaymentStateSubmitted PaymentState = "submitted"
PaymentStateSettled PaymentState = "settled"
PaymentStateFailed PaymentState = "failed"
PaymentStateCancelled PaymentState = "cancelled"
)
// PaymentFailureCode captures terminal reasons.
type PaymentFailureCode string
const (
PaymentFailureCodeUnspecified PaymentFailureCode = "unspecified"
PaymentFailureCodeBalance PaymentFailureCode = "balance"
PaymentFailureCodeLedger PaymentFailureCode = "ledger"
PaymentFailureCodeFX PaymentFailureCode = "fx"
PaymentFailureCodeChain PaymentFailureCode = "chain"
PaymentFailureCodeFees PaymentFailureCode = "fees"
PaymentFailureCodePolicy PaymentFailureCode = "policy"
)
// PaymentEndpointType indicates how value should be routed.
type PaymentEndpointType string
const (
EndpointTypeUnspecified PaymentEndpointType = "unspecified"
EndpointTypeLedger PaymentEndpointType = "ledger"
EndpointTypeManagedWallet PaymentEndpointType = "managed_wallet"
EndpointTypeExternalChain PaymentEndpointType = "external_chain"
EndpointTypeCard PaymentEndpointType = "card"
)
// LedgerEndpoint describes ledger routing.
type LedgerEndpoint struct {
LedgerAccountRef string `bson:"ledgerAccountRef" json:"ledgerAccountRef"`
ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty" json:"contraLedgerAccountRef,omitempty"`
}
// ManagedWalletEndpoint describes managed wallet routing.
type ManagedWalletEndpoint struct {
ManagedWalletRef string `bson:"managedWalletRef" json:"managedWalletRef"`
Asset *chainv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
}
// ExternalChainEndpoint describes an external address.
type ExternalChainEndpoint struct {
Asset *chainv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
Address string `bson:"address" json:"address"`
Memo string `bson:"memo,omitempty" json:"memo,omitempty"`
}
// CardEndpoint describes a card payout destination.
type CardEndpoint struct {
Pan string `bson:"pan,omitempty" json:"pan,omitempty"`
Token string `bson:"token,omitempty" json:"token,omitempty"`
Cardholder string `bson:"cardholder,omitempty" json:"cardholder,omitempty"`
CardholderSurname string `bson:"cardholderSurname,omitempty" json:"cardholderSurname,omitempty"`
ExpMonth uint32 `bson:"expMonth,omitempty" json:"expMonth,omitempty"`
ExpYear uint32 `bson:"expYear,omitempty" json:"expYear,omitempty"`
Country string `bson:"country,omitempty" json:"country,omitempty"`
MaskedPan string `bson:"maskedPan,omitempty" json:"maskedPan,omitempty"`
}
// CardPayout stores gateway payout tracking info.
type CardPayout struct {
PayoutRef string `bson:"payoutRef,omitempty" json:"payoutRef,omitempty"`
ProviderPaymentID string `bson:"providerPaymentId,omitempty" json:"providerPaymentId,omitempty"`
Status string `bson:"status,omitempty" json:"status,omitempty"`
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
CardCountry string `bson:"cardCountry,omitempty" json:"cardCountry,omitempty"`
MaskedPan string `bson:"maskedPan,omitempty" json:"maskedPan,omitempty"`
ProviderCode string `bson:"providerCode,omitempty" json:"providerCode,omitempty"`
GatewayReference string `bson:"gatewayReference,omitempty" json:"gatewayReference,omitempty"`
}
// PaymentEndpoint is a polymorphic payment destination/source.
type PaymentEndpoint struct {
Type PaymentEndpointType `bson:"type" json:"type"`
Ledger *LedgerEndpoint `bson:"ledger,omitempty" json:"ledger,omitempty"`
ManagedWallet *ManagedWalletEndpoint `bson:"managedWallet,omitempty" json:"managedWallet,omitempty"`
ExternalChain *ExternalChainEndpoint `bson:"externalChain,omitempty" json:"externalChain,omitempty"`
Card *CardEndpoint `bson:"card,omitempty" json:"card,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
}
// FXIntent captures FX conversion preferences.
type FXIntent struct {
Pair *fxv1.CurrencyPair `bson:"pair,omitempty" json:"pair,omitempty"`
Side fxv1.Side `bson:"side,omitempty" json:"side,omitempty"`
Firm bool `bson:"firm,omitempty" json:"firm,omitempty"`
TTLMillis int64 `bson:"ttlMillis,omitempty" json:"ttlMillis,omitempty"`
PreferredProvider string `bson:"preferredProvider,omitempty" json:"preferredProvider,omitempty"`
MaxAgeMillis int32 `bson:"maxAgeMillis,omitempty" json:"maxAgeMillis,omitempty"`
}
// PaymentIntent models the requested payment operation.
type PaymentIntent struct {
Kind PaymentKind `bson:"kind" json:"kind"`
Source PaymentEndpoint `bson:"source" json:"source"`
Destination PaymentEndpoint `bson:"destination" json:"destination"`
Amount *moneyv1.Money `bson:"amount" json:"amount"`
RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"`
FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"`
FeePolicy *feesv1.PolicyOverrides `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"`
SettlementMode orchestratorv1.SettlementMode `bson:"settlementMode,omitempty" json:"settlementMode,omitempty"`
Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,omitempty"`
Customer *Customer `bson:"customer,omitempty" json:"customer,omitempty"`
}
// Customer captures payer/recipient identity details for downstream processing.
type Customer struct {
ID string `bson:"id,omitempty" json:"id,omitempty"`
FirstName string `bson:"firstName,omitempty" json:"firstName,omitempty"`
MiddleName string `bson:"middleName,omitempty" json:"middleName,omitempty"`
LastName string `bson:"lastName,omitempty" json:"lastName,omitempty"`
IP string `bson:"ip,omitempty" json:"ip,omitempty"`
Zip string `bson:"zip,omitempty" json:"zip,omitempty"`
Country string `bson:"country,omitempty" json:"country,omitempty"`
State string `bson:"state,omitempty" json:"state,omitempty"`
City string `bson:"city,omitempty" json:"city,omitempty"`
Address string `bson:"address,omitempty" json:"address,omitempty"`
}
// PaymentQuoteSnapshot stores the latest quote info.
type PaymentQuoteSnapshot struct {
DebitAmount *moneyv1.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"`
ExpectedSettlementAmount *moneyv1.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"`
ExpectedFeeTotal *moneyv1.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"`
FeeLines []*feesv1.DerivedPostingLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"`
FeeRules []*feesv1.AppliedRule `bson:"feeRules,omitempty" json:"feeRules,omitempty"`
FXQuote *oraclev1.Quote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"`
NetworkFee *chainv1.EstimateTransferFeeResponse `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
QuoteRef string `bson:"quoteRef,omitempty" json:"quoteRef,omitempty"`
}
// ExecutionRefs links to downstream systems.
type ExecutionRefs struct {
DebitEntryRef string `bson:"debitEntryRef,omitempty" json:"debitEntryRef,omitempty"`
CreditEntryRef string `bson:"creditEntryRef,omitempty" json:"creditEntryRef,omitempty"`
FXEntryRef string `bson:"fxEntryRef,omitempty" json:"fxEntryRef,omitempty"`
ChainTransferRef string `bson:"chainTransferRef,omitempty" json:"chainTransferRef,omitempty"`
CardPayoutRef string `bson:"cardPayoutRef,omitempty" json:"cardPayoutRef,omitempty"`
FeeTransferRef string `bson:"feeTransferRef,omitempty" json:"feeTransferRef,omitempty"`
}
// ExecutionStep describes a planned or executed payment step for reporting.
type ExecutionStep struct {
Code string `bson:"code,omitempty" json:"code,omitempty"`
Description string `bson:"description,omitempty" json:"description,omitempty"`
Amount *moneyv1.Money `bson:"amount,omitempty" json:"amount,omitempty"`
NetworkFee *moneyv1.Money `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
SourceWalletRef string `bson:"sourceWalletRef,omitempty" json:"sourceWalletRef,omitempty"`
DestinationRef string `bson:"destinationRef,omitempty" json:"destinationRef,omitempty"`
TransferRef string `bson:"transferRef,omitempty" json:"transferRef,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
}
// ExecutionPlan captures the ordered list of steps to execute a payment.
type ExecutionPlan struct {
Steps []*ExecutionStep `bson:"steps,omitempty" json:"steps,omitempty"`
TotalNetworkFee *moneyv1.Money `bson:"totalNetworkFee,omitempty" json:"totalNetworkFee,omitempty"`
}
// Payment persists orchestrated payment lifecycle.
type Payment struct {
storable.Base `bson:",inline" json:",inline"`
model.OrganizationBoundBase `bson:",inline" json:",inline"`
PaymentRef string `bson:"paymentRef" json:"paymentRef"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
Intent PaymentIntent `bson:"intent" json:"intent"`
State PaymentState `bson:"state" json:"state"`
FailureCode PaymentFailureCode `bson:"failureCode,omitempty" json:"failureCode,omitempty"`
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
LastQuote *PaymentQuoteSnapshot `bson:"lastQuote,omitempty" json:"lastQuote,omitempty"`
Execution *ExecutionRefs `bson:"execution,omitempty" json:"execution,omitempty"`
ExecutionPlan *ExecutionPlan `bson:"executionPlan,omitempty" json:"executionPlan,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
CardPayout *CardPayout `bson:"cardPayout,omitempty" json:"cardPayout,omitempty"`
}
// Collection implements storable.Storable.
func (*Payment) Collection() string {
return mservice.Payments
}
// PaymentFilter enables filtered queries.
type PaymentFilter struct {
States []PaymentState
SourceRef string
DestinationRef string
Cursor string
Limit int32
}
// PaymentList contains paginated results.
type PaymentList struct {
Items []*Payment
NextCursor string
}
// Normalize harmonises string fields for indexing and comparisons.
func (p *Payment) Normalize() {
p.PaymentRef = strings.TrimSpace(p.PaymentRef)
p.IdempotencyKey = strings.TrimSpace(p.IdempotencyKey)
p.FailureReason = strings.TrimSpace(p.FailureReason)
if p.Metadata != nil {
for k, v := range p.Metadata {
p.Metadata[k] = strings.TrimSpace(v)
}
}
normalizeEndpoint(&p.Intent.Source)
normalizeEndpoint(&p.Intent.Destination)
if p.Intent.Attributes != nil {
for k, v := range p.Intent.Attributes {
p.Intent.Attributes[k] = strings.TrimSpace(v)
}
}
if p.Intent.Customer != nil {
p.Intent.Customer.ID = strings.TrimSpace(p.Intent.Customer.ID)
p.Intent.Customer.FirstName = strings.TrimSpace(p.Intent.Customer.FirstName)
p.Intent.Customer.MiddleName = strings.TrimSpace(p.Intent.Customer.MiddleName)
p.Intent.Customer.LastName = strings.TrimSpace(p.Intent.Customer.LastName)
p.Intent.Customer.IP = strings.TrimSpace(p.Intent.Customer.IP)
p.Intent.Customer.Zip = strings.TrimSpace(p.Intent.Customer.Zip)
p.Intent.Customer.Country = strings.TrimSpace(p.Intent.Customer.Country)
p.Intent.Customer.State = strings.TrimSpace(p.Intent.Customer.State)
p.Intent.Customer.City = strings.TrimSpace(p.Intent.Customer.City)
p.Intent.Customer.Address = strings.TrimSpace(p.Intent.Customer.Address)
}
if p.Execution != nil {
p.Execution.DebitEntryRef = strings.TrimSpace(p.Execution.DebitEntryRef)
p.Execution.CreditEntryRef = strings.TrimSpace(p.Execution.CreditEntryRef)
p.Execution.FXEntryRef = strings.TrimSpace(p.Execution.FXEntryRef)
p.Execution.ChainTransferRef = strings.TrimSpace(p.Execution.ChainTransferRef)
}
if p.ExecutionPlan != nil {
for _, step := range p.ExecutionPlan.Steps {
if step == nil {
continue
}
step.Code = strings.TrimSpace(step.Code)
step.Description = strings.TrimSpace(step.Description)
step.SourceWalletRef = strings.TrimSpace(step.SourceWalletRef)
step.DestinationRef = strings.TrimSpace(step.DestinationRef)
step.TransferRef = strings.TrimSpace(step.TransferRef)
if step.Metadata != nil {
for k, v := range step.Metadata {
step.Metadata[k] = strings.TrimSpace(v)
}
}
}
}
}
func normalizeEndpoint(ep *PaymentEndpoint) {
if ep == nil {
return
}
if ep.Metadata != nil {
for k, v := range ep.Metadata {
ep.Metadata[k] = strings.TrimSpace(v)
}
}
switch ep.Type {
case EndpointTypeLedger:
if ep.Ledger != nil {
ep.Ledger.LedgerAccountRef = strings.TrimSpace(ep.Ledger.LedgerAccountRef)
ep.Ledger.ContraLedgerAccountRef = strings.TrimSpace(ep.Ledger.ContraLedgerAccountRef)
}
case EndpointTypeManagedWallet:
if ep.ManagedWallet != nil {
ep.ManagedWallet.ManagedWalletRef = strings.TrimSpace(ep.ManagedWallet.ManagedWalletRef)
if ep.ManagedWallet.Asset != nil {
ep.ManagedWallet.Asset.TokenSymbol = strings.TrimSpace(strings.ToUpper(ep.ManagedWallet.Asset.TokenSymbol))
ep.ManagedWallet.Asset.ContractAddress = strings.TrimSpace(strings.ToLower(ep.ManagedWallet.Asset.ContractAddress))
}
}
case EndpointTypeExternalChain:
if ep.ExternalChain != nil {
ep.ExternalChain.Address = strings.TrimSpace(strings.ToLower(ep.ExternalChain.Address))
ep.ExternalChain.Memo = strings.TrimSpace(ep.ExternalChain.Memo)
if ep.ExternalChain.Asset != nil {
ep.ExternalChain.Asset.TokenSymbol = strings.TrimSpace(strings.ToUpper(ep.ExternalChain.Asset.TokenSymbol))
ep.ExternalChain.Asset.ContractAddress = strings.TrimSpace(strings.ToLower(ep.ExternalChain.Asset.ContractAddress))
}
}
case EndpointTypeCard:
if ep.Card != nil {
ep.Card.Pan = strings.TrimSpace(ep.Card.Pan)
ep.Card.Token = strings.TrimSpace(ep.Card.Token)
ep.Card.Cardholder = strings.TrimSpace(ep.Card.Cardholder)
ep.Card.CardholderSurname = strings.TrimSpace(ep.Card.CardholderSurname)
ep.Card.Country = strings.TrimSpace(ep.Card.Country)
ep.Card.MaskedPan = strings.TrimSpace(ep.Card.MaskedPan)
}
}
}