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 }