separated quotation and payments
This commit is contained in:
15
api/payments/storage/model/operation.go
Normal file
15
api/payments/storage/model/operation.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
type OperationState string
|
||||
|
||||
const (
|
||||
OperationStateCreated OperationState = "created" // record exists, not started
|
||||
OperationStateProcessing OperationState = "processing" // we are working on it
|
||||
OperationStatePlanned OperationState = "planned" // waiting for execution
|
||||
OperationStateWaiting OperationState = "waiting" // waiting external world
|
||||
|
||||
OperationStateSuccess OperationState = "success" // final success
|
||||
OperationStateFailed OperationState = "failed" // final failure
|
||||
OperationStateCancelled OperationState = "cancelled" // final cancelled
|
||||
OperationStateSkipped OperationState = "skipped" // final skipped
|
||||
)
|
||||
539
api/payments/storage/model/payment.go
Normal file
539
api/payments/storage/model/payment.go
Normal file
@@ -0,0 +1,539 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
"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"
|
||||
CommitPolicyAfterFailure CommitPolicy = "AFTER_FAILURE"
|
||||
CommitPolicyAfterCanceled CommitPolicy = "AFTER_TERMINAL"
|
||||
)
|
||||
|
||||
// PaymentState enumerates lifecycle phases.
|
||||
type PaymentState string
|
||||
|
||||
const (
|
||||
PaymentStateUnspecified PaymentState = "unspecified"
|
||||
PaymentStateAccepted PaymentState = "accepted"
|
||||
PaymentStateFundsReserved PaymentState = "funds_reserved"
|
||||
PaymentStateSubmitted PaymentState = "submitted"
|
||||
PaymentStateSuccess PaymentState = "success"
|
||||
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"
|
||||
PaymentFailureCodeSettlement PaymentFailureCode = "settlement"
|
||||
)
|
||||
|
||||
// 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"
|
||||
RailOperationExternalDebit RailOperation = "EXTERNAL_DEBIT"
|
||||
RailOperationExternalCredit RailOperation = "EXTERNAL_CREDIT"
|
||||
RailOperationMove RailOperation = "MOVE"
|
||||
RailOperationSend RailOperation = "SEND"
|
||||
RailOperationFee RailOperation = "FEE"
|
||||
RailOperationObserveConfirm RailOperation = "OBSERVE_CONFIRM"
|
||||
RailOperationFXConvert RailOperation = "FX_CONVERT"
|
||||
RailOperationBlock RailOperation = "BLOCK"
|
||||
RailOperationRelease RailOperation = "RELEASE"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
CanBlock bool `bson:"canBlock,omitempty" json:"canBlock,omitempty"`
|
||||
CanRelease bool `bson:"canRelease,omitempty" json:"canRelease,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"`
|
||||
InvokeURI string `bson:"invokeUri,omitempty" json:"invokeUri,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 {
|
||||
Ref string `bson:"ref" json:"ref"`
|
||||
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"`
|
||||
DebitSettlementAmount *paymenttypes.Money `bson:"debitSettlementAmount,omitempty" json:"debitSettlementAmount,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"`
|
||||
FromRole *account_role.AccountRole `bson:"fromRole,omitempty" json:"fromRole,omitempty"`
|
||||
ToRole *account_role.AccountRole `bson:"toRole,omitempty" json:"toRole,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"`
|
||||
OperationRef string `bson:"operationRef,omitempty" json:"operationRef,omitempty"`
|
||||
Error string `bson:"error,omitempty" json:"error,omitempty"`
|
||||
State OperationState `bson:"state,omitempty" json:"state,omitempty"`
|
||||
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (s *ExecutionStep) IsTerminal() bool {
|
||||
if s.State == OperationStateSuccess ||
|
||||
s.State == OperationStateFailed ||
|
||||
s.State == OperationStateCancelled ||
|
||||
s.State == OperationStateSkipped {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *ExecutionStep) IsSuccess() bool {
|
||||
return s.State == OperationStateSuccess
|
||||
}
|
||||
|
||||
func (s *ExecutionStep) IsFailed() bool {
|
||||
return s.State == OperationStateFailed
|
||||
}
|
||||
|
||||
func (s *ExecutionStep) ReadyForNext() bool {
|
||||
switch s.State {
|
||||
case OperationStateSuccess,
|
||||
OperationStateSkipped:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
OrganizationRef 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, CommitPolicyAfterFailure, CommitPolicyAfterCanceled:
|
||||
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
|
||||
}
|
||||
93
api/payments/storage/model/plan_template.go
Normal file
93
api/payments/storage/model/plan_template.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
// OrchestrationStep defines a template step for execution planning.
|
||||
type OrchestrationStep struct {
|
||||
StepID string `bson:"stepId" json:"stepId"`
|
||||
Rail Rail `bson:"rail" json:"rail"`
|
||||
Operation string `bson:"operation" json:"operation"`
|
||||
DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"`
|
||||
CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"`
|
||||
CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"`
|
||||
FromRole *account_role.AccountRole `bson:"fromRole,omitempty" json:"fromRole,omitempty"`
|
||||
ToRole *account_role.AccountRole `bson:"toRole,omitempty" json:"toRole,omitempty"`
|
||||
}
|
||||
|
||||
// PaymentPlanTemplate stores reusable orchestration templates.
|
||||
type PaymentPlanTemplate struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
FromRail Rail `bson:"fromRail" json:"fromRail"`
|
||||
ToRail Rail `bson:"toRail" json:"toRail"`
|
||||
Network string `bson:"network,omitempty" json:"network,omitempty"`
|
||||
Steps []OrchestrationStep `bson:"steps,omitempty" json:"steps,omitempty"`
|
||||
IsEnabled bool `bson:"isEnabled" json:"isEnabled"`
|
||||
}
|
||||
|
||||
// Collection implements storable.Storable.
|
||||
func (*PaymentPlanTemplate) Collection() string {
|
||||
return mservice.PaymentPlanTemplates
|
||||
}
|
||||
|
||||
// Normalize standardizes template fields for matching and indexing.
|
||||
func (t *PaymentPlanTemplate) Normalize() {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
t.FromRail = Rail(strings.ToUpper(strings.TrimSpace(string(t.FromRail))))
|
||||
t.ToRail = Rail(strings.ToUpper(strings.TrimSpace(string(t.ToRail))))
|
||||
t.Network = strings.ToUpper(strings.TrimSpace(t.Network))
|
||||
if len(t.Steps) == 0 {
|
||||
return
|
||||
}
|
||||
for i := range t.Steps {
|
||||
step := &t.Steps[i]
|
||||
step.StepID = strings.TrimSpace(step.StepID)
|
||||
step.Rail = Rail(strings.ToUpper(strings.TrimSpace(string(step.Rail))))
|
||||
step.Operation = strings.ToLower(strings.TrimSpace(step.Operation))
|
||||
step.CommitPolicy = normalizeCommitPolicy(step.CommitPolicy)
|
||||
step.DependsOn = normalizeStringList(step.DependsOn)
|
||||
step.CommitAfter = normalizeStringList(step.CommitAfter)
|
||||
step.FromRole = normalizeAccountRole(step.FromRole)
|
||||
step.ToRole = normalizeAccountRole(step.ToRole)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAccountRole(role *account_role.AccountRole) *account_role.AccountRole {
|
||||
if role == nil {
|
||||
return nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(string(*role))
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
if parsed, ok := account_role.Parse(trimmed); ok {
|
||||
if parsed == "" {
|
||||
return nil
|
||||
}
|
||||
normalized := parsed
|
||||
return &normalized
|
||||
}
|
||||
normalized := account_role.AccountRole(strings.ToLower(trimmed))
|
||||
return &normalized
|
||||
}
|
||||
|
||||
// PaymentPlanTemplateFilter selects templates for lookup.
|
||||
type PaymentPlanTemplateFilter struct {
|
||||
FromRail Rail
|
||||
ToRail Rail
|
||||
Network string
|
||||
IsEnabled *bool
|
||||
}
|
||||
|
||||
// PaymentPlanTemplateList holds template results.
|
||||
type PaymentPlanTemplateList struct {
|
||||
Items []*PaymentPlanTemplate
|
||||
}
|
||||
29
api/payments/storage/model/quote.go
Normal file
29
api/payments/storage/model/quote.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
// PaymentQuoteRecord stores a quoted payment snapshot for later execution.
|
||||
type PaymentQuoteRecord struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
model.OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||
|
||||
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
||||
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
||||
Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"`
|
||||
Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"`
|
||||
Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"`
|
||||
Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"`
|
||||
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
||||
PurgeAt time.Time `bson:"purgeAt,omitempty" json:"purgeAt,omitempty"`
|
||||
Hash string `bson:"hash" json:"hash"`
|
||||
}
|
||||
|
||||
// Collection implements storable.Storable.
|
||||
func (*PaymentQuoteRecord) Collection() string {
|
||||
return "payment_quotes"
|
||||
}
|
||||
47
api/payments/storage/model/route.go
Normal file
47
api/payments/storage/model/route.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
// PaymentRoute defines an allowed rail transition for orchestration.
|
||||
type PaymentRoute struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
FromRail Rail `bson:"fromRail" json:"fromRail"`
|
||||
ToRail Rail `bson:"toRail" json:"toRail"`
|
||||
Network string `bson:"network,omitempty" json:"network,omitempty"`
|
||||
RequiresObserve bool `bson:"requiresObserve,omitempty" json:"requiresObserve,omitempty"`
|
||||
IsEnabled bool `bson:"isEnabled" json:"isEnabled"`
|
||||
}
|
||||
|
||||
// Collection implements storable.Storable.
|
||||
func (*PaymentRoute) Collection() string {
|
||||
return mservice.PaymentRoutes
|
||||
}
|
||||
|
||||
// Normalize standardizes route fields for consistent indexing and matching.
|
||||
func (r *PaymentRoute) Normalize() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.FromRail = Rail(strings.ToUpper(strings.TrimSpace(string(r.FromRail))))
|
||||
r.ToRail = Rail(strings.ToUpper(strings.TrimSpace(string(r.ToRail))))
|
||||
r.Network = strings.ToUpper(strings.TrimSpace(r.Network))
|
||||
}
|
||||
|
||||
// PaymentRouteFilter selects routes for lookup.
|
||||
type PaymentRouteFilter struct {
|
||||
FromRail Rail
|
||||
ToRail Rail
|
||||
Network string
|
||||
IsEnabled *bool
|
||||
}
|
||||
|
||||
// PaymentRouteList holds route results.
|
||||
type PaymentRouteList struct {
|
||||
Items []*PaymentRoute
|
||||
}
|
||||
Reference in New Issue
Block a user