366 lines
11 KiB
Go
366 lines
11 KiB
Go
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"
|
|
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/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 *sharedv1.PaymentQuote) *sharedv1.PaymentQuote {
|
|
if quote != nil {
|
|
return quote
|
|
}
|
|
if payment != nil && payment.LastQuote != nil {
|
|
return &sharedv1.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 &sharedv1.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
|
|
}
|