Fully separated payment quotation and orchestration flows
This commit is contained in:
365
api/payments/quotation/internal/service/plan/helpers.go
Normal file
365
api/payments/quotation/internal/service/plan/helpers.go
Normal file
@@ -0,0 +1,365 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
|
||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
)
|
||||
|
||||
type moneyGetter interface {
|
||||
GetAmount() string
|
||||
GetCurrency() string
|
||||
}
|
||||
|
||||
func cloneMoney(input *paymenttypes.Money) *paymenttypes.Money {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{Currency: input.GetCurrency(), Amount: input.GetAmount()}
|
||||
}
|
||||
|
||||
func cloneStringList(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
clean := strings.TrimSpace(value)
|
||||
if clean == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, clean)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func cloneMetadata(input map[string]string) map[string]string {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
clone := make(map[string]string, len(input))
|
||||
for k, v := range input {
|
||||
clone[k] = strings.TrimSpace(v)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func attributeLookup(attrs map[string]string, keys ...string) string {
|
||||
if len(attrs) == 0 || len(keys) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, key := range keys {
|
||||
needle := strings.ToLower(strings.TrimSpace(key))
|
||||
if needle == "" {
|
||||
continue
|
||||
}
|
||||
for attrKey, value := range attrs {
|
||||
if strings.EqualFold(strings.TrimSpace(attrKey), needle) {
|
||||
if val := strings.TrimSpace(value); val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decimalFromMoney(m moneyGetter) (decimal.Decimal, error) {
|
||||
if m == nil {
|
||||
return decimal.Zero, nil
|
||||
}
|
||||
return decimal.NewFromString(m.GetAmount())
|
||||
}
|
||||
|
||||
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 cloneProtoMoney(input *moneyv1.Money) *moneyv1.Money {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{Currency: input.GetCurrency(), Amount: input.GetAmount()}
|
||||
}
|
||||
|
||||
func decimalFromProto(value *moneyv1.Decimal) *paymenttypes.Decimal {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Decimal{Value: value.GetValue()}
|
||||
}
|
||||
|
||||
func decimalToProto(value *paymenttypes.Decimal) *moneyv1.Decimal {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Decimal{Value: value.GetValue()}
|
||||
}
|
||||
|
||||
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 fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote {
|
||||
if quote == nil {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.FXQuote{
|
||||
QuoteRef: strings.TrimSpace(quote.GetQuoteRef()),
|
||||
Pair: pairFromProto(quote.GetPair()),
|
||||
Side: fxSideFromProto(quote.GetSide()),
|
||||
Price: decimalFromProto(quote.GetPrice()),
|
||||
BaseAmount: moneyFromProto(quote.GetBaseAmount()),
|
||||
QuoteAmount: moneyFromProto(quote.GetQuoteAmount()),
|
||||
ExpiresAtUnixMs: quote.GetExpiresAtUnixMs(),
|
||||
Provider: strings.TrimSpace(quote.GetProvider()),
|
||||
RateRef: strings.TrimSpace(quote.GetRateRef()),
|
||||
Firm: quote.GetFirm(),
|
||||
}
|
||||
}
|
||||
|
||||
func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote {
|
||||
if quote == nil {
|
||||
return nil
|
||||
}
|
||||
return &oraclev1.Quote{
|
||||
QuoteRef: strings.TrimSpace(quote.QuoteRef),
|
||||
Pair: pairToProto(quote.Pair),
|
||||
Side: fxSideToProto(quote.Side),
|
||||
Price: decimalToProto(quote.Price),
|
||||
BaseAmount: protoMoney(quote.BaseAmount),
|
||||
QuoteAmount: protoMoney(quote.QuoteAmount),
|
||||
ExpiresAtUnixMs: quote.ExpiresAtUnixMs,
|
||||
Provider: strings.TrimSpace(quote.Provider),
|
||||
RateRef: strings.TrimSpace(quote.RateRef),
|
||||
Firm: quote.Firm,
|
||||
}
|
||||
}
|
||||
|
||||
func entrySideFromProto(side accountingv1.EntrySide) paymenttypes.EntrySide {
|
||||
switch side {
|
||||
case accountingv1.EntrySide_ENTRY_SIDE_DEBIT:
|
||||
return paymenttypes.EntrySideDebit
|
||||
case accountingv1.EntrySide_ENTRY_SIDE_CREDIT:
|
||||
return paymenttypes.EntrySideCredit
|
||||
default:
|
||||
return paymenttypes.EntrySideUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func entrySideToProto(side paymenttypes.EntrySide) accountingv1.EntrySide {
|
||||
switch side {
|
||||
case paymenttypes.EntrySideDebit:
|
||||
return accountingv1.EntrySide_ENTRY_SIDE_DEBIT
|
||||
case paymenttypes.EntrySideCredit:
|
||||
return accountingv1.EntrySide_ENTRY_SIDE_CREDIT
|
||||
default:
|
||||
return accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func postingLineTypeFromProto(lineType accountingv1.PostingLineType) paymenttypes.PostingLineType {
|
||||
switch lineType {
|
||||
case accountingv1.PostingLineType_POSTING_LINE_FEE:
|
||||
return paymenttypes.PostingLineTypeFee
|
||||
case accountingv1.PostingLineType_POSTING_LINE_TAX:
|
||||
return paymenttypes.PostingLineTypeTax
|
||||
case accountingv1.PostingLineType_POSTING_LINE_SPREAD:
|
||||
return paymenttypes.PostingLineTypeSpread
|
||||
case accountingv1.PostingLineType_POSTING_LINE_REVERSAL:
|
||||
return paymenttypes.PostingLineTypeReversal
|
||||
default:
|
||||
return paymenttypes.PostingLineTypeUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func postingLineTypeToProto(lineType paymenttypes.PostingLineType) accountingv1.PostingLineType {
|
||||
switch lineType {
|
||||
case paymenttypes.PostingLineTypeFee:
|
||||
return accountingv1.PostingLineType_POSTING_LINE_FEE
|
||||
case paymenttypes.PostingLineTypeTax:
|
||||
return accountingv1.PostingLineType_POSTING_LINE_TAX
|
||||
case paymenttypes.PostingLineTypeSpread:
|
||||
return accountingv1.PostingLineType_POSTING_LINE_SPREAD
|
||||
case paymenttypes.PostingLineTypeReversal:
|
||||
return accountingv1.PostingLineType_POSTING_LINE_REVERSAL
|
||||
default:
|
||||
return accountingv1.PostingLineType_POSTING_LINE_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func feeLinesFromProto(lines []*feesv1.DerivedPostingLine) []*paymenttypes.FeeLine {
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]*paymenttypes.FeeLine, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if line == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, &paymenttypes.FeeLine{
|
||||
LedgerAccountRef: strings.TrimSpace(line.GetLedgerAccountRef()),
|
||||
Money: moneyFromProto(line.GetMoney()),
|
||||
LineType: postingLineTypeFromProto(line.GetLineType()),
|
||||
Side: entrySideFromProto(line.GetSide()),
|
||||
Meta: cloneMetadata(line.GetMeta()),
|
||||
})
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func feeLinesToProto(lines []*paymenttypes.FeeLine) []*feesv1.DerivedPostingLine {
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]*feesv1.DerivedPostingLine, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if line == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, &feesv1.DerivedPostingLine{
|
||||
LedgerAccountRef: strings.TrimSpace(line.LedgerAccountRef),
|
||||
Money: protoMoney(line.Money),
|
||||
LineType: postingLineTypeToProto(line.LineType),
|
||||
Side: entrySideToProto(line.Side),
|
||||
Meta: cloneMetadata(line.Meta),
|
||||
})
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func executionQuote(payment *model.Payment, quote *orchestratorv1.PaymentQuote) *orchestratorv1.PaymentQuote {
|
||||
if quote != nil {
|
||||
return quote
|
||||
}
|
||||
if payment != nil && payment.LastQuote != nil {
|
||||
return &orchestratorv1.PaymentQuote{
|
||||
DebitAmount: protoMoney(payment.LastQuote.DebitAmount),
|
||||
DebitSettlementAmount: protoMoney(payment.LastQuote.DebitSettlementAmount),
|
||||
ExpectedSettlementAmount: protoMoney(payment.LastQuote.ExpectedSettlementAmount),
|
||||
ExpectedFeeTotal: protoMoney(payment.LastQuote.ExpectedFeeTotal),
|
||||
FeeLines: feeLinesToProto(payment.LastQuote.FeeLines),
|
||||
FxQuote: fxQuoteToProto(payment.LastQuote.FXQuote),
|
||||
QuoteRef: strings.TrimSpace(payment.LastQuote.QuoteRef),
|
||||
}
|
||||
}
|
||||
return &orchestratorv1.PaymentQuote{}
|
||||
}
|
||||
|
||||
func makeMoney(currency string, value decimal.Decimal) *moneyv1.Money {
|
||||
return &moneyv1.Money{Currency: currency, Amount: value.String()}
|
||||
}
|
||||
|
||||
func ensureCurrency(m *moneyv1.Money, targetCurrency string, quote *oraclev1.Quote) (*moneyv1.Money, error) {
|
||||
if m == nil || strings.TrimSpace(targetCurrency) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if strings.EqualFold(m.GetCurrency(), targetCurrency) {
|
||||
return cloneProtoMoney(m), nil
|
||||
}
|
||||
return convertWithQuote(m, quote, targetCurrency)
|
||||
}
|
||||
|
||||
func convertWithQuote(m *moneyv1.Money, quote *oraclev1.Quote, targetCurrency string) (*moneyv1.Money, error) {
|
||||
if m == nil || quote == nil || quote.GetPair() == nil || quote.GetPrice() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
base := strings.TrimSpace(quote.GetPair().GetBase())
|
||||
qt := strings.TrimSpace(quote.GetPair().GetQuote())
|
||||
if base == "" || qt == "" || strings.TrimSpace(targetCurrency) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
price, err := decimal.NewFromString(quote.GetPrice().GetValue())
|
||||
if err != nil || price.IsZero() {
|
||||
return nil, err
|
||||
}
|
||||
value, err := decimalFromMoney(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case strings.EqualFold(m.GetCurrency(), base) && strings.EqualFold(targetCurrency, qt):
|
||||
return makeMoney(targetCurrency, value.Mul(price)), nil
|
||||
case strings.EqualFold(m.GetCurrency(), qt) && strings.EqualFold(targetCurrency, base):
|
||||
return makeMoney(targetCurrency, value.Div(price)), nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func cardPayoutAmount(payment *model.Payment) (*paymenttypes.Money, error) {
|
||||
if payment == nil {
|
||||
return nil, merrors.InvalidArgument("payment is required")
|
||||
}
|
||||
amount := cloneMoney(payment.Intent.Amount)
|
||||
if payment.LastQuote != nil {
|
||||
settlement := payment.LastQuote.ExpectedSettlementAmount
|
||||
if settlement != nil && strings.TrimSpace(settlement.GetAmount()) != "" && strings.TrimSpace(settlement.GetCurrency()) != "" {
|
||||
amount = cloneMoney(settlement)
|
||||
}
|
||||
}
|
||||
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||
return nil, merrors.InvalidArgument("card payout: amount is required")
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
Reference in New Issue
Block a user