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