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" ) // 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" ) // 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"` } // 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"` 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"` Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,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"` FeeQuoteToken string `bson:"feeQuoteToken,omitempty" json:"feeQuoteToken,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"` } // 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"` Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,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.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) } } 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)) } } } }