Files
sendico/api/payments/quotation/internal/service/quotation/convert.go
2026-02-24 18:33:12 +01:00

540 lines
17 KiB
Go

package quotation
import (
"strings"
"github.com/tech/sendico/payments/storage/model"
chainasset "github.com/tech/sendico/pkg/chain"
payecon "github.com/tech/sendico/pkg/payments/economics"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
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"
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
)
func intentFromProto(src *sharedv1.PaymentIntent) model.PaymentIntent {
if src == nil {
return model.PaymentIntent{}
}
attrs := cloneMetadata(src.GetAttributes())
feeTreatment := feeTreatmentFromAttributes(attrs)
delete(attrs, "fee_treatment")
delete(attrs, "feeTreatment")
delete(attrs, "settlement_mode")
delete(attrs, "settlementMode")
settlementCurrency := derivedSettlementCurrencyFromProtoIntent(src)
requiresFX := derivedRequiresFXFromProtoIntent(src, settlementCurrency)
intent := model.PaymentIntent{
Ref: src.GetRef(),
Kind: modelKindFromProto(src.GetKind()),
Source: endpointFromProto(src.GetSource()),
Destination: endpointFromProto(src.GetDestination()),
Amount: moneyFromProto(src.GetAmount()),
RequiresFX: requiresFX,
FeePolicy: feePolicyFromProto(src.GetFeePolicy()),
SettlementMode: settlementModeFromProto(src.GetSettlementMode()),
FeeTreatment: feeTreatment,
SettlementCurrency: settlementCurrency,
Attributes: attrs,
Customer: customerFromProto(src.GetCustomer()),
}
if src.GetFx() != nil {
intent.FX = fxIntentFromProto(src.GetFx())
}
return intent
}
func endpointFromProto(src *sharedv1.PaymentEndpoint) model.PaymentEndpoint {
if src == nil {
return model.PaymentEndpoint{Type: model.EndpointTypeUnspecified}
}
result := model.PaymentEndpoint{
Type: model.EndpointTypeUnspecified,
InstanceID: strings.TrimSpace(src.GetInstanceId()),
Metadata: cloneMetadata(src.GetMetadata()),
}
if ledger := src.GetLedger(); ledger != nil {
result.Type = model.EndpointTypeLedger
result.Ledger = &model.LedgerEndpoint{
LedgerAccountRef: strings.TrimSpace(ledger.GetLedgerAccountRef()),
ContraLedgerAccountRef: strings.TrimSpace(ledger.GetContraLedgerAccountRef()),
}
return result
}
if managed := src.GetManagedWallet(); managed != nil {
result.Type = model.EndpointTypeManagedWallet
result.ManagedWallet = &model.ManagedWalletEndpoint{
ManagedWalletRef: strings.TrimSpace(managed.GetManagedWalletRef()),
Asset: assetFromProto(managed.GetAsset()),
}
return result
}
if external := src.GetExternalChain(); external != nil {
result.Type = model.EndpointTypeExternalChain
result.ExternalChain = &model.ExternalChainEndpoint{
Asset: assetFromProto(external.GetAsset()),
Address: strings.TrimSpace(external.GetAddress()),
Memo: strings.TrimSpace(external.GetMemo()),
}
return result
}
if card := src.GetCard(); card != nil {
result.Type = model.EndpointTypeCard
result.Card = &model.CardEndpoint{
Pan: strings.TrimSpace(card.GetPan()),
Token: strings.TrimSpace(card.GetToken()),
Cardholder: strings.TrimSpace(card.GetCardholderName()),
CardholderSurname: strings.TrimSpace(card.GetCardholderSurname()),
ExpMonth: card.GetExpMonth(),
ExpYear: card.GetExpYear(),
Country: strings.TrimSpace(card.GetCountry()),
MaskedPan: strings.TrimSpace(card.GetMaskedPan()),
}
return result
}
return result
}
func fxIntentFromProto(src *sharedv1.FXIntent) *model.FXIntent {
if src == nil {
return nil
}
return &model.FXIntent{
Pair: pairFromProto(src.GetPair()),
Side: fxSideFromProto(src.GetSide()),
Firm: src.GetFirm(),
TTLMillis: src.GetTtlMs(),
PreferredProvider: strings.TrimSpace(src.GetPreferredProvider()),
MaxAgeMillis: src.GetMaxAgeMs(),
}
}
func protoIntentFromModel(src model.PaymentIntent) *sharedv1.PaymentIntent {
attrs := cloneMetadata(src.Attributes)
if attrs == nil {
attrs = map[string]string{}
}
if feeTreatment := strings.TrimSpace(string(src.FeeTreatment)); feeTreatment != "" && feeTreatment != string(model.FeeTreatmentUnspecified) {
attrs["fee_treatment"] = feeTreatment
}
if len(attrs) == 0 {
attrs = nil
}
settlementCurrency := strings.TrimSpace(src.SettlementCurrency)
if settlementCurrency == "" {
settlementCurrency = derivedSettlementCurrencyFromModelIntent(src)
}
intent := &sharedv1.PaymentIntent{
Ref: src.Ref,
Kind: protoKindFromModel(src.Kind),
Source: protoEndpointFromModel(src.Source),
Destination: protoEndpointFromModel(src.Destination),
Amount: protoMoney(src.Amount),
RequiresFx: derivedRequiresFXFromModelIntent(src, settlementCurrency),
FeePolicy: feePolicyToProto(src.FeePolicy),
SettlementMode: settlementModeToProto(src.SettlementMode),
SettlementCurrency: settlementCurrency,
Attributes: attrs,
Customer: protoCustomerFromModel(src.Customer),
}
if src.FX != nil {
intent.Fx = protoFXIntentFromModel(src.FX)
}
return intent
}
func feeTreatmentFromAttributes(attrs map[string]string) model.FeeTreatment {
if len(attrs) == 0 {
return model.FeeTreatmentAddToSource
}
keys := []string{"fee_treatment", "feeTreatment"}
for _, key := range keys {
if value := strings.TrimSpace(attrs[key]); value != "" {
switch payecon.ResolveFeeTreatmentFromStringOrDefault(value) {
case quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION:
return model.FeeTreatmentDeductFromDestination
default:
return model.FeeTreatmentAddToSource
}
}
}
return model.FeeTreatmentAddToSource
}
func derivedSettlementCurrencyFromProtoIntent(src *sharedv1.PaymentIntent) string {
if src == nil {
return ""
}
if fx := src.GetFx(); fx != nil && fx.GetPair() != nil {
if currency := settlementCurrencyFromPair(fx.GetPair(), fx.GetSide()); currency != "" {
return currency
}
}
if amount := src.GetAmount(); amount != nil {
if currency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency())); currency != "" {
return currency
}
}
return strings.ToUpper(strings.TrimSpace(src.GetSettlementCurrency()))
}
func derivedSettlementCurrencyFromModelIntent(src model.PaymentIntent) string {
if src.FX != nil && src.FX.Pair != nil {
pair := &fxv1.CurrencyPair{
Base: strings.TrimSpace(src.FX.Pair.Base),
Quote: strings.TrimSpace(src.FX.Pair.Quote),
}
if currency := settlementCurrencyFromPair(pair, fxSideToProto(src.FX.Side)); currency != "" {
return currency
}
}
if src.Amount != nil {
if currency := strings.ToUpper(strings.TrimSpace(src.Amount.Currency)); currency != "" {
return currency
}
}
return ""
}
func settlementCurrencyFromPair(pair *fxv1.CurrencyPair, side fxv1.Side) string {
if pair == nil {
return ""
}
base := strings.ToUpper(strings.TrimSpace(pair.GetBase()))
quote := strings.ToUpper(strings.TrimSpace(pair.GetQuote()))
switch side {
case fxv1.Side_BUY_BASE_SELL_QUOTE:
if base != "" {
return base
}
case fxv1.Side_SELL_BASE_BUY_QUOTE, fxv1.Side_SIDE_UNSPECIFIED:
if quote != "" {
return quote
}
}
if quote != "" {
return quote
}
return base
}
func derivedRequiresFXFromProtoIntent(src *sharedv1.PaymentIntent, settlementCurrency string) bool {
if src == nil {
return false
}
if fx := src.GetFx(); fx != nil && fx.GetPair() != nil {
return true
}
amount := src.GetAmount()
if amount == nil {
return false
}
amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
settlementCurrency = strings.ToUpper(strings.TrimSpace(settlementCurrency))
return amountCurrency != "" && settlementCurrency != "" && !strings.EqualFold(amountCurrency, settlementCurrency)
}
func derivedRequiresFXFromModelIntent(src model.PaymentIntent, settlementCurrency string) bool {
if src.FX != nil && src.FX.Pair != nil {
return true
}
if src.Amount == nil {
return false
}
amountCurrency := strings.ToUpper(strings.TrimSpace(src.Amount.Currency))
settlementCurrency = strings.ToUpper(strings.TrimSpace(settlementCurrency))
return amountCurrency != "" && settlementCurrency != "" && !strings.EqualFold(amountCurrency, settlementCurrency)
}
func customerFromProto(src *sharedv1.Customer) *model.Customer {
if src == nil {
return nil
}
return &model.Customer{
ID: strings.TrimSpace(src.GetId()),
FirstName: strings.TrimSpace(src.GetFirstName()),
MiddleName: strings.TrimSpace(src.GetMiddleName()),
LastName: strings.TrimSpace(src.GetLastName()),
IP: strings.TrimSpace(src.GetIp()),
Zip: strings.TrimSpace(src.GetZip()),
Country: strings.TrimSpace(src.GetCountry()),
State: strings.TrimSpace(src.GetState()),
City: strings.TrimSpace(src.GetCity()),
Address: strings.TrimSpace(src.GetAddress()),
}
}
func protoCustomerFromModel(src *model.Customer) *sharedv1.Customer {
if src == nil {
return nil
}
return &sharedv1.Customer{
Id: strings.TrimSpace(src.ID),
FirstName: strings.TrimSpace(src.FirstName),
MiddleName: strings.TrimSpace(src.MiddleName),
LastName: strings.TrimSpace(src.LastName),
Ip: strings.TrimSpace(src.IP),
Zip: strings.TrimSpace(src.Zip),
Country: strings.TrimSpace(src.Country),
State: strings.TrimSpace(src.State),
City: strings.TrimSpace(src.City),
Address: strings.TrimSpace(src.Address),
}
}
func protoEndpointFromModel(src model.PaymentEndpoint) *sharedv1.PaymentEndpoint {
endpoint := &sharedv1.PaymentEndpoint{
Metadata: cloneMetadata(src.Metadata),
InstanceId: strings.TrimSpace(src.InstanceID),
}
switch src.Type {
case model.EndpointTypeLedger:
if src.Ledger != nil {
endpoint.Endpoint = &sharedv1.PaymentEndpoint_Ledger{
Ledger: &sharedv1.LedgerEndpoint{
LedgerAccountRef: src.Ledger.LedgerAccountRef,
ContraLedgerAccountRef: src.Ledger.ContraLedgerAccountRef,
},
}
}
case model.EndpointTypeManagedWallet:
if src.ManagedWallet != nil {
endpoint.Endpoint = &sharedv1.PaymentEndpoint_ManagedWallet{
ManagedWallet: &sharedv1.ManagedWalletEndpoint{
ManagedWalletRef: src.ManagedWallet.ManagedWalletRef,
Asset: assetToProto(src.ManagedWallet.Asset),
},
}
}
case model.EndpointTypeExternalChain:
if src.ExternalChain != nil {
endpoint.Endpoint = &sharedv1.PaymentEndpoint_ExternalChain{
ExternalChain: &sharedv1.ExternalChainEndpoint{
Asset: assetToProto(src.ExternalChain.Asset),
Address: src.ExternalChain.Address,
Memo: src.ExternalChain.Memo,
},
}
}
case model.EndpointTypeCard:
if src.Card != nil {
card := &sharedv1.CardEndpoint{
CardholderName: src.Card.Cardholder,
CardholderSurname: src.Card.CardholderSurname,
ExpMonth: src.Card.ExpMonth,
ExpYear: src.Card.ExpYear,
Country: src.Card.Country,
MaskedPan: src.Card.MaskedPan,
}
if pan := strings.TrimSpace(src.Card.Pan); pan != "" {
card.Card = &sharedv1.CardEndpoint_Pan{Pan: pan}
}
if token := strings.TrimSpace(src.Card.Token); token != "" {
card.Card = &sharedv1.CardEndpoint_Token{Token: token}
}
endpoint.Endpoint = &sharedv1.PaymentEndpoint_Card{Card: card}
}
default:
// leave unspecified
}
return endpoint
}
func protoFXIntentFromModel(src *model.FXIntent) *sharedv1.FXIntent {
if src == nil {
return nil
}
return &sharedv1.FXIntent{
Pair: pairToProto(src.Pair),
Side: fxSideToProto(src.Side),
Firm: src.Firm,
TtlMs: src.TTLMillis,
PreferredProvider: src.PreferredProvider,
MaxAgeMs: src.MaxAgeMillis,
}
}
func protoKindFromModel(kind model.PaymentKind) sharedv1.PaymentKind {
switch kind {
case model.PaymentKindPayout:
return sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT
case model.PaymentKindInternalTransfer:
return sharedv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER
case model.PaymentKindFXConversion:
return sharedv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION
default:
return sharedv1.PaymentKind_PAYMENT_KIND_UNSPECIFIED
}
}
func modelKindFromProto(kind sharedv1.PaymentKind) model.PaymentKind {
switch kind {
case sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT:
return model.PaymentKindPayout
case sharedv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER:
return model.PaymentKindInternalTransfer
case sharedv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION:
return model.PaymentKindFXConversion
default:
return model.PaymentKindUnspecified
}
}
func settlementModeFromProto(mode paymentv1.SettlementMode) model.SettlementMode {
switch mode {
case paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE:
return model.SettlementModeFixSource
case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
return model.SettlementModeFixReceived
default:
return model.SettlementModeUnspecified
}
}
func settlementModeToProto(mode model.SettlementMode) paymentv1.SettlementMode {
switch mode {
case model.SettlementModeFixSource:
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
case model.SettlementModeFixReceived:
return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED
default:
return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED
}
}
func moneyFromProto(m *moneyv1.Money) *paymenttypes.Money {
if m == nil {
return nil
}
return &paymenttypes.Money{
Currency: m.GetCurrency(),
Amount: m.GetAmount(),
}
}
func protoMoney(m *paymenttypes.Money) *moneyv1.Money {
if m == nil {
return nil
}
return &moneyv1.Money{
Currency: m.GetCurrency(),
Amount: m.GetAmount(),
}
}
func feePolicyFromProto(src *feesv1.PolicyOverrides) *paymenttypes.FeePolicy {
if src == nil {
return nil
}
return &paymenttypes.FeePolicy{
InsufficientNet: insufficientPolicyFromProto(src.GetInsufficientNet()),
}
}
func feePolicyToProto(src *paymenttypes.FeePolicy) *feesv1.PolicyOverrides {
if src == nil {
return nil
}
return &feesv1.PolicyOverrides{
InsufficientNet: insufficientPolicyToProto(src.InsufficientNet),
}
}
func insufficientPolicyFromProto(policy feesv1.InsufficientNetPolicy) paymenttypes.InsufficientNetPolicy {
switch policy {
case feesv1.InsufficientNetPolicy_BLOCK_POSTING:
return paymenttypes.InsufficientNetBlockPosting
case feesv1.InsufficientNetPolicy_SWEEP_ORG_CASH:
return paymenttypes.InsufficientNetSweepOrgCash
case feesv1.InsufficientNetPolicy_INVOICE_LATER:
return paymenttypes.InsufficientNetInvoiceLater
default:
return paymenttypes.InsufficientNetUnspecified
}
}
func insufficientPolicyToProto(policy paymenttypes.InsufficientNetPolicy) feesv1.InsufficientNetPolicy {
switch policy {
case paymenttypes.InsufficientNetBlockPosting:
return feesv1.InsufficientNetPolicy_BLOCK_POSTING
case paymenttypes.InsufficientNetSweepOrgCash:
return feesv1.InsufficientNetPolicy_SWEEP_ORG_CASH
case paymenttypes.InsufficientNetInvoiceLater:
return feesv1.InsufficientNetPolicy_INVOICE_LATER
default:
return feesv1.InsufficientNetPolicy_INSUFFICIENT_NET_UNSPECIFIED
}
}
func pairFromProto(pair *fxv1.CurrencyPair) *paymenttypes.CurrencyPair {
if pair == nil {
return nil
}
return &paymenttypes.CurrencyPair{
Base: pair.GetBase(),
Quote: pair.GetQuote(),
}
}
func pairToProto(pair *paymenttypes.CurrencyPair) *fxv1.CurrencyPair {
if pair == nil {
return nil
}
return &fxv1.CurrencyPair{
Base: pair.GetBase(),
Quote: pair.GetQuote(),
}
}
func fxSideFromProto(side fxv1.Side) paymenttypes.FXSide {
switch side {
case fxv1.Side_BUY_BASE_SELL_QUOTE:
return paymenttypes.FXSideBuyBaseSellQuote
case fxv1.Side_SELL_BASE_BUY_QUOTE:
return paymenttypes.FXSideSellBaseBuyQuote
default:
return paymenttypes.FXSideUnspecified
}
}
func fxSideToProto(side paymenttypes.FXSide) fxv1.Side {
switch side {
case paymenttypes.FXSideBuyBaseSellQuote:
return fxv1.Side_BUY_BASE_SELL_QUOTE
case paymenttypes.FXSideSellBaseBuyQuote:
return fxv1.Side_SELL_BASE_BUY_QUOTE
default:
return fxv1.Side_SIDE_UNSPECIFIED
}
}
func assetFromProto(asset *chainv1.Asset) *paymenttypes.Asset {
if asset == nil {
return nil
}
return &paymenttypes.Asset{
Chain: chainasset.NetworkAlias(asset.GetChain()),
TokenSymbol: asset.GetTokenSymbol(),
ContractAddress: asset.GetContractAddress(),
}
}
func assetToProto(asset *paymenttypes.Asset) *chainv1.Asset {
if asset == nil {
return nil
}
return &chainv1.Asset{
Chain: chainasset.NetworkFromString(asset.Chain),
TokenSymbol: asset.TokenSymbol,
ContractAddress: asset.ContractAddress,
}
}