package model import ( "strings" "time" "github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" paymenttypes "github.com/tech/sendico/pkg/payments/types" ) // PaymentKind captures the orchestrator intent type. type PaymentKind string const ( PaymentKindUnspecified PaymentKind = "unspecified" PaymentKindPayout PaymentKind = "payout" PaymentKindInternalTransfer PaymentKind = "internal_transfer" PaymentKindFXConversion PaymentKind = "fx_conversion" ) // SettlementMode defines how fees/FX variance is handled. type SettlementMode string const ( SettlementModeUnspecified SettlementMode = "unspecified" SettlementModeFixSource SettlementMode = "fix_source" SettlementModeFixReceived SettlementMode = "fix_received" ) // CommitPolicy controls when a step is committed during orchestration. type CommitPolicy string const ( CommitPolicyUnspecified CommitPolicy = "UNSPECIFIED" CommitPolicyImmediate CommitPolicy = "IMMEDIATE" CommitPolicyAfterSuccess CommitPolicy = "AFTER_SUCCESS" ) // 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" ) // Rail identifies a payment rail for orchestration. type Rail string const ( RailUnspecified Rail = "UNSPECIFIED" RailCrypto Rail = "CRYPTO" RailProviderSettlement Rail = "PROVIDER_SETTLEMENT" RailLedger Rail = "LEDGER" RailCardPayout Rail = "CARD_PAYOUT" RailFiatOnRamp Rail = "FIAT_ONRAMP" ) // RailOperation identifies an explicit action within a payment plan. type RailOperation string const ( RailOperationUnspecified RailOperation = "UNSPECIFIED" RailOperationDebit RailOperation = "DEBIT" RailOperationCredit RailOperation = "CREDIT" RailOperationSend RailOperation = "SEND" RailOperationFee RailOperation = "FEE" RailOperationObserveConfirm RailOperation = "OBSERVE_CONFIRM" RailOperationFXConvert RailOperation = "FX_CONVERT" ) // RailCapabilities are declared per gateway instance. type RailCapabilities struct { CanPayIn bool `bson:"canPayIn,omitempty" json:"canPayIn,omitempty"` CanPayOut bool `bson:"canPayOut,omitempty" json:"canPayOut,omitempty"` CanReadBalance bool `bson:"canReadBalance,omitempty" json:"canReadBalance,omitempty"` CanSendFee bool `bson:"canSendFee,omitempty" json:"canSendFee,omitempty"` RequiresObserveConfirm bool `bson:"requiresObserveConfirm,omitempty" json:"requiresObserveConfirm,omitempty"` } // LimitsOverride applies per-currency overrides for limits. type LimitsOverride struct { MaxVolume string `bson:"maxVolume,omitempty" json:"maxVolume,omitempty"` MinAmount string `bson:"minAmount,omitempty" json:"minAmount,omitempty"` MaxAmount string `bson:"maxAmount,omitempty" json:"maxAmount,omitempty"` MaxFee string `bson:"maxFee,omitempty" json:"maxFee,omitempty"` MaxOps int `bson:"maxOps,omitempty" json:"maxOps,omitempty"` } // Limits define time-bucketed and per-tx constraints. type Limits struct { MinAmount string `bson:"minAmount,omitempty" json:"minAmount,omitempty"` MaxAmount string `bson:"maxAmount,omitempty" json:"maxAmount,omitempty"` PerTxMaxFee string `bson:"perTxMaxFee,omitempty" json:"perTxMaxFee,omitempty"` PerTxMinAmount string `bson:"perTxMinAmount,omitempty" json:"perTxMinAmount,omitempty"` PerTxMaxAmount string `bson:"perTxMaxAmount,omitempty" json:"perTxMaxAmount,omitempty"` VolumeLimit map[string]string `bson:"volumeLimit,omitempty" json:"volumeLimit,omitempty"` VelocityLimit map[string]int `bson:"velocityLimit,omitempty" json:"velocityLimit,omitempty"` CurrencyLimits map[string]LimitsOverride `bson:"currencyLimits,omitempty" json:"currencyLimits,omitempty"` } // GatewayInstanceDescriptor standardizes gateway instance self-declaration. type GatewayInstanceDescriptor struct { ID string `bson:"id" json:"id"` InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"` Rail Rail `bson:"rail" json:"rail"` Network string `bson:"network,omitempty" json:"network,omitempty"` Currencies []string `bson:"currencies,omitempty" json:"currencies,omitempty"` Capabilities RailCapabilities `bson:"capabilities,omitempty" json:"capabilities,omitempty"` Limits Limits `bson:"limits,omitempty" json:"limits,omitempty"` Version string `bson:"version,omitempty" json:"version,omitempty"` IsEnabled bool `bson:"isEnabled" json:"isEnabled"` } // 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 *paymenttypes.Asset `bson:"asset,omitempty" json:"asset,omitempty"` } // ExternalChainEndpoint describes an external address. type ExternalChainEndpoint struct { Asset *paymenttypes.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"` InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"` 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 *paymenttypes.CurrencyPair `bson:"pair,omitempty" json:"pair,omitempty"` Side paymenttypes.FXSide `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 *paymenttypes.Money `bson:"amount" json:"amount"` RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"` FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"` FeePolicy *paymenttypes.FeePolicy `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"` SettlementMode SettlementMode `bson:"settlementMode,omitempty" json:"settlementMode,omitempty"` SettlementCurrency string `bson:"settlementCurrency,omitempty" json:"settlementCurrency,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 *paymenttypes.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"` ExpectedSettlementAmount *paymenttypes.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"` ExpectedFeeTotal *paymenttypes.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"` FeeLines []*paymenttypes.FeeLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"` FeeRules []*paymenttypes.AppliedRule `bson:"feeRules,omitempty" json:"feeRules,omitempty"` FXQuote *paymenttypes.FXQuote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"` NetworkFee *paymenttypes.NetworkFeeEstimate `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"` } // PaymentStep is an explicit action within a payment plan. type PaymentStep struct { StepID string `bson:"stepId,omitempty" json:"stepId,omitempty"` Rail Rail `bson:"rail" json:"rail"` GatewayID string `bson:"gatewayId,omitempty" json:"gatewayId,omitempty"` InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"` Action RailOperation `bson:"action" json:"action"` DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"` CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"` CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"` Amount *paymenttypes.Money `bson:"amount,omitempty" json:"amount,omitempty"` Ref string `bson:"ref,omitempty" json:"ref,omitempty"` } // PaymentPlan captures the ordered list of steps to execute a payment. type PaymentPlan struct { ID string `bson:"id,omitempty" json:"id,omitempty"` FXQuote *paymenttypes.FXQuote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"` Fees []*paymenttypes.FeeLine `bson:"fees,omitempty" json:"fees,omitempty"` Steps []*PaymentStep `bson:"steps,omitempty" json:"steps,omitempty"` IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotencyKey,omitempty"` CreatedAt time.Time `bson:"createdAt,omitempty" json:"createdAt,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 *paymenttypes.Money `bson:"amount,omitempty" json:"amount,omitempty"` NetworkFee *paymenttypes.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 *paymenttypes.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"` PaymentPlan *PaymentPlan `bson:"paymentPlan,omitempty" json:"paymentPlan,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) } } p.Intent.SettlementCurrency = strings.TrimSpace(p.Intent.SettlementCurrency) 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) } } } } if p.PaymentPlan != nil { p.PaymentPlan.ID = strings.TrimSpace(p.PaymentPlan.ID) p.PaymentPlan.IdempotencyKey = strings.TrimSpace(p.PaymentPlan.IdempotencyKey) for _, step := range p.PaymentPlan.Steps { if step == nil { continue } step.StepID = strings.TrimSpace(step.StepID) step.Rail = Rail(strings.TrimSpace(string(step.Rail))) step.GatewayID = strings.TrimSpace(step.GatewayID) step.InstanceID = strings.TrimSpace(step.InstanceID) step.Action = RailOperation(strings.TrimSpace(string(step.Action))) step.CommitPolicy = normalizeCommitPolicy(step.CommitPolicy) step.DependsOn = normalizeStringList(step.DependsOn) step.CommitAfter = normalizeStringList(step.CommitAfter) step.Ref = strings.TrimSpace(step.Ref) } } } func normalizeEndpoint(ep *PaymentEndpoint) { if ep == nil { return } ep.InstanceID = strings.TrimSpace(ep.InstanceID) 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.Chain = strings.TrimSpace(strings.ToUpper(ep.ManagedWallet.Asset.Chain)) 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.Chain = strings.TrimSpace(strings.ToUpper(ep.ExternalChain.Asset.Chain)) 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) } } } func normalizeCommitPolicy(policy CommitPolicy) CommitPolicy { val := strings.ToUpper(strings.TrimSpace(string(policy))) switch CommitPolicy(val) { case CommitPolicyImmediate, CommitPolicyAfterSuccess: return CommitPolicy(val) default: if val == "" { return CommitPolicyUnspecified } return CommitPolicy(val) } } func normalizeStringList(items []string) []string { if len(items) == 0 { return nil } result := make([]string, 0, len(items)) for _, item := range items { clean := strings.TrimSpace(item) if clean == "" { continue } result = append(result, clean) } if len(result) == 0 { return nil } return result }