+ quotation provider

This commit is contained in:
Stephan D
2025-12-11 01:13:13 +01:00
parent bdf766075e
commit a4481fb63d
102 changed files with 2242 additions and 246 deletions

View File

@@ -26,15 +26,44 @@ type ExternalChainEndpoint struct {
Memo string `json:"memo,omitempty"`
}
// CardEndpoint represents a card payout payload.
// CardEndpoint represents a card payout payload (PAN or network token).
type CardEndpoint struct {
Pan string `json:"pan,omitempty"`
Token string `json:"token,omitempty"`
Cardholder string `json:"cardholder,omitempty"`
ExpMonth uint32 `json:"exp_month,omitempty"`
ExpYear uint32 `json:"exp_year,omitempty"`
Country string `json:"country,omitempty"`
MaskedPan string `json:"masked_pan,omitempty"`
Pan string `json:"pan"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
ExpMonth uint32 `json:"exp_month,omitempty"`
ExpYear uint32 `json:"exp_year,omitempty"`
Country string `json:"country,omitempty"`
}
// CardTokenEndpoint represents a vaulted card token payout payload.
type CardTokenEndpoint struct {
Token string `json:"token"`
MaskedPan string `json:"masked_pan"`
}
// WalletEndpoint represents a Sendico wallet payout payload.
type WalletEndpoint struct {
WalletID string `json:"walletId"`
}
// BankAccountEndpoint represents a domestic bank account payout payload.
type BankAccountEndpoint struct {
RecipientName string `json:"recipientName"`
Inn string `json:"inn"`
Kpp string `json:"kpp"`
BankName string `json:"bankName"`
Bik string `json:"bik"`
AccountNumber string `json:"accountNumber"`
CorrespondentAccount string `json:"correspondentAccount"`
}
// IBANEndpoint represents an international bank account payout payload.
type IBANEndpoint struct {
IBAN string `json:"iban"`
AccountHolder string `json:"accountHolder"`
BIC string `json:"bic,omitempty"`
BankName string `json:"bankName,omitempty"`
}
// LegacyPaymentEndpoint mirrors the previous bag-of-pointers DTO for backward compatibility.

View File

@@ -10,9 +10,13 @@ type EndpointType string
const (
EndpointTypeLedger EndpointType = "ledger"
EndpointTypeManagedWallet EndpointType = "managed_wallet"
EndpointTypeExternalChain EndpointType = "external_chain"
EndpointTypeManagedWallet EndpointType = "managedWallet"
EndpointTypeExternalChain EndpointType = "cryptoAddress"
EndpointTypeCard EndpointType = "card"
EndpointTypeCardToken EndpointType = "cardToken"
EndpointTypeWallet EndpointType = "wallet"
EndpointTypeBankAccount EndpointType = "bankAccount"
EndpointTypeIBAN EndpointType = "iban"
)
// Endpoint is a discriminated union for payment endpoints.
@@ -35,10 +39,11 @@ func newEndpoint(kind EndpointType, payload interface{}, metadata map[string]str
}
func (e Endpoint) decodePayload(expected EndpointType, dst interface{}) error {
if e.Type == "" {
actual := normalizeEndpointType(e.Type)
if actual == "" {
return merrors.InvalidArgument("endpoint type is required")
}
if e.Type != expected {
if actual != expected {
return merrors.InvalidArgument("expected endpoint type " + string(expected) + ", got " + string(e.Type))
}
if len(e.Data) == 0 {
@@ -62,7 +67,7 @@ func (e *Endpoint) UnmarshalJSON(data []byte) error {
return merrors.InvalidArgument("endpoint type is required")
}
*e = Endpoint{
Type: envelope.Type,
Type: normalizeEndpointType(envelope.Type),
Data: envelope.Data,
Metadata: cloneStringMap(envelope.Metadata),
}
@@ -101,6 +106,22 @@ func NewCardEndpointDTO(payload CardEndpoint, metadata map[string]string) (Endpo
return newEndpoint(EndpointTypeCard, payload, metadata)
}
func NewCardTokenEndpointDTO(payload CardTokenEndpoint, metadata map[string]string) (Endpoint, error) {
return newEndpoint(EndpointTypeCardToken, payload, metadata)
}
func NewWalletEndpointDTO(payload WalletEndpoint, metadata map[string]string) (Endpoint, error) {
return newEndpoint(EndpointTypeWallet, payload, metadata)
}
func NewBankAccountEndpointDTO(payload BankAccountEndpoint, metadata map[string]string) (Endpoint, error) {
return newEndpoint(EndpointTypeBankAccount, payload, metadata)
}
func NewIBANEndpointDTO(payload IBANEndpoint, metadata map[string]string) (Endpoint, error) {
return newEndpoint(EndpointTypeIBAN, payload, metadata)
}
func (e Endpoint) DecodeLedger() (LedgerEndpoint, error) {
var payload LedgerEndpoint
return payload, e.decodePayload(EndpointTypeLedger, &payload)
@@ -121,6 +142,26 @@ func (e Endpoint) DecodeCard() (CardEndpoint, error) {
return payload, e.decodePayload(EndpointTypeCard, &payload)
}
func (e Endpoint) DecodeCardToken() (CardTokenEndpoint, error) {
var payload CardTokenEndpoint
return payload, e.decodePayload(EndpointTypeCardToken, &payload)
}
func (e Endpoint) DecodeWallet() (WalletEndpoint, error) {
var payload WalletEndpoint
return payload, e.decodePayload(EndpointTypeWallet, &payload)
}
func (e Endpoint) DecodeBankAccount() (BankAccountEndpoint, error) {
var payload BankAccountEndpoint
return payload, e.decodePayload(EndpointTypeBankAccount, &payload)
}
func (e Endpoint) DecodeIBAN() (IBANEndpoint, error) {
var payload IBANEndpoint
return payload, e.decodePayload(EndpointTypeIBAN, &payload)
}
func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) {
if old == nil {
return nil, nil
@@ -168,7 +209,7 @@ func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint,
Metadata: cloneStringMap(new.Metadata),
}
switch new.Type {
switch normalizeEndpointType(new.Type) {
case EndpointTypeLedger:
payload, err := new.DecodeLedger()
if err != nil {
@@ -199,6 +240,20 @@ func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint,
return legacy, nil
}
var endpointTypeAliases = map[EndpointType]EndpointType{
"managed_wallet": EndpointTypeManagedWallet,
"external_chain": EndpointTypeExternalChain,
"card_token": EndpointTypeCardToken,
"bank_account": EndpointTypeBankAccount,
}
func normalizeEndpointType(t EndpointType) EndpointType {
if canonical, ok := endpointTypeAliases[t]; ok {
return canonical
}
return t
}
func cloneStringMap(src map[string]string) map[string]string {
if len(src) == 0 {
return nil

View File

@@ -4,43 +4,54 @@ import (
"github.com/tech/sendico/pkg/merrors"
)
type QuotePayment struct {
type PaymentBase struct {
IdempotencyKey string `json:"idempotencyKey"`
Intent PaymentIntent `json:"intent"`
PreviewOnly bool `json:"previewOnly"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type InitiatePayment struct {
IdempotencyKey string `json:"idempotencyKey"`
Metadata map[string]string `json:"metadata,omitempty"`
Intent *PaymentIntent `json:"intent,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
}
func (r QuotePayment) Validate() error {
if r.IdempotencyKey == "" {
func (b *PaymentBase) Validate() error {
if b.IdempotencyKey == "" {
return merrors.InvalidArgument("idempotencyKey is required", "idempotencyKey")
}
return nil
}
if validator, ok := any(r.Intent).(interface{ Validate() error }); ok {
if err := validator.Validate(); err != nil {
return err
}
type QuotePayment struct {
PaymentBase `json:",inline"`
Intent PaymentIntent `json:"intent"`
PreviewOnly bool `json:"previewOnly"`
}
func (r *QuotePayment) Validate() error {
// base checks
if err := r.PaymentBase.Validate(); err != nil {
return err
}
// intent is mandatory, so validate always
if err := r.Intent.Validate(); err != nil {
return err
}
return nil
}
// Validate проверяет базовые инварианты запроса на инициацию платежа.
type InitiatePayment struct {
PaymentBase `json:",inline"`
Intent *PaymentIntent `json:"intent,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
}
func (r InitiatePayment) Validate() error {
if r.IdempotencyKey == "" {
return merrors.InvalidArgument("idempotencyKey is required", "idempotencyKey")
// base checks
if err := r.PaymentBase.Validate(); err != nil {
return err
}
hasIntent := r.Intent != nil
hasQuote := r.QuoteRef != ""
// must be exactly one
switch {
case !hasIntent && !hasQuote:
return merrors.NoData("either intent or quoteRef must be provided")
@@ -48,11 +59,10 @@ func (r InitiatePayment) Validate() error {
return merrors.DataConflict("intent and quoteRef are mutually exclusive")
}
// if intent provided → validate it
if hasIntent {
if validator, ok := any(*r.Intent).(interface{ Validate() error }); ok {
if err := validator.Validate(); err != nil {
return err
}
if err := r.Intent.Validate(); err != nil {
return err
}
}

View File

@@ -1,12 +1,47 @@
package srequest
import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
)
type PaymentIntent struct {
Kind PaymentKind `json:"kind,omitempty"`
Source *Endpoint `json:"source,omitempty"`
Destination *Endpoint `json:"destination,omitempty"`
Amount *Money `json:"amount,omitempty"`
RequiresFX bool `json:"requires_fx,omitempty"`
Amount *model.Money `json:"amount,omitempty"`
FX *FXIntent `json:"fx,omitempty"`
SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
}
func (p *PaymentIntent) Validate() error {
// Kind must be set (non-zero)
var zeroKind PaymentKind
if p.Kind == zeroKind {
return merrors.InvalidArgument("kind is required", "intent.kind")
}
if p.Source == nil {
return merrors.InvalidArgument("source is required", "intent.source")
}
if p.Destination == nil {
return merrors.InvalidArgument("destination is required", "intent.destination")
}
if p.Amount == nil {
return merrors.InvalidArgument("amount is required", "intent.amount")
}
if err := ValidateMoney(p.Amount); err != nil {
return err
}
if p.FX != nil {
if err := p.FX.Validate(); err != nil {
return err
}
}
return nil
}

View File

@@ -5,6 +5,8 @@ import (
"reflect"
"strings"
"testing"
"github.com/tech/sendico/pkg/model"
)
func TestEndpointDTOBuildersAndDecoders(t *testing.T) {
@@ -86,7 +88,14 @@ func TestEndpointDTOBuildersAndDecoders(t *testing.T) {
})
t.Run("card", func(t *testing.T) {
payload := CardEndpoint{Pan: "pan", Token: "token", Cardholder: "Jane", ExpMonth: 12, ExpYear: 2030, Country: "US", MaskedPan: "****"}
payload := CardEndpoint{
Pan: "pan",
FirstName: "Jane",
LastName: "Doe",
ExpMonth: 12,
ExpYear: 2030,
Country: "US",
}
endpoint, err := NewCardEndpointDTO(payload, map[string]string{"k": "v"})
if err != nil {
t.Fatalf("build card endpoint: %v", err)
@@ -106,6 +115,94 @@ func TestEndpointDTOBuildersAndDecoders(t *testing.T) {
}
})
t.Run("card token", func(t *testing.T) {
payload := CardTokenEndpoint{Token: "token", MaskedPan: "****1234"}
endpoint, err := NewCardTokenEndpointDTO(payload, nil)
if err != nil {
t.Fatalf("build card token endpoint: %v", err)
}
if endpoint.Type != EndpointTypeCardToken {
t.Fatalf("expected type %s got %s", EndpointTypeCardToken, endpoint.Type)
}
decoded, err := endpoint.DecodeCardToken()
if err != nil {
t.Fatalf("decode card token: %v", err)
}
if !reflect.DeepEqual(decoded, payload) {
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
}
})
t.Run("wallet", func(t *testing.T) {
payload := WalletEndpoint{WalletID: "wallet-1"}
endpoint, err := NewWalletEndpointDTO(payload, nil)
if err != nil {
t.Fatalf("build wallet endpoint: %v", err)
}
if endpoint.Type != EndpointTypeWallet {
t.Fatalf("expected type %s got %s", EndpointTypeWallet, endpoint.Type)
}
decoded, err := endpoint.DecodeWallet()
if err != nil {
t.Fatalf("decode wallet: %v", err)
}
if decoded != payload {
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
}
})
t.Run("bank account", func(t *testing.T) {
payload := BankAccountEndpoint{
RecipientName: "ACME",
Inn: "inn",
Kpp: "kpp",
BankName: "bank",
Bik: "bik",
AccountNumber: "123",
CorrespondentAccount: "456",
}
endpoint, err := NewBankAccountEndpointDTO(payload, map[string]string{"note": "n"})
if err != nil {
t.Fatalf("build bank account endpoint: %v", err)
}
if endpoint.Type != EndpointTypeBankAccount {
t.Fatalf("expected type %s got %s", EndpointTypeBankAccount, endpoint.Type)
}
decoded, err := endpoint.DecodeBankAccount()
if err != nil {
t.Fatalf("decode bank account: %v", err)
}
if !reflect.DeepEqual(decoded, payload) {
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
}
if endpoint.Metadata["note"] != "n" {
t.Fatalf("expected metadata copy, got %s", endpoint.Metadata["note"])
}
})
t.Run("iban", func(t *testing.T) {
payload := IBANEndpoint{
IBAN: "DE123",
AccountHolder: "John Doe",
BIC: "BICCODE",
BankName: "BankName",
}
endpoint, err := NewIBANEndpointDTO(payload, nil)
if err != nil {
t.Fatalf("build iban endpoint: %v", err)
}
if endpoint.Type != EndpointTypeIBAN {
t.Fatalf("expected type %s got %s", EndpointTypeIBAN, endpoint.Type)
}
decoded, err := endpoint.DecodeIBAN()
if err != nil {
t.Fatalf("decode iban: %v", err)
}
if !reflect.DeepEqual(decoded, payload) {
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
}
})
t.Run("type mismatch", func(t *testing.T) {
endpoint, err := NewLedgerEndpointDTO(LedgerEndpoint{LedgerAccountRef: "acc"}, nil)
if err != nil {
@@ -122,6 +219,24 @@ func TestEndpointDTOBuildersAndDecoders(t *testing.T) {
t.Fatalf("expected decode error")
}
})
t.Run("legacy type alias normalizes", func(t *testing.T) {
raw := []byte(`{"type":"managed_wallet","data":{"managed_wallet_ref":"mw-legacy"}}`)
var endpoint Endpoint
if err := json.Unmarshal(raw, &endpoint); err != nil {
t.Fatalf("unmarshal with legacy type: %v", err)
}
if endpoint.Type != EndpointTypeManagedWallet {
t.Fatalf("expected normalized type %s got %s", EndpointTypeManagedWallet, endpoint.Type)
}
payload, err := endpoint.DecodeManagedWallet()
if err != nil {
t.Fatalf("decode managed wallet with alias: %v", err)
}
if payload.ManagedWalletRef != "mw-legacy" {
t.Fatalf("decoded payload mismatch from alias: %#v", payload)
}
})
}
func TestPaymentIntentJSONRoundTrip(t *testing.T) {
@@ -140,8 +255,7 @@ func TestPaymentIntentJSONRoundTrip(t *testing.T) {
Kind: PaymentKindPayout,
Source: &source,
Destination: &dest,
Amount: &Money{Amount: "10", Currency: "USD"},
RequiresFX: true,
Amount: &model.Money{Amount: "10", Currency: "USD"},
FX: &FXIntent{
Pair: &CurrencyPair{Base: "USD", Quote: "EUR"},
Side: FXSideBuyBaseSellQuote,
@@ -163,7 +277,7 @@ func TestPaymentIntentJSONRoundTrip(t *testing.T) {
t.Fatalf("unmarshal intent: %v", err)
}
if decoded.Kind != intent.Kind || decoded.RequiresFX != intent.RequiresFX || decoded.SettlementMode != intent.SettlementMode {
if decoded.Kind != intent.Kind || decoded.SettlementMode != intent.SettlementMode {
t.Fatalf("scalar fields changed after round trip")
}
if decoded.Amount == nil || *decoded.Amount != *intent.Amount {
@@ -210,7 +324,7 @@ func TestPaymentIntentMinimalRoundTrip(t *testing.T) {
Kind: PaymentKindInternalTransfer,
Source: &source,
Destination: &dest,
Amount: &Money{Amount: "1", Currency: "USD"},
Amount: &model.Money{Amount: "1", Currency: "USD"},
}
data, err := json.Marshal(intent)
@@ -222,7 +336,7 @@ func TestPaymentIntentMinimalRoundTrip(t *testing.T) {
t.Fatalf("unmarshal intent: %v", err)
}
if decoded.Kind != intent.Kind || decoded.RequiresFX || decoded.FX != nil {
if decoded.Kind != intent.Kind || decoded.FX != nil {
t.Fatalf("unexpected fx data in minimal intent: %#v", decoded)
}
if decoded.Amount == nil || *decoded.Amount != *intent.Amount {
@@ -287,7 +401,7 @@ func TestLegacyEndpointRoundTrip(t *testing.T) {
func TestLegacyEndpointConversionRejectsMultiple(t *testing.T) {
_, err := LegacyPaymentEndpointToEndpointDTO(&LegacyPaymentEndpoint{
Ledger: &LedgerEndpoint{LedgerAccountRef: "a"},
Card: &CardEndpoint{Token: "t"},
Card: &CardEndpoint{Pan: "t"},
})
if err == nil {
t.Fatalf("expected error when multiple legacy endpoints are set")

View File

@@ -1,8 +1,33 @@
package srequest
type Money struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
import (
"github.com/shopspring/decimal"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
)
func ValidateMoney(m *model.Money) error {
if m.Amount == "" {
return merrors.InvalidArgument("amount is required", "intent.amount")
}
if m.Currency == "" {
return merrors.InvalidArgument("currency is required", "intent.currency")
}
if _, err := decimal.NewFromString(m.Amount); err != nil {
return merrors.InvalidArgument("invalid amount decimal", "intent.amount")
}
if len(m.Currency) != 3 {
return merrors.InvalidArgument("currency must be 3 letters", "intent.currency")
}
for _, c := range m.Currency {
if c < 'A' || c > 'Z' {
return merrors.InvalidArgument("currency must be uppercase A-Z", "intent.currency")
}
}
return nil
}
type CurrencyPair struct {
@@ -10,6 +35,35 @@ type CurrencyPair struct {
Quote string `json:"quote"`
}
func (p *CurrencyPair) Validate() error {
if p.Base == "" {
return merrors.InvalidArgument("base currency is required", "intent.fx.pair.base")
}
if p.Quote == "" {
return merrors.InvalidArgument("quote currency is required", "intent.fx.pair.quote")
}
if len(p.Base) != 3 {
return merrors.InvalidArgument("base currency must be 3 letters", "intent.fx.pair.base")
}
if len(p.Quote) != 3 {
return merrors.InvalidArgument("quote currency must be 3 letters", "intent.fx.pair.quote")
}
for _, c := range p.Base {
if c < 'A' || c > 'Z' {
return merrors.InvalidArgument("base currency must be uppercase A-Z", "intent.fx.pair.base")
}
}
for _, c := range p.Quote {
if c < 'A' || c > 'Z' {
return merrors.InvalidArgument("quote currency must be uppercase A-Z", "intent.fx.pair.quote")
}
}
return nil
}
type FXIntent struct {
Pair *CurrencyPair `json:"pair,omitempty"`
Side FXSide `json:"side,omitempty"`
@@ -18,3 +72,29 @@ type FXIntent struct {
PreferredProvider string `json:"preferred_provider,omitempty"`
MaxAgeMs int32 `json:"max_age_ms,omitempty"`
}
func (fx *FXIntent) Validate() error {
if fx.Pair != nil {
if err := fx.Pair.Validate(); err != nil {
return err
}
}
var zeroSide FXSide
if fx.Side == zeroSide {
return merrors.InvalidArgument("fx side is required", "intent.fx.side")
}
if fx.TTLms < 0 {
return merrors.InvalidArgument("fx ttl_ms cannot be negative", "intent.fx.ttl_ms")
}
if fx.TTLms == 0 && fx.Firm {
return merrors.InvalidArgument("firm quote requires positive ttl_ms", "intent.fx.ttl_ms")
}
if fx.MaxAgeMs < 0 {
return merrors.InvalidArgument("fx max_age_ms cannot be negative", "intent.fx.max_age_ms")
}
return nil
}

View File

@@ -0,0 +1,5 @@
package srequest
type Validatable interface {
Validate() error
}

View File

@@ -1,17 +1,15 @@
package sresponse
import moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
import (
"github.com/tech/sendico/pkg/model"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)
type Money struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
}
func toMoney(m *moneyv1.Money) *Money {
func toMoney(m *moneyv1.Money) *model.Money {
if m == nil {
return nil
}
return &Money{
return &model.Money{
Amount: m.GetAmount(),
Currency: m.GetCurrency(),
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
@@ -13,40 +14,40 @@ import (
type FeeLine struct {
LedgerAccountRef string `json:"ledgerAccountRef,omitempty"`
Amount *Money `json:"amount,omitempty"`
Amount *model.Money `json:"amount,omitempty"`
LineType string `json:"lineType,omitempty"`
Side string `json:"side,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
type NetworkFee struct {
NetworkFee *Money `json:"networkFee,omitempty"`
EstimationContext string `json:"estimationContext,omitempty"`
NetworkFee *model.Money `json:"networkFee,omitempty"`
EstimationContext string `json:"estimationContext,omitempty"`
}
type FxQuote struct {
QuoteRef string `json:"quoteRef,omitempty"`
BaseCurrency string `json:"baseCurrency,omitempty"`
QuoteCurrency string `json:"quoteCurrency,omitempty"`
Side string `json:"side,omitempty"`
Price string `json:"price,omitempty"`
BaseAmount *Money `json:"baseAmount,omitempty"`
QuoteAmount *Money `json:"quoteAmount,omitempty"`
ExpiresAtUnixMs int64 `json:"expiresAtUnixMs,omitempty"`
Provider string `json:"provider,omitempty"`
RateRef string `json:"rateRef,omitempty"`
Firm bool `json:"firm,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
BaseCurrency string `json:"baseCurrency,omitempty"`
QuoteCurrency string `json:"quoteCurrency,omitempty"`
Side string `json:"side,omitempty"`
Price string `json:"price,omitempty"`
BaseAmount *model.Money `json:"baseAmount,omitempty"`
QuoteAmount *model.Money `json:"quoteAmount,omitempty"`
ExpiresAtUnixMs int64 `json:"expiresAtUnixMs,omitempty"`
Provider string `json:"provider,omitempty"`
RateRef string `json:"rateRef,omitempty"`
Firm bool `json:"firm,omitempty"`
}
type PaymentQuote struct {
QuoteRef string `json:"quoteRef,omitempty"`
DebitAmount *Money `json:"debitAmount,omitempty"`
ExpectedSettlementAmount *Money `json:"expectedSettlementAmount,omitempty"`
ExpectedFeeTotal *Money `json:"expectedFeeTotal,omitempty"`
FeeQuoteToken string `json:"feeQuoteToken,omitempty"`
FeeLines []FeeLine `json:"feeLines,omitempty"`
NetworkFee *NetworkFee `json:"networkFee,omitempty"`
FxQuote *FxQuote `json:"fxQuote,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
DebitAmount *model.Money `json:"debitAmount,omitempty"`
ExpectedSettlementAmount *model.Money `json:"expectedSettlementAmount,omitempty"`
ExpectedFeeTotal *model.Money `json:"expectedFeeTotal,omitempty"`
FeeQuoteToken string `json:"feeQuoteToken,omitempty"`
FeeLines []FeeLine `json:"feeLines,omitempty"`
NetworkFee *NetworkFee `json:"networkFee,omitempty"`
FxQuote *FxQuote `json:"fxQuote,omitempty"`
}
type Payment struct {

View File

@@ -6,6 +6,7 @@ import (
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -36,10 +37,10 @@ type walletsResponse struct {
}
type walletBalance struct {
Available *Money `json:"available,omitempty"`
PendingInbound *Money `json:"pendingInbound,omitempty"`
PendingOutbound *Money `json:"pendingOutbound,omitempty"`
CalculatedAt string `json:"calculatedAt,omitempty"`
Available *model.Money `json:"available,omitempty"`
PendingInbound *model.Money `json:"pendingInbound,omitempty"`
PendingOutbound *model.Money `json:"pendingOutbound,omitempty"`
CalculatedAt string `json:"calculatedAt,omitempty"`
}
type walletBalanceResponse struct {