Merge pull request '+ quotation provider' (#60) from quote-front-59 into main
Some checks failed
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
Some checks failed
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #60
This commit was merged in pull request #60.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
// Quote represents a firm or indicative quote persisted by the oracle.
|
||||
@@ -16,8 +17,8 @@ type Quote struct {
|
||||
Pair CurrencyPair `bson:"pair" json:"pair"`
|
||||
Side QuoteSide `bson:"side" json:"side"`
|
||||
Price string `bson:"price" json:"price"`
|
||||
BaseAmount Money `bson:"baseAmount" json:"baseAmount"`
|
||||
QuoteAmount Money `bson:"quoteAmount" json:"quoteAmount"`
|
||||
BaseAmount model.Money `bson:"baseAmount" json:"baseAmount"`
|
||||
QuoteAmount model.Money `bson:"quoteAmount" json:"quoteAmount"`
|
||||
AmountType QuoteAmountType `bson:"amountType" json:"amountType"`
|
||||
ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"`
|
||||
ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"`
|
||||
|
||||
@@ -51,12 +51,6 @@ type CurrencyPair struct {
|
||||
Quote string `bson:"quote" json:"quote"`
|
||||
}
|
||||
|
||||
// Money represents an exact decimal amount with its currency.
|
||||
type Money struct {
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
Amount string `bson:"amount" json:"amount"`
|
||||
}
|
||||
|
||||
// QuoteMeta carries request-scoped metadata associated with a quote.
|
||||
type QuoteMeta struct {
|
||||
model.OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||
|
||||
@@ -62,12 +62,6 @@ const (
|
||||
OutboxStatusFailed OutboxStatus = "failed"
|
||||
)
|
||||
|
||||
// Money represents an exact decimal amount with its currency.
|
||||
type Money struct {
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
Amount string `bson:"amount" json:"amount"` // stored as string for exact decimal representation
|
||||
}
|
||||
|
||||
// LedgerMeta carries organization-scoped metadata for ledger entities.
|
||||
type LedgerMeta struct {
|
||||
model.OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||
|
||||
6
api/pkg/model/money.go
Normal file
6
api/pkg/model/money.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Money struct {
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
Amount string `bson:"amount" json:"amount"`
|
||||
}
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
PaymentTypeBankAccount
|
||||
PaymentTypeWallet
|
||||
PaymentTypeCryptoAddress
|
||||
PaymentTypeLedger
|
||||
)
|
||||
|
||||
var paymentTypeToString = map[PaymentType]string{
|
||||
@@ -28,6 +29,7 @@ var paymentTypeToString = map[PaymentType]string{
|
||||
PaymentTypeBankAccount: "bankAccount",
|
||||
PaymentTypeWallet: "wallet",
|
||||
PaymentTypeCryptoAddress: "cryptoAddress",
|
||||
PaymentTypeLedger: "ledger",
|
||||
}
|
||||
|
||||
var paymentTypeFromString = map[string]PaymentType{
|
||||
@@ -37,6 +39,7 @@ var paymentTypeFromString = map[string]PaymentType{
|
||||
"bankAccount": PaymentTypeBankAccount,
|
||||
"wallet": PaymentTypeWallet,
|
||||
"cryptoAddress": PaymentTypeCryptoAddress,
|
||||
"ledger": PaymentTypeLedger,
|
||||
}
|
||||
|
||||
func (t PaymentType) String() string {
|
||||
|
||||
@@ -76,10 +76,11 @@ message CardEndpoint {
|
||||
string token = 2; // network or gateway-issued token
|
||||
}
|
||||
string cardholder_name = 3;
|
||||
uint32 exp_month = 4;
|
||||
uint32 exp_year = 5;
|
||||
string country = 6;
|
||||
string masked_pan = 7;
|
||||
string cardholder_surname = 4;
|
||||
uint32 exp_month = 5;
|
||||
uint32 exp_year = 6;
|
||||
string country = 7;
|
||||
string masked_pan = 8;
|
||||
}
|
||||
|
||||
message PaymentEndpoint {
|
||||
|
||||
@@ -21,6 +21,7 @@ require (
|
||||
github.com/go-chi/metrics v0.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tech/sendico/gateway/chain v0.0.0-00010101000000-000000000000
|
||||
github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -213,6 +213,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
5
api/server/interface/api/srequest/validateable.go
Normal file
5
api/server/interface/api/srequest/validateable.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
type Validatable interface {
|
||||
Validate() error
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
@@ -45,7 +46,7 @@ func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIn
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
Amount: mapMoney(intent.Amount),
|
||||
RequiresFx: intent.RequiresFX,
|
||||
RequiresFx: fx != nil,
|
||||
Fx: fx,
|
||||
SettlementMode: settlementMode,
|
||||
Attributes: copyStringMap(intent.Attributes),
|
||||
@@ -99,6 +100,14 @@ func mapPaymentEndpoint(endpoint *srequest.Endpoint, field string) (*orchestrato
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_Card{
|
||||
Card: mapCardEndpoint(&payload),
|
||||
}
|
||||
case srequest.EndpointTypeCardToken:
|
||||
payload, err := endpoint.DecodeCardToken()
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
}
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_Card{
|
||||
Card: mapCardTokenEndpoint(&payload),
|
||||
}
|
||||
case "":
|
||||
return nil, merrors.InvalidArgument(field + " endpoint type is required")
|
||||
default:
|
||||
@@ -163,7 +172,7 @@ func mapAsset(asset *srequest.Asset) (*chainv1.Asset, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapMoney(m *srequest.Money) *moneyv1.Money {
|
||||
func mapMoney(m *model.Money) *moneyv1.Money {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -206,21 +215,28 @@ func mapCardEndpoint(card *srequest.CardEndpoint) *orchestratorv1.CardEndpoint {
|
||||
return nil
|
||||
}
|
||||
result := &orchestratorv1.CardEndpoint{
|
||||
CardholderName: strings.TrimSpace(card.Cardholder),
|
||||
ExpMonth: card.ExpMonth,
|
||||
ExpYear: card.ExpYear,
|
||||
Country: strings.TrimSpace(card.Country),
|
||||
MaskedPan: strings.TrimSpace(card.MaskedPan),
|
||||
CardholderName: strings.TrimSpace(card.FirstName),
|
||||
CardholderSurname: strings.TrimSpace(card.LastName),
|
||||
ExpMonth: card.ExpMonth,
|
||||
ExpYear: card.ExpYear,
|
||||
Country: strings.TrimSpace(card.Country),
|
||||
}
|
||||
if pan := strings.TrimSpace(card.Pan); pan != "" {
|
||||
result.Card = &orchestratorv1.CardEndpoint_Pan{Pan: pan}
|
||||
}
|
||||
if token := strings.TrimSpace(card.Token); token != "" {
|
||||
result.Card = &orchestratorv1.CardEndpoint_Token{Token: token}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mapCardTokenEndpoint(card *srequest.CardTokenEndpoint) *orchestratorv1.CardEndpoint {
|
||||
if card == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestratorv1.CardEndpoint{
|
||||
Card: &orchestratorv1.CardEndpoint_Token{Token: strings.TrimSpace(card.Token)},
|
||||
MaskedPan: strings.TrimSpace(card.MaskedPan),
|
||||
}
|
||||
}
|
||||
|
||||
func mapPaymentKind(kind srequest.PaymentKind) (orchestratorv1.PaymentKind, error) {
|
||||
switch strings.TrimSpace(string(kind)) {
|
||||
case "", string(srequest.PaymentKindUnspecified):
|
||||
|
||||
@@ -36,11 +36,17 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
||||
|
||||
payload, err := decodeQuotePayload(r)
|
||||
if err != nil {
|
||||
a.logger.Debug("Failed to decode payload", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
if err := payload.Validate(); err != nil {
|
||||
a.logger.Debug("Failed to validate payload", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
intent, err := mapPaymentIntent(&payload.Intent)
|
||||
if err != nil {
|
||||
a.logger.Debug("Failed to map payment intent", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
@@ -66,7 +72,7 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
|
||||
|
||||
payload := &srequest.QuotePayment{}
|
||||
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
|
||||
return nil, merrors.InvalidArgument("invalid payload: "+err.Error(), "payload")
|
||||
}
|
||||
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
|
||||
if err := payload.Validate(); err != nil {
|
||||
|
||||
@@ -35,7 +35,7 @@ type PaymentAPI struct {
|
||||
permissionRef primitive.ObjectID
|
||||
}
|
||||
|
||||
func (a *PaymentAPI) Name() mservice.Type { return mservice.PaymentOrchestrator }
|
||||
func (a *PaymentAPI) Name() mservice.Type { return mservice.Payments }
|
||||
|
||||
func (a *PaymentAPI) Finish(ctx context.Context) error {
|
||||
if a.client != nil {
|
||||
@@ -48,12 +48,12 @@ func (a *PaymentAPI) Finish(ctx context.Context) error {
|
||||
|
||||
func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
|
||||
p := &PaymentAPI{
|
||||
logger: apiCtx.Logger().Named(mservice.PaymentOrchestrator),
|
||||
logger: apiCtx.Logger().Named(mservice.Payments),
|
||||
enf: apiCtx.Permissions().Enforcer(),
|
||||
oph: mutil.CreatePH(mservice.Organizations),
|
||||
}
|
||||
|
||||
desc, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.PaymentOrchestrator)
|
||||
desc, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.Payments)
|
||||
if err != nil {
|
||||
p.logger.Warn("Failed to fetch payment orchestrator permission description", zap.Error(err))
|
||||
return nil, err
|
||||
|
||||
20
frontend/pshared/lib/api/requests/payment/base.dart
Normal file
20
frontend/pshared/lib/api/requests/payment/base.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
|
||||
part 'base.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentBaseRequest {
|
||||
final String idempotencyKey;
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
const PaymentBaseRequest({
|
||||
required this.idempotencyKey,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory PaymentBaseRequest.fromJson(Map<String, dynamic> json) => _$PaymentBaseRequestFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PaymentBaseRequestToJson(this);
|
||||
|
||||
}
|
||||
24
frontend/pshared/lib/api/requests/payment/initiate.dart
Normal file
24
frontend/pshared/lib/api/requests/payment/initiate.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/api/requests/payment/base.dart';
|
||||
import 'package:pshared/data/dto/payment/intent/payment.dart';
|
||||
|
||||
part 'initiate.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class InitiatePaymentRequest extends PaymentBaseRequest {
|
||||
final PaymentIntentDTO? intent;
|
||||
final String? quoteRef;
|
||||
|
||||
const InitiatePaymentRequest({
|
||||
required super.idempotencyKey,
|
||||
super.metadata,
|
||||
this.intent,
|
||||
this.quoteRef,
|
||||
});
|
||||
|
||||
factory InitiatePaymentRequest.fromJson(Map<String, dynamic> json) => _$InitiatePaymentRequestFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$InitiatePaymentRequestToJson(this);
|
||||
}
|
||||
26
frontend/pshared/lib/api/requests/payment/quote.dart
Normal file
26
frontend/pshared/lib/api/requests/payment/quote.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:pshared/api/requests/payment/base.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/intent/payment.dart';
|
||||
|
||||
part 'quote.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class QuotePaymentRequest extends PaymentBaseRequest {
|
||||
final PaymentIntentDTO intent;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool previewOnly;
|
||||
|
||||
const QuotePaymentRequest({
|
||||
required super.idempotencyKey,
|
||||
super.metadata,
|
||||
required this.intent,
|
||||
this.previewOnly = false,
|
||||
});
|
||||
|
||||
factory QuotePaymentRequest.fromJson(Map<String, dynamic> json) => _$QuotePaymentRequestFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$QuotePaymentRequestToJson(this);
|
||||
}
|
||||
20
frontend/pshared/lib/api/responses/payment/quotation.dart
Normal file
20
frontend/pshared/lib/api/responses/payment/quotation.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/api/responses/base.dart';
|
||||
import 'package:pshared/api/responses/token.dart';
|
||||
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
||||
|
||||
part 'quotation.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class PaymentQuoteResponse extends BaseAuthorizedResponse {
|
||||
|
||||
final PaymentQuoteDTO quote;
|
||||
|
||||
const PaymentQuoteResponse({required super.accessToken, required this.quote});
|
||||
|
||||
factory PaymentQuoteResponse.fromJson(Map<String, dynamic> json) => _$PaymentQuoteResponseFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$PaymentQuoteResponseToJson(this);
|
||||
}
|
||||
24
frontend/pshared/lib/data/dto/payment/asset.dart
Normal file
24
frontend/pshared/lib/data/dto/payment/asset.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'asset.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class AssetDTO {
|
||||
final String chain;
|
||||
|
||||
@JsonKey(name: 'token_symbol')
|
||||
final String tokenSymbol;
|
||||
|
||||
@JsonKey(name: 'contract_address')
|
||||
final String? contractAddress;
|
||||
|
||||
const AssetDTO({
|
||||
required this.chain,
|
||||
required this.tokenSymbol,
|
||||
this.contractAddress,
|
||||
});
|
||||
|
||||
factory AssetDTO.fromJson(Map<String, dynamic> json) => _$AssetDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$AssetDTOToJson(this);
|
||||
}
|
||||
@@ -4,17 +4,28 @@ part 'card.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class CardPaymentDataDTO {
|
||||
class CardEndpointDTO {
|
||||
final String pan;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
|
||||
const CardPaymentDataDTO({
|
||||
@JsonKey(name: 'exp_month')
|
||||
final int? expMonth;
|
||||
|
||||
@JsonKey(name: 'exp_year')
|
||||
final int? expYear;
|
||||
|
||||
final String? country;
|
||||
|
||||
const CardEndpointDTO({
|
||||
required this.pan,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.expMonth,
|
||||
required this.expYear,
|
||||
this.country,
|
||||
});
|
||||
|
||||
factory CardPaymentDataDTO.fromJson(Map<String, dynamic> json) => _$CardPaymentDataDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CardPaymentDataDTOToJson(this);
|
||||
factory CardEndpointDTO.fromJson(Map<String, dynamic> json) => _$CardEndpointDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CardEndpointDTOToJson(this);
|
||||
}
|
||||
|
||||
20
frontend/pshared/lib/data/dto/payment/card_token.dart
Normal file
20
frontend/pshared/lib/data/dto/payment/card_token.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'card_token.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class CardTokenEndpointDTO {
|
||||
final String token;
|
||||
|
||||
@JsonKey(name: 'masked_pan')
|
||||
final String maskedPan;
|
||||
|
||||
const CardTokenEndpointDTO({
|
||||
required this.maskedPan,
|
||||
required this.token,
|
||||
});
|
||||
|
||||
factory CardTokenEndpointDTO.fromJson(Map<String, dynamic> json) => _$CardTokenEndpointDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CardTokenEndpointDTOToJson(this);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'crypto_address.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class CryptoAddressPaymentDataDTO {
|
||||
final String address;
|
||||
final String network;
|
||||
final String? destinationTag;
|
||||
|
||||
const CryptoAddressPaymentDataDTO({
|
||||
required this.address,
|
||||
required this.network,
|
||||
this.destinationTag,
|
||||
});
|
||||
|
||||
factory CryptoAddressPaymentDataDTO.fromJson(Map<String, dynamic> json) => _$CryptoAddressPaymentDataDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CryptoAddressPaymentDataDTOToJson(this);
|
||||
}
|
||||
18
frontend/pshared/lib/data/dto/payment/currency_pair.dart
Normal file
18
frontend/pshared/lib/data/dto/payment/currency_pair.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'currency_pair.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class CurrencyPairDTO {
|
||||
final String base;
|
||||
final String quote;
|
||||
|
||||
const CurrencyPairDTO({
|
||||
required this.base,
|
||||
required this.quote,
|
||||
});
|
||||
|
||||
factory CurrencyPairDTO.fromJson(Map<String, dynamic> json) => _$CurrencyPairDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CurrencyPairDTOToJson(this);
|
||||
}
|
||||
20
frontend/pshared/lib/data/dto/payment/endpoint.dart
Normal file
20
frontend/pshared/lib/data/dto/payment/endpoint.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'endpoint.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentEndpointDTO {
|
||||
final String type;
|
||||
final Map<String, dynamic> data;
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
const PaymentEndpointDTO({
|
||||
required this.type,
|
||||
required this.data,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory PaymentEndpointDTO.fromJson(Map<String, dynamic> json) => _$PaymentEndpointDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PaymentEndpointDTOToJson(this);
|
||||
}
|
||||
22
frontend/pshared/lib/data/dto/payment/external_chain.dart
Normal file
22
frontend/pshared/lib/data/dto/payment/external_chain.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/asset.dart';
|
||||
|
||||
part 'external_chain.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class ExternalChainEndpointDTO {
|
||||
final AssetDTO? asset;
|
||||
final String address;
|
||||
final String? memo;
|
||||
|
||||
const ExternalChainEndpointDTO({
|
||||
this.asset,
|
||||
required this.address,
|
||||
this.memo,
|
||||
});
|
||||
|
||||
factory ExternalChainEndpointDTO.fromJson(Map<String, dynamic> json) => _$ExternalChainEndpointDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ExternalChainEndpointDTOToJson(this);
|
||||
}
|
||||
26
frontend/pshared/lib/data/dto/payment/fee_line.dart
Normal file
26
frontend/pshared/lib/data/dto/payment/fee_line.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/money.dart';
|
||||
|
||||
part 'fee_line.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class FeeLineDTO {
|
||||
final String? ledgerAccountRef;
|
||||
final MoneyDTO? amount;
|
||||
final String? lineType;
|
||||
final String? side;
|
||||
final Map<String, String>? meta;
|
||||
|
||||
const FeeLineDTO({
|
||||
this.ledgerAccountRef,
|
||||
this.amount,
|
||||
this.lineType,
|
||||
this.side,
|
||||
this.meta,
|
||||
});
|
||||
|
||||
factory FeeLineDTO.fromJson(Map<String, dynamic> json) => _$FeeLineDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$FeeLineDTOToJson(this);
|
||||
}
|
||||
40
frontend/pshared/lib/data/dto/payment/fx_quote.dart
Normal file
40
frontend/pshared/lib/data/dto/payment/fx_quote.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/money.dart';
|
||||
|
||||
part 'fx_quote.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class FxQuoteDTO {
|
||||
final String? quoteRef;
|
||||
final String? baseCurrency;
|
||||
final String? quoteCurrency;
|
||||
final String? side;
|
||||
final String? price;
|
||||
final MoneyDTO? baseAmount;
|
||||
final MoneyDTO? quoteAmount;
|
||||
final int? expiresAtUnixMs;
|
||||
final String? provider;
|
||||
final String? rateRef;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool? firm;
|
||||
|
||||
const FxQuoteDTO({
|
||||
this.quoteRef,
|
||||
this.baseCurrency,
|
||||
this.quoteCurrency,
|
||||
this.side,
|
||||
this.price,
|
||||
this.baseAmount,
|
||||
this.quoteAmount,
|
||||
this.expiresAtUnixMs,
|
||||
this.provider,
|
||||
this.rateRef,
|
||||
this.firm = false,
|
||||
});
|
||||
|
||||
factory FxQuoteDTO.fromJson(Map<String, dynamic> json) => _$FxQuoteDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$FxQuoteDTOToJson(this);
|
||||
}
|
||||
34
frontend/pshared/lib/data/dto/payment/intent/fx.dart
Normal file
34
frontend/pshared/lib/data/dto/payment/intent/fx.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/currency_pair.dart';
|
||||
|
||||
part 'fx.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class FxIntentDTO {
|
||||
final CurrencyPairDTO? pair;
|
||||
final String? side;
|
||||
final bool firm;
|
||||
|
||||
@JsonKey(name: 'ttl_ms')
|
||||
final int? ttlMs;
|
||||
|
||||
@JsonKey(name: 'preferred_provider')
|
||||
final String? preferredProvider;
|
||||
|
||||
@JsonKey(name: 'max_age_ms')
|
||||
final int? maxAgeMs;
|
||||
|
||||
const FxIntentDTO({
|
||||
this.pair,
|
||||
this.side,
|
||||
this.firm = false,
|
||||
this.ttlMs,
|
||||
this.preferredProvider,
|
||||
this.maxAgeMs,
|
||||
});
|
||||
|
||||
factory FxIntentDTO.fromJson(Map<String, dynamic> json) => _$FxIntentDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$FxIntentDTOToJson(this);
|
||||
}
|
||||
36
frontend/pshared/lib/data/dto/payment/intent/payment.dart
Normal file
36
frontend/pshared/lib/data/dto/payment/intent/payment.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/endpoint.dart';
|
||||
import 'package:pshared/data/dto/payment/intent/fx.dart';
|
||||
import 'package:pshared/data/dto/payment/money.dart';
|
||||
|
||||
part 'payment.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentIntentDTO {
|
||||
final String? kind;
|
||||
final PaymentEndpointDTO? source;
|
||||
final PaymentEndpointDTO? destination;
|
||||
final MoneyDTO? amount;
|
||||
|
||||
final FxIntentDTO? fx;
|
||||
|
||||
@JsonKey(name: 'settlement_mode')
|
||||
final String? settlementMode;
|
||||
|
||||
final Map<String, String>? attributes;
|
||||
|
||||
const PaymentIntentDTO({
|
||||
this.kind,
|
||||
this.source,
|
||||
this.destination,
|
||||
this.amount,
|
||||
this.fx,
|
||||
this.settlementMode,
|
||||
this.attributes,
|
||||
});
|
||||
|
||||
factory PaymentIntentDTO.fromJson(Map<String, dynamic> json) => _$PaymentIntentDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PaymentIntentDTOToJson(this);
|
||||
}
|
||||
21
frontend/pshared/lib/data/dto/payment/ledger.dart
Normal file
21
frontend/pshared/lib/data/dto/payment/ledger.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'ledger.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class LedgerEndpointDTO {
|
||||
@JsonKey(name: 'ledger_account_ref')
|
||||
final String ledgerAccountRef;
|
||||
|
||||
@JsonKey(name: 'contra_ledger_account_ref')
|
||||
final String? contraLedgerAccountRef;
|
||||
|
||||
const LedgerEndpointDTO({
|
||||
required this.ledgerAccountRef,
|
||||
this.contraLedgerAccountRef,
|
||||
});
|
||||
|
||||
factory LedgerEndpointDTO.fromJson(Map<String, dynamic> json) => _$LedgerEndpointDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LedgerEndpointDTOToJson(this);
|
||||
}
|
||||
22
frontend/pshared/lib/data/dto/payment/managed_wallet.dart
Normal file
22
frontend/pshared/lib/data/dto/payment/managed_wallet.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/asset.dart';
|
||||
|
||||
part 'managed_wallet.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class ManagedWalletEndpointDTO {
|
||||
@JsonKey(name: 'managed_wallet_ref')
|
||||
final String managedWalletRef;
|
||||
|
||||
final AssetDTO? asset;
|
||||
|
||||
const ManagedWalletEndpointDTO({
|
||||
required this.managedWalletRef,
|
||||
this.asset,
|
||||
});
|
||||
|
||||
factory ManagedWalletEndpointDTO.fromJson(Map<String, dynamic> json) => _$ManagedWalletEndpointDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ManagedWalletEndpointDTOToJson(this);
|
||||
}
|
||||
18
frontend/pshared/lib/data/dto/payment/money.dart
Normal file
18
frontend/pshared/lib/data/dto/payment/money.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'money.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class MoneyDTO {
|
||||
final String amount;
|
||||
final String currency;
|
||||
|
||||
const MoneyDTO({
|
||||
required this.amount,
|
||||
required this.currency,
|
||||
});
|
||||
|
||||
factory MoneyDTO.fromJson(Map<String, dynamic> json) => _$MoneyDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$MoneyDTOToJson(this);
|
||||
}
|
||||
20
frontend/pshared/lib/data/dto/payment/network_fee.dart
Normal file
20
frontend/pshared/lib/data/dto/payment/network_fee.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/money.dart';
|
||||
|
||||
part 'network_fee.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class NetworkFeeDTO {
|
||||
final MoneyDTO? networkFee;
|
||||
final String? estimationContext;
|
||||
|
||||
const NetworkFeeDTO({
|
||||
this.networkFee,
|
||||
this.estimationContext,
|
||||
});
|
||||
|
||||
factory NetworkFeeDTO.fromJson(Map<String, dynamic> json) => _$NetworkFeeDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$NetworkFeeDTOToJson(this);
|
||||
}
|
||||
28
frontend/pshared/lib/data/dto/payment/payment.dart
Normal file
28
frontend/pshared/lib/data/dto/payment/payment.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
||||
|
||||
part 'payment.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentDTO {
|
||||
final String? paymentRef;
|
||||
final String? idempotencyKey;
|
||||
final String? state;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
final PaymentQuoteDTO? lastQuote;
|
||||
|
||||
const PaymentDTO({
|
||||
this.paymentRef,
|
||||
this.idempotencyKey,
|
||||
this.state,
|
||||
this.failureCode,
|
||||
this.failureReason,
|
||||
this.lastQuote,
|
||||
});
|
||||
|
||||
factory PaymentDTO.fromJson(Map<String, dynamic> json) => _$PaymentDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PaymentDTOToJson(this);
|
||||
}
|
||||
35
frontend/pshared/lib/data/dto/payment/payment_quote.dart
Normal file
35
frontend/pshared/lib/data/dto/payment/payment_quote.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/fee_line.dart';
|
||||
import 'package:pshared/data/dto/payment/fx_quote.dart';
|
||||
import 'package:pshared/data/dto/payment/money.dart';
|
||||
import 'package:pshared/data/dto/payment/network_fee.dart';
|
||||
|
||||
part 'payment_quote.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentQuoteDTO {
|
||||
final String? quoteRef;
|
||||
final MoneyDTO? debitAmount;
|
||||
final MoneyDTO? expectedSettlementAmount;
|
||||
final MoneyDTO? expectedFeeTotal;
|
||||
final String? feeQuoteToken;
|
||||
final List<FeeLineDTO>? feeLines;
|
||||
final NetworkFeeDTO? networkFee;
|
||||
final FxQuoteDTO? fxQuote;
|
||||
|
||||
const PaymentQuoteDTO({
|
||||
this.quoteRef,
|
||||
this.debitAmount,
|
||||
this.expectedSettlementAmount,
|
||||
this.expectedFeeTotal,
|
||||
this.feeQuoteToken,
|
||||
this.feeLines,
|
||||
this.networkFee,
|
||||
this.fxQuote,
|
||||
});
|
||||
|
||||
factory PaymentQuoteDTO.fromJson(Map<String, dynamic> json) => _$PaymentQuoteDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PaymentQuoteDTOToJson(this);
|
||||
}
|
||||
20
frontend/pshared/lib/data/mapper/payment/asset.dart
Normal file
20
frontend/pshared/lib/data/mapper/payment/asset.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:pshared/data/dto/payment/asset.dart';
|
||||
import 'package:pshared/data/mapper/payment/enums.dart';
|
||||
import 'package:pshared/models/payment/asset.dart';
|
||||
|
||||
|
||||
extension PaymentAssetMapper on PaymentAsset {
|
||||
AssetDTO toDTO() => AssetDTO(
|
||||
chain: chainNetworkToValue(chain),
|
||||
tokenSymbol: tokenSymbol,
|
||||
contractAddress: contractAddress,
|
||||
);
|
||||
}
|
||||
|
||||
extension AssetDTOMapper on AssetDTO {
|
||||
PaymentAsset toDomain() => PaymentAsset(
|
||||
chain: chainNetworkFromValue(chain),
|
||||
tokenSymbol: tokenSymbol,
|
||||
contractAddress: contractAddress,
|
||||
);
|
||||
}
|
||||
@@ -3,17 +3,23 @@ import 'package:pshared/models/payment/methods/card.dart';
|
||||
|
||||
|
||||
extension CardPaymentMethodMapper on CardPaymentMethod {
|
||||
CardPaymentDataDTO toDTO() => CardPaymentDataDTO(
|
||||
CardEndpointDTO toDTO() => CardEndpointDTO(
|
||||
pan: pan,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
expMonth: expMonth,
|
||||
expYear: expYear,
|
||||
country: country,
|
||||
);
|
||||
}
|
||||
|
||||
extension CardPaymentDataDTOMapper on CardPaymentDataDTO {
|
||||
extension CardPaymentDataDTOMapper on CardEndpointDTO {
|
||||
CardPaymentMethod toDomain() => CardPaymentMethod(
|
||||
pan: pan,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
expMonth: expMonth,
|
||||
expYear: expYear,
|
||||
country: country,
|
||||
);
|
||||
}
|
||||
|
||||
17
frontend/pshared/lib/data/mapper/payment/card_token.dart
Normal file
17
frontend/pshared/lib/data/mapper/payment/card_token.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:pshared/data/dto/payment/card_token.dart';
|
||||
import 'package:pshared/models/payment/methods/card_token.dart';
|
||||
|
||||
|
||||
extension CardTokenPaymentMethodMapper on CardTokenPaymentMethod {
|
||||
CardTokenEndpointDTO toDTO() => CardTokenEndpointDTO(
|
||||
token: token,
|
||||
maskedPan: maskedPan,
|
||||
);
|
||||
}
|
||||
|
||||
extension CardTokenPaymentDataDTOMapper on CardTokenEndpointDTO {
|
||||
CardTokenPaymentMethod toDomain() => CardTokenPaymentMethod(
|
||||
token: token,
|
||||
maskedPan: maskedPan,
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import 'package:pshared/data/dto/payment/crypto_address.dart';
|
||||
import 'package:pshared/data/dto/payment/external_chain.dart';
|
||||
import 'package:pshared/data/mapper/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
|
||||
|
||||
extension CryptoAddressPaymentMethodMapper on CryptoAddressPaymentMethod {
|
||||
CryptoAddressPaymentDataDTO toDTO() => CryptoAddressPaymentDataDTO(
|
||||
ExternalChainEndpointDTO toDTO() => ExternalChainEndpointDTO(
|
||||
address: address,
|
||||
network: network,
|
||||
destinationTag: destinationTag,
|
||||
asset: asset?.toDTO(),
|
||||
memo: memo,
|
||||
);
|
||||
}
|
||||
|
||||
extension CryptoAddressPaymentDataDTOMapper on CryptoAddressPaymentDataDTO {
|
||||
extension CryptoAddressPaymentDataDTOMapper on ExternalChainEndpointDTO {
|
||||
CryptoAddressPaymentMethod toDomain() => CryptoAddressPaymentMethod(
|
||||
address: address,
|
||||
network: network,
|
||||
destinationTag: destinationTag,
|
||||
asset: asset?.toDomain(),
|
||||
memo: memo,
|
||||
);
|
||||
}
|
||||
|
||||
16
frontend/pshared/lib/data/mapper/payment/currency_pair.dart
Normal file
16
frontend/pshared/lib/data/mapper/payment/currency_pair.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:pshared/data/dto/payment/currency_pair.dart';
|
||||
import 'package:pshared/models/payment/currency_pair.dart';
|
||||
|
||||
extension CurrencyPairMapper on CurrencyPair {
|
||||
CurrencyPairDTO toDTO() => CurrencyPairDTO(
|
||||
base: base,
|
||||
quote: quote,
|
||||
);
|
||||
}
|
||||
|
||||
extension CurrencyPairDTOMapper on CurrencyPairDTO {
|
||||
CurrencyPair toDomain() => CurrencyPair(
|
||||
base: base,
|
||||
quote: quote,
|
||||
);
|
||||
}
|
||||
183
frontend/pshared/lib/data/mapper/payment/enums.dart
Normal file
183
frontend/pshared/lib/data/mapper/payment/enums.dart
Normal file
@@ -0,0 +1,183 @@
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/models/payment/fx/side.dart';
|
||||
import 'package:pshared/models/payment/insufficient_net_policy.dart';
|
||||
import 'package:pshared/models/payment/kind.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||
|
||||
|
||||
PaymentKind paymentKindFromValue(String? value) {
|
||||
switch (value) {
|
||||
case 'payout':
|
||||
return PaymentKind.payout;
|
||||
case 'internal_transfer':
|
||||
return PaymentKind.internalTransfer;
|
||||
case 'fx_conversion':
|
||||
return PaymentKind.fxConversion;
|
||||
case 'unspecified':
|
||||
return PaymentKind.unspecified;
|
||||
default:
|
||||
throw ArgumentError('Unknown PaymentKind value: $value');
|
||||
}
|
||||
}
|
||||
|
||||
String paymentKindToValue(PaymentKind kind) {
|
||||
switch (kind) {
|
||||
case PaymentKind.payout:
|
||||
return 'payout';
|
||||
case PaymentKind.internalTransfer:
|
||||
return 'internal_transfer';
|
||||
case PaymentKind.fxConversion:
|
||||
return 'fx_conversion';
|
||||
case PaymentKind.unspecified:
|
||||
return 'unspecified';
|
||||
}
|
||||
}
|
||||
|
||||
SettlementMode settlementModeFromValue(String? value) {
|
||||
switch (value) {
|
||||
case 'fix_source':
|
||||
return SettlementMode.fixSource;
|
||||
case 'fix_received':
|
||||
return SettlementMode.fixReceived;
|
||||
case 'unspecified':
|
||||
return SettlementMode.unspecified;
|
||||
default:
|
||||
throw ArgumentError('Unknown SettlementMode value: $value');
|
||||
}
|
||||
}
|
||||
|
||||
String settlementModeToValue(SettlementMode mode) {
|
||||
switch (mode) {
|
||||
case SettlementMode.fixSource:
|
||||
return 'fix_source';
|
||||
case SettlementMode.fixReceived:
|
||||
return 'fix_received';
|
||||
case SettlementMode.unspecified:
|
||||
return 'unspecified';
|
||||
}
|
||||
}
|
||||
|
||||
FxSide fxSideFromValue(String? value) {
|
||||
switch (value) {
|
||||
case 'buy_base_sell_quote':
|
||||
return FxSide.buyBaseSellQuote;
|
||||
case 'sell_base_buy_quote':
|
||||
return FxSide.sellBaseBuyQuote;
|
||||
case 'unspecified':
|
||||
return FxSide.unspecified;
|
||||
default:
|
||||
throw ArgumentError('Unknown FxSide value: $value');
|
||||
}
|
||||
}
|
||||
|
||||
String fxSideToValue(FxSide side) {
|
||||
switch (side) {
|
||||
case FxSide.buyBaseSellQuote:
|
||||
return 'buy_base_sell_quote';
|
||||
case FxSide.sellBaseBuyQuote:
|
||||
return 'sell_base_buy_quote';
|
||||
case FxSide.unspecified:
|
||||
return 'unspecified';
|
||||
}
|
||||
}
|
||||
|
||||
ChainNetwork chainNetworkFromValue(String? value) {
|
||||
switch (value) {
|
||||
case 'ethereum_mainnet':
|
||||
return ChainNetwork.ethereumMainnet;
|
||||
case 'arbitrum_one':
|
||||
return ChainNetwork.arbitrumOne;
|
||||
case 'other_evm':
|
||||
return ChainNetwork.otherEvm;
|
||||
case 'unspecified':
|
||||
return ChainNetwork.unspecified;
|
||||
default:
|
||||
throw ArgumentError('Unknown ChainNetwork value: $value');
|
||||
}
|
||||
}
|
||||
|
||||
String chainNetworkToValue(ChainNetwork chain) {
|
||||
switch (chain) {
|
||||
case ChainNetwork.ethereumMainnet:
|
||||
return 'ethereum_mainnet';
|
||||
case ChainNetwork.arbitrumOne:
|
||||
return 'arbitrum_one';
|
||||
case ChainNetwork.otherEvm:
|
||||
return 'other_evm';
|
||||
case ChainNetwork.unspecified:
|
||||
return 'unspecified';
|
||||
}
|
||||
}
|
||||
|
||||
InsufficientNetPolicy insufficientNetPolicyFromValue(String? value) {
|
||||
switch (value) {
|
||||
case 'block_posting':
|
||||
return InsufficientNetPolicy.blockPosting;
|
||||
case 'sweep_org_cash':
|
||||
return InsufficientNetPolicy.sweepOrgCash;
|
||||
case 'invoice_later':
|
||||
return InsufficientNetPolicy.invoiceLater;
|
||||
case 'unspecified':
|
||||
return InsufficientNetPolicy.unspecified;
|
||||
default:
|
||||
throw ArgumentError('Unknown InsufficientNetPolicy value: $value');
|
||||
}
|
||||
}
|
||||
|
||||
String insufficientNetPolicyToValue(InsufficientNetPolicy policy) {
|
||||
switch (policy) {
|
||||
case InsufficientNetPolicy.blockPosting:
|
||||
return 'block_posting';
|
||||
case InsufficientNetPolicy.sweepOrgCash:
|
||||
return 'sweep_org_cash';
|
||||
case InsufficientNetPolicy.invoiceLater:
|
||||
return 'invoice_later';
|
||||
case InsufficientNetPolicy.unspecified:
|
||||
return 'unspecified';
|
||||
}
|
||||
}
|
||||
|
||||
PaymentType endpointTypeFromValue(String? value) {
|
||||
switch (value) {
|
||||
case 'managedWallet':
|
||||
return PaymentType.managedWallet;
|
||||
case 'externalChain':
|
||||
return PaymentType.externalChain;
|
||||
case 'card':
|
||||
return PaymentType.card;
|
||||
case 'cardToken':
|
||||
return PaymentType.cardToken;
|
||||
case 'ledger':
|
||||
return PaymentType.ledger;
|
||||
case 'bankAccount':
|
||||
return PaymentType.bankAccount;
|
||||
case 'iban':
|
||||
return PaymentType.iban;
|
||||
case 'wallet':
|
||||
return PaymentType.wallet;
|
||||
default:
|
||||
throw ArgumentError('Unknown PaymentType value: $value');
|
||||
}
|
||||
}
|
||||
|
||||
String endpointTypeToValue(PaymentType type) {
|
||||
switch (type) {
|
||||
case PaymentType.ledger:
|
||||
return 'ledger';
|
||||
case PaymentType.managedWallet:
|
||||
return 'managedWallet';
|
||||
case PaymentType.externalChain:
|
||||
return 'externalChain';
|
||||
case PaymentType.card:
|
||||
return 'card';
|
||||
case PaymentType.cardToken:
|
||||
return 'cardToken';
|
||||
case PaymentType.bankAccount:
|
||||
return 'bankAccount';
|
||||
case PaymentType.iban:
|
||||
return 'iban';
|
||||
case PaymentType.wallet:
|
||||
return 'wallet';
|
||||
}
|
||||
}
|
||||
24
frontend/pshared/lib/data/mapper/payment/fee_line.dart
Normal file
24
frontend/pshared/lib/data/mapper/payment/fee_line.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:pshared/data/dto/payment/fee_line.dart';
|
||||
import 'package:pshared/data/mapper/payment/money.dart';
|
||||
import 'package:pshared/models/payment/fees/line.dart';
|
||||
|
||||
|
||||
extension FeeLineDTOMapper on FeeLineDTO {
|
||||
FeeLine toDomain() => FeeLine(
|
||||
ledgerAccountRef: ledgerAccountRef,
|
||||
amount: amount?.toDomain(),
|
||||
lineType: lineType,
|
||||
side: side,
|
||||
meta: meta,
|
||||
);
|
||||
}
|
||||
|
||||
extension FeeLineMapper on FeeLine {
|
||||
FeeLineDTO toDTO() => FeeLineDTO(
|
||||
ledgerAccountRef: ledgerAccountRef,
|
||||
amount: amount?.toDTO(),
|
||||
lineType: lineType,
|
||||
side: side,
|
||||
meta: meta,
|
||||
);
|
||||
}
|
||||
36
frontend/pshared/lib/data/mapper/payment/fx_quote.dart
Normal file
36
frontend/pshared/lib/data/mapper/payment/fx_quote.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:pshared/data/dto/payment/fx_quote.dart';
|
||||
import 'package:pshared/data/mapper/payment/money.dart';
|
||||
import 'package:pshared/models/payment/fx/quote.dart';
|
||||
|
||||
|
||||
extension FxQuoteDTOMapper on FxQuoteDTO {
|
||||
FxQuote toDomain() => FxQuote(
|
||||
quoteRef: quoteRef,
|
||||
baseCurrency: baseCurrency,
|
||||
quoteCurrency: quoteCurrency,
|
||||
side: side,
|
||||
price: price,
|
||||
baseAmount: baseAmount?.toDomain(),
|
||||
quoteAmount: quoteAmount?.toDomain(),
|
||||
expiresAtUnixMs: expiresAtUnixMs,
|
||||
provider: provider,
|
||||
rateRef: rateRef,
|
||||
firm: firm ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
extension FxQuoteMapper on FxQuote {
|
||||
FxQuoteDTO toDTO() => FxQuoteDTO(
|
||||
quoteRef: quoteRef,
|
||||
baseCurrency: baseCurrency,
|
||||
quoteCurrency: quoteCurrency,
|
||||
side: side,
|
||||
price: price,
|
||||
baseAmount: baseAmount?.toDTO(),
|
||||
quoteAmount: quoteAmount?.toDTO(),
|
||||
expiresAtUnixMs: expiresAtUnixMs,
|
||||
provider: provider,
|
||||
rateRef: rateRef,
|
||||
firm: firm,
|
||||
);
|
||||
}
|
||||
27
frontend/pshared/lib/data/mapper/payment/intent/fx.dart
Normal file
27
frontend/pshared/lib/data/mapper/payment/intent/fx.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:pshared/data/dto/payment/intent/fx.dart';
|
||||
import 'package:pshared/data/mapper/payment/currency_pair.dart';
|
||||
import 'package:pshared/data/mapper/payment/enums.dart';
|
||||
import 'package:pshared/models/payment/fx/intent.dart';
|
||||
|
||||
|
||||
extension FxIntentMapper on FxIntent {
|
||||
FxIntentDTO toDTO() => FxIntentDTO(
|
||||
pair: pair?.toDTO(),
|
||||
side: fxSideToValue(side),
|
||||
firm: firm,
|
||||
ttlMs: ttlMs,
|
||||
preferredProvider: preferredProvider,
|
||||
maxAgeMs: maxAgeMs,
|
||||
);
|
||||
}
|
||||
|
||||
extension FxIntentDTOMapper on FxIntentDTO {
|
||||
FxIntent toDomain() => FxIntent(
|
||||
pair: pair?.toDomain(),
|
||||
side: fxSideFromValue(side),
|
||||
firm: firm,
|
||||
ttlMs: ttlMs,
|
||||
preferredProvider: preferredProvider,
|
||||
maxAgeMs: maxAgeMs,
|
||||
);
|
||||
}
|
||||
30
frontend/pshared/lib/data/mapper/payment/intent/payment.dart
Normal file
30
frontend/pshared/lib/data/mapper/payment/intent/payment.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:pshared/data/dto/payment/intent/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/enums.dart';
|
||||
import 'package:pshared/data/mapper/payment/intent/fx.dart';
|
||||
import 'package:pshared/data/mapper/payment/money.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
|
||||
extension PaymentIntentMapper on PaymentIntent {
|
||||
PaymentIntentDTO toDTO() => PaymentIntentDTO(
|
||||
kind: paymentKindToValue(kind),
|
||||
source: source?.toDTO(),
|
||||
destination: destination?.toDTO(),
|
||||
amount: amount?.toDTO(),
|
||||
fx: fx?.toDTO(),
|
||||
settlementMode: settlementModeToValue(settlementMode),
|
||||
attributes: attributes,
|
||||
);
|
||||
}
|
||||
|
||||
extension PaymentIntentDTOMapper on PaymentIntentDTO {
|
||||
PaymentIntent toDomain() => PaymentIntent(
|
||||
kind: paymentKindFromValue(kind),
|
||||
source: source?.toDomain(),
|
||||
destination: destination?.toDomain(),
|
||||
amount: amount?.toDomain(),
|
||||
fx: fx?.toDomain(),
|
||||
settlementMode: settlementModeFromValue(settlementMode),
|
||||
attributes: attributes,
|
||||
);
|
||||
}
|
||||
17
frontend/pshared/lib/data/mapper/payment/ledger.dart
Normal file
17
frontend/pshared/lib/data/mapper/payment/ledger.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:pshared/data/dto/payment/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
|
||||
|
||||
extension LedgerPaymentMethodMapper on LedgerPaymentMethod {
|
||||
LedgerEndpointDTO toDTO() => LedgerEndpointDTO(
|
||||
ledgerAccountRef: ledgerAccountRef,
|
||||
contraLedgerAccountRef: contraLedgerAccountRef,
|
||||
);
|
||||
}
|
||||
|
||||
extension LedgerPaymentDataDTOMapper on LedgerEndpointDTO {
|
||||
LedgerPaymentMethod toDomain() => LedgerPaymentMethod(
|
||||
ledgerAccountRef: ledgerAccountRef,
|
||||
contraLedgerAccountRef: contraLedgerAccountRef,
|
||||
);
|
||||
}
|
||||
18
frontend/pshared/lib/data/mapper/payment/managed_wallet.dart
Normal file
18
frontend/pshared/lib/data/mapper/payment/managed_wallet.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:pshared/data/dto/payment/managed_wallet.dart';
|
||||
import 'package:pshared/data/mapper/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
|
||||
|
||||
extension ManagedWalletPaymentMethodMapper on ManagedWalletPaymentMethod {
|
||||
ManagedWalletEndpointDTO toDTO() => ManagedWalletEndpointDTO(
|
||||
managedWalletRef: managedWalletRef,
|
||||
asset: asset?.toDTO(),
|
||||
);
|
||||
}
|
||||
|
||||
extension ManagedWalletDataDTOMapper on ManagedWalletEndpointDTO {
|
||||
ManagedWalletPaymentMethod toDomain() => ManagedWalletPaymentMethod(
|
||||
managedWalletRef: managedWalletRef,
|
||||
asset: asset?.toDomain(),
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,30 @@
|
||||
import 'package:pshared/data/dto/payment/card.dart';
|
||||
import 'package:pshared/data/dto/payment/crypto_address.dart';
|
||||
import 'package:pshared/data/dto/payment/card_token.dart';
|
||||
import 'package:pshared/data/dto/payment/external_chain.dart';
|
||||
import 'package:pshared/data/dto/payment/iban.dart';
|
||||
import 'package:pshared/data/dto/payment/ledger.dart';
|
||||
import 'package:pshared/data/dto/payment/managed_wallet.dart';
|
||||
import 'package:pshared/data/dto/payment/method.dart';
|
||||
import 'package:pshared/data/dto/payment/russian_bank.dart';
|
||||
import 'package:pshared/data/dto/payment/wallet.dart';
|
||||
import 'package:pshared/data/mapper/payment/card_token.dart';
|
||||
import 'package:pshared/data/mapper/payment/crypto_address.dart';
|
||||
import 'package:pshared/data/mapper/payment/card.dart';
|
||||
import 'package:pshared/data/mapper/payment/iban.dart';
|
||||
import 'package:pshared/data/mapper/payment/ledger.dart';
|
||||
import 'package:pshared/data/mapper/payment/managed_wallet.dart';
|
||||
import 'package:pshared/data/mapper/payment/russian_bank.dart';
|
||||
import 'package:pshared/data/mapper/payment/type.dart';
|
||||
import 'package:pshared/data/mapper/payment/wallet.dart';
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/organization/bound.dart';
|
||||
import 'package:pshared/models/payment/methods/card.dart';
|
||||
import 'package:pshared/models/payment/methods/card_token.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/iban.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||
@@ -40,20 +49,17 @@ extension PaymentMethodMapper on PaymentMethod {
|
||||
isMain: isMain,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _dataToJson(PaymentMethodData data) {
|
||||
switch (data.type) {
|
||||
case PaymentType.card:
|
||||
return (data as CardPaymentMethod).toDTO().toJson();
|
||||
case PaymentType.iban:
|
||||
return (data as IbanPaymentMethod).toDTO().toJson();
|
||||
case PaymentType.bankAccount:
|
||||
return (data as RussianBankAccountPaymentMethod).toDTO().toJson();
|
||||
case PaymentType.wallet:
|
||||
return (data as WalletPaymentMethod).toDTO().toJson();
|
||||
case PaymentType.cryptoAddress:
|
||||
return (data as CryptoAddressPaymentMethod).toDTO().toJson();
|
||||
}
|
||||
}
|
||||
Map<String, dynamic> _dataToJson(PaymentMethodData data) => switch (data) {
|
||||
CardPaymentMethod card => card.toDTO().toJson(),
|
||||
CardTokenPaymentMethod cardToken => cardToken.toDTO().toJson(),
|
||||
IbanPaymentMethod iban => iban.toDTO().toJson(),
|
||||
RussianBankAccountPaymentMethod bankAccount => bankAccount.toDTO().toJson(),
|
||||
WalletPaymentMethod wallet => wallet.toDTO().toJson(),
|
||||
CryptoAddressPaymentMethod crypto => crypto.toDTO().toJson(),
|
||||
LedgerPaymentMethod ledger => ledger.toDTO().toJson(),
|
||||
ManagedWalletPaymentMethod managedWallet => managedWallet.toDTO().toJson(),
|
||||
_ => throw UnsupportedError('Unsupported payment method data: ${data.runtimeType}'),
|
||||
};
|
||||
}
|
||||
|
||||
extension PaymentMethodDTOMapper on PaymentMethodDTO {
|
||||
@@ -73,15 +79,21 @@ extension PaymentMethodDTOMapper on PaymentMethodDTO {
|
||||
PaymentMethodData _dataToDomain(PaymentType paymentType, Map<String, dynamic> payload) {
|
||||
switch (paymentType) {
|
||||
case PaymentType.card:
|
||||
return CardPaymentDataDTO.fromJson(payload).toDomain();
|
||||
return CardEndpointDTO.fromJson(payload).toDomain();
|
||||
case PaymentType.cardToken:
|
||||
return CardTokenEndpointDTO.fromJson(payload).toDomain();
|
||||
case PaymentType.iban:
|
||||
return IbanPaymentDataDTO.fromJson(payload).toDomain();
|
||||
case PaymentType.bankAccount:
|
||||
return RussianBankAccountPaymentDataDTO.fromJson(payload).toDomain();
|
||||
case PaymentType.wallet:
|
||||
return WalletPaymentDataDTO.fromJson(payload).toDomain();
|
||||
case PaymentType.cryptoAddress:
|
||||
return CryptoAddressPaymentDataDTO.fromJson(payload).toDomain();
|
||||
case PaymentType.externalChain:
|
||||
return ExternalChainEndpointDTO.fromJson(payload).toDomain();
|
||||
case PaymentType.ledger:
|
||||
return LedgerEndpointDTO.fromJson(payload).toDomain();
|
||||
case PaymentType.managedWallet:
|
||||
return ManagedWalletEndpointDTO.fromJson(payload).toDomain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
frontend/pshared/lib/data/mapper/payment/money.dart
Normal file
16
frontend/pshared/lib/data/mapper/payment/money.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:pshared/data/dto/payment/money.dart';
|
||||
import 'package:pshared/models/payment/money.dart';
|
||||
|
||||
extension MoneyMapper on Money {
|
||||
MoneyDTO toDTO() => MoneyDTO(
|
||||
amount: amount,
|
||||
currency: currency,
|
||||
);
|
||||
}
|
||||
|
||||
extension MoneyDTOMapper on MoneyDTO {
|
||||
Money toDomain() => Money(
|
||||
amount: amount,
|
||||
currency: currency,
|
||||
);
|
||||
}
|
||||
18
frontend/pshared/lib/data/mapper/payment/network_fee.dart
Normal file
18
frontend/pshared/lib/data/mapper/payment/network_fee.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:pshared/data/dto/payment/network_fee.dart';
|
||||
import 'package:pshared/data/mapper/payment/money.dart';
|
||||
import 'package:pshared/models/payment/fees/network.dart';
|
||||
|
||||
|
||||
extension NetworkFeeDTOMapper on NetworkFeeDTO {
|
||||
NetworkFee toDomain() => NetworkFee(
|
||||
networkFee: networkFee?.toDomain(),
|
||||
estimationContext: estimationContext,
|
||||
);
|
||||
}
|
||||
|
||||
extension NetworkFeeMapper on NetworkFee {
|
||||
NetworkFeeDTO toDTO() => NetworkFeeDTO(
|
||||
networkFee: networkFee?.toDTO(),
|
||||
estimationContext: estimationContext,
|
||||
);
|
||||
}
|
||||
133
frontend/pshared/lib/data/mapper/payment/payment.dart
Normal file
133
frontend/pshared/lib/data/mapper/payment/payment.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'package:pshared/data/dto/payment/card.dart';
|
||||
import 'package:pshared/data/dto/payment/card_token.dart';
|
||||
import 'package:pshared/data/dto/payment/endpoint.dart';
|
||||
import 'package:pshared/data/dto/payment/external_chain.dart';
|
||||
import 'package:pshared/data/dto/payment/ledger.dart';
|
||||
import 'package:pshared/data/dto/payment/managed_wallet.dart';
|
||||
import 'package:pshared/data/mapper/payment/asset.dart';
|
||||
import 'package:pshared/data/mapper/payment/type.dart';
|
||||
import 'package:pshared/models/payment/methods/card.dart';
|
||||
import 'package:pshared/models/payment/methods/card_token.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
extension PaymentMethodDataEndpointMapper on PaymentMethodData {
|
||||
PaymentEndpointDTO toDTO() {
|
||||
final metadata = this.metadata;
|
||||
|
||||
switch (type) {
|
||||
case PaymentType.ledger:
|
||||
final payload = this as LedgerPaymentMethod;
|
||||
return PaymentEndpointDTO(
|
||||
type: paymentTypeToValue(type),
|
||||
data: LedgerEndpointDTO(
|
||||
ledgerAccountRef: payload.ledgerAccountRef,
|
||||
contraLedgerAccountRef: payload.contraLedgerAccountRef,
|
||||
).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.managedWallet:
|
||||
final payload = this as ManagedWalletPaymentMethod;
|
||||
return PaymentEndpointDTO(
|
||||
type: paymentTypeToValue(type),
|
||||
data: ManagedWalletEndpointDTO(
|
||||
managedWalletRef: payload.managedWalletRef,
|
||||
asset: payload.asset?.toDTO(),
|
||||
).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.externalChain:
|
||||
final payload = this as CryptoAddressPaymentMethod;
|
||||
return PaymentEndpointDTO(
|
||||
type: paymentTypeToValue(type),
|
||||
data: ExternalChainEndpointDTO(
|
||||
asset: payload.asset?.toDTO(),
|
||||
address: payload.address,
|
||||
memo: payload.memo,
|
||||
).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.card:
|
||||
final payload = this as CardPaymentMethod;
|
||||
return PaymentEndpointDTO(
|
||||
type: paymentTypeToValue(type),
|
||||
data: CardEndpointDTO(
|
||||
pan: payload.pan,
|
||||
expMonth: payload.expMonth,
|
||||
expYear: payload.expYear,
|
||||
country: payload.country,
|
||||
firstName: payload.firstName,
|
||||
lastName: payload.lastName,
|
||||
).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.cardToken:
|
||||
final payload = this as CardTokenPaymentMethod;
|
||||
return PaymentEndpointDTO(
|
||||
type: paymentTypeToValue(type),
|
||||
data: CardTokenEndpointDTO(
|
||||
token: payload.token,
|
||||
maskedPan: payload.maskedPan,
|
||||
).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError('Unsupported payment endpoint type: $type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PaymentEndpointDTOMapper on PaymentEndpointDTO {
|
||||
PaymentMethodData toDomain() {
|
||||
final metadata = this.metadata;
|
||||
|
||||
switch (paymentTypeFromValue(type)) {
|
||||
case PaymentType.ledger:
|
||||
final payload = LedgerEndpointDTO.fromJson(data);
|
||||
return LedgerPaymentMethod(
|
||||
ledgerAccountRef: payload.ledgerAccountRef,
|
||||
contraLedgerAccountRef: payload.contraLedgerAccountRef,
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.managedWallet:
|
||||
final payload = ManagedWalletEndpointDTO.fromJson(data);
|
||||
return ManagedWalletPaymentMethod(
|
||||
managedWalletRef: payload.managedWalletRef,
|
||||
asset: payload.asset?.toDomain(),
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.externalChain:
|
||||
final payload = ExternalChainEndpointDTO.fromJson(data);
|
||||
return CryptoAddressPaymentMethod(
|
||||
asset: payload.asset?.toDomain(),
|
||||
address: payload.address,
|
||||
memo: payload.memo,
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.card:
|
||||
final payload = CardEndpointDTO.fromJson(data);
|
||||
return CardPaymentMethod(
|
||||
pan: payload.pan,
|
||||
firstName: payload.firstName,
|
||||
lastName: payload.lastName,
|
||||
expMonth: payload.expMonth,
|
||||
expYear: payload.expYear,
|
||||
country: payload.country,
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.cardToken:
|
||||
final payload = CardTokenEndpointDTO.fromJson(data);
|
||||
return CardTokenPaymentMethod(
|
||||
token: payload.token,
|
||||
maskedPan: payload.maskedPan,
|
||||
metadata: metadata,
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError('Unsupported payment endpoint type: ${paymentTypeFromValue(type)}');
|
||||
}
|
||||
}
|
||||
}
|
||||
33
frontend/pshared/lib/data/mapper/payment/payment_quote.dart
Normal file
33
frontend/pshared/lib/data/mapper/payment/payment_quote.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
||||
import 'package:pshared/data/mapper/payment/fee_line.dart';
|
||||
import 'package:pshared/data/mapper/payment/fx_quote.dart';
|
||||
import 'package:pshared/data/mapper/payment/money.dart';
|
||||
import 'package:pshared/data/mapper/payment/network_fee.dart';
|
||||
import 'package:pshared/models/payment/quote.dart';
|
||||
|
||||
|
||||
extension PaymentQuoteDTOMapper on PaymentQuoteDTO {
|
||||
PaymentQuote toDomain() => PaymentQuote(
|
||||
quoteRef: quoteRef,
|
||||
debitAmount: debitAmount?.toDomain(),
|
||||
expectedSettlementAmount: expectedSettlementAmount?.toDomain(),
|
||||
expectedFeeTotal: expectedFeeTotal?.toDomain(),
|
||||
feeQuoteToken: feeQuoteToken,
|
||||
feeLines: feeLines?.map((line) => line.toDomain()).toList(),
|
||||
networkFee: networkFee?.toDomain(),
|
||||
fxQuote: fxQuote?.toDomain(),
|
||||
);
|
||||
}
|
||||
|
||||
extension PaymentQuoteMapper on PaymentQuote {
|
||||
PaymentQuoteDTO toDTO() => PaymentQuoteDTO(
|
||||
quoteRef: quoteRef,
|
||||
debitAmount: debitAmount?.toDTO(),
|
||||
expectedSettlementAmount: expectedSettlementAmount?.toDTO(),
|
||||
expectedFeeTotal: expectedFeeTotal?.toDTO(),
|
||||
feeQuoteToken: feeQuoteToken,
|
||||
feeLines: feeLines?.map((line) => line.toDTO()).toList(),
|
||||
networkFee: networkFee?.toDTO(),
|
||||
fxQuote: fxQuote?.toDTO(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:pshared/data/dto/payment/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/payment_quote.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
|
||||
|
||||
extension PaymentDTOMapper on PaymentDTO {
|
||||
Payment toDomain() => Payment(
|
||||
paymentRef: paymentRef,
|
||||
idempotencyKey: idempotencyKey,
|
||||
state: state,
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
lastQuote: lastQuote?.toDomain(),
|
||||
);
|
||||
}
|
||||
|
||||
extension PaymentMapper on Payment {
|
||||
PaymentDTO toDTO() => PaymentDTO(
|
||||
paymentRef: paymentRef,
|
||||
idempotencyKey: idempotencyKey,
|
||||
state: state,
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
lastQuote: lastQuote?.toDTO(),
|
||||
);
|
||||
}
|
||||
@@ -7,12 +7,18 @@ PaymentType paymentTypeFromValue(String value) {
|
||||
return PaymentType.iban;
|
||||
case 'card':
|
||||
return PaymentType.card;
|
||||
case 'cardToken':
|
||||
return PaymentType.cardToken;
|
||||
case 'bankAccount':
|
||||
return PaymentType.bankAccount;
|
||||
case 'ledger':
|
||||
return PaymentType.ledger;
|
||||
case 'wallet':
|
||||
return PaymentType.wallet;
|
||||
case 'managedWallet':
|
||||
return PaymentType.managedWallet;
|
||||
case 'cryptoAddress':
|
||||
return PaymentType.cryptoAddress;
|
||||
return PaymentType.externalChain;
|
||||
default:
|
||||
return PaymentType.iban;
|
||||
}
|
||||
@@ -24,11 +30,17 @@ String paymentTypeToValue(PaymentType type) {
|
||||
return 'iban';
|
||||
case PaymentType.card:
|
||||
return 'card';
|
||||
case PaymentType.cardToken:
|
||||
return 'cardToken';
|
||||
case PaymentType.ledger:
|
||||
return 'ledger';
|
||||
case PaymentType.bankAccount:
|
||||
return 'bankAccount';
|
||||
case PaymentType.wallet:
|
||||
return 'wallet';
|
||||
case PaymentType.cryptoAddress:
|
||||
case PaymentType.managedWallet:
|
||||
return 'managedWallet';
|
||||
case PaymentType.externalChain:
|
||||
return 'cryptoAddress';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +29,25 @@
|
||||
"resourceEmpty": "Empty data",
|
||||
"@resourceEmpty": {
|
||||
"description": "Default message shown when no data is available"
|
||||
},
|
||||
|
||||
"chainNetworkUnspecified": "Unspecified network",
|
||||
"@chainNetworkUnspecified": {
|
||||
"description": "Fallback label when the chain network is not known"
|
||||
},
|
||||
|
||||
"chainNetworkEthereumMainnet": "Ethereum Mainnet",
|
||||
"@chainNetworkEthereumMainnet": {
|
||||
"description": "Label for the Ethereum mainnet network"
|
||||
},
|
||||
|
||||
"chainNetworkArbitrumOne": "Arbitrum One",
|
||||
"@chainNetworkArbitrumOne": {
|
||||
"description": "Label for the Arbitrum One network"
|
||||
},
|
||||
|
||||
"chainNetworkOtherEvm": "Other EVM chain",
|
||||
"@chainNetworkOtherEvm": {
|
||||
"description": "Label for any other EVM-compatible network"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +29,25 @@
|
||||
"resourceEmpty": "Нет данных",
|
||||
"@resourceEmpty": {
|
||||
"description": "Default message shown when no data is available"
|
||||
},
|
||||
|
||||
"chainNetworkUnspecified": "Сеть не указана",
|
||||
"@chainNetworkUnspecified": {
|
||||
"description": "Fallback label when the chain network is not known"
|
||||
},
|
||||
|
||||
"chainNetworkEthereumMainnet": "Ethereum Mainnet",
|
||||
"@chainNetworkEthereumMainnet": {
|
||||
"description": "Label for the Ethereum mainnet network"
|
||||
},
|
||||
|
||||
"chainNetworkArbitrumOne": "Arbitrum One",
|
||||
"@chainNetworkArbitrumOne": {
|
||||
"description": "Label for the Arbitrum One network"
|
||||
},
|
||||
|
||||
"chainNetworkOtherEvm": "Другая EVM сеть",
|
||||
"@chainNetworkOtherEvm": {
|
||||
"description": "Label for any other EVM-compatible network"
|
||||
}
|
||||
}
|
||||
|
||||
14
frontend/pshared/lib/models/payment/asset.dart
Normal file
14
frontend/pshared/lib/models/payment/asset.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
|
||||
|
||||
class PaymentAsset {
|
||||
final ChainNetwork chain;
|
||||
final String tokenSymbol;
|
||||
final String? contractAddress;
|
||||
|
||||
const PaymentAsset({
|
||||
this.chain = ChainNetwork.unspecified,
|
||||
required this.tokenSymbol,
|
||||
this.contractAddress,
|
||||
});
|
||||
}
|
||||
1
frontend/pshared/lib/models/payment/chain_network.dart
Normal file
1
frontend/pshared/lib/models/payment/chain_network.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum ChainNetwork { unspecified, ethereumMainnet, arbitrumOne, otherEvm }
|
||||
9
frontend/pshared/lib/models/payment/currency_pair.dart
Normal file
9
frontend/pshared/lib/models/payment/currency_pair.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
class CurrencyPair {
|
||||
final String base;
|
||||
final String quote;
|
||||
|
||||
const CurrencyPair({
|
||||
required this.base,
|
||||
required this.quote,
|
||||
});
|
||||
}
|
||||
18
frontend/pshared/lib/models/payment/fees/line.dart
Normal file
18
frontend/pshared/lib/models/payment/fees/line.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:pshared/models/payment/money.dart';
|
||||
|
||||
|
||||
class FeeLine {
|
||||
final String? ledgerAccountRef;
|
||||
final Money? amount;
|
||||
final String? lineType;
|
||||
final String? side;
|
||||
final Map<String, String>? meta;
|
||||
|
||||
const FeeLine({
|
||||
required this.ledgerAccountRef,
|
||||
required this.amount,
|
||||
required this.lineType,
|
||||
required this.side,
|
||||
required this.meta,
|
||||
});
|
||||
}
|
||||
12
frontend/pshared/lib/models/payment/fees/network.dart
Normal file
12
frontend/pshared/lib/models/payment/fees/network.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:pshared/models/payment/money.dart';
|
||||
|
||||
|
||||
class NetworkFee {
|
||||
final Money? networkFee;
|
||||
final String? estimationContext;
|
||||
|
||||
const NetworkFee({
|
||||
required this.networkFee,
|
||||
required this.estimationContext,
|
||||
});
|
||||
}
|
||||
21
frontend/pshared/lib/models/payment/fx/intent.dart
Normal file
21
frontend/pshared/lib/models/payment/fx/intent.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:pshared/models/payment/currency_pair.dart';
|
||||
import 'package:pshared/models/payment/fx/side.dart';
|
||||
|
||||
|
||||
class FxIntent {
|
||||
final CurrencyPair? pair;
|
||||
final FxSide side;
|
||||
final bool firm;
|
||||
final int? ttlMs;
|
||||
final String? preferredProvider;
|
||||
final int? maxAgeMs;
|
||||
|
||||
const FxIntent({
|
||||
this.pair,
|
||||
this.side = FxSide.unspecified,
|
||||
this.firm = false,
|
||||
this.ttlMs,
|
||||
this.preferredProvider,
|
||||
this.maxAgeMs,
|
||||
});
|
||||
}
|
||||
30
frontend/pshared/lib/models/payment/fx/quote.dart
Normal file
30
frontend/pshared/lib/models/payment/fx/quote.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:pshared/models/payment/money.dart';
|
||||
|
||||
|
||||
class FxQuote {
|
||||
final String? quoteRef;
|
||||
final String? baseCurrency;
|
||||
final String? quoteCurrency;
|
||||
final String? side;
|
||||
final String? price;
|
||||
final Money? baseAmount;
|
||||
final Money? quoteAmount;
|
||||
final int? expiresAtUnixMs;
|
||||
final String? provider;
|
||||
final String? rateRef;
|
||||
final bool firm;
|
||||
|
||||
const FxQuote({
|
||||
required this.quoteRef,
|
||||
required this.baseCurrency,
|
||||
required this.quoteCurrency,
|
||||
required this.side,
|
||||
required this.price,
|
||||
required this.baseAmount,
|
||||
required this.quoteAmount,
|
||||
required this.expiresAtUnixMs,
|
||||
required this.provider,
|
||||
required this.rateRef,
|
||||
this.firm = false,
|
||||
});
|
||||
}
|
||||
1
frontend/pshared/lib/models/payment/fx/side.dart
Normal file
1
frontend/pshared/lib/models/payment/fx/side.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum FxSide { unspecified, buyBaseSellQuote, sellBaseBuyQuote }
|
||||
@@ -0,0 +1,6 @@
|
||||
enum InsufficientNetPolicy {
|
||||
unspecified,
|
||||
blockPosting,
|
||||
sweepOrgCash,
|
||||
invoiceLater
|
||||
}
|
||||
26
frontend/pshared/lib/models/payment/intent.dart
Normal file
26
frontend/pshared/lib/models/payment/intent.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:pshared/models/payment/fx/intent.dart';
|
||||
import 'package:pshared/models/payment/kind.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/money.dart';
|
||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||
|
||||
|
||||
class PaymentIntent {
|
||||
final PaymentKind kind;
|
||||
final PaymentMethodData? source;
|
||||
final PaymentMethodData? destination;
|
||||
final Money? amount;
|
||||
final FxIntent? fx;
|
||||
final SettlementMode settlementMode;
|
||||
final Map<String, String>? attributes;
|
||||
|
||||
const PaymentIntent({
|
||||
this.kind = PaymentKind.unspecified,
|
||||
this.source,
|
||||
this.destination,
|
||||
this.amount,
|
||||
this.fx,
|
||||
this.settlementMode = SettlementMode.unspecified,
|
||||
this.attributes,
|
||||
});
|
||||
}
|
||||
1
frontend/pshared/lib/models/payment/kind.dart
Normal file
1
frontend/pshared/lib/models/payment/kind.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum PaymentKind { unspecified, payout, internalTransfer, fxConversion }
|
||||
@@ -2,17 +2,25 @@ import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
class CardPaymentMethod extends PaymentMethodData {
|
||||
class CardPaymentMethod implements PaymentMethodData {
|
||||
@override
|
||||
final PaymentType type = PaymentType.card;
|
||||
|
||||
final String pan;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final int? expMonth;
|
||||
final int? expYear;
|
||||
final String? country;
|
||||
@override
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
CardPaymentMethod({
|
||||
const CardPaymentMethod({
|
||||
required this.pan,
|
||||
this.expMonth,
|
||||
this.expYear,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
this.country,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
18
frontend/pshared/lib/models/payment/methods/card_token.dart
Normal file
18
frontend/pshared/lib/models/payment/methods/card_token.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
class CardTokenPaymentMethod implements PaymentMethodData {
|
||||
@override
|
||||
final PaymentType type = PaymentType.cardToken;
|
||||
final String token;
|
||||
final String maskedPan;
|
||||
@override
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
const CardTokenPaymentMethod({
|
||||
required this.token,
|
||||
required this.maskedPan,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
class CryptoAddressPaymentMethod extends PaymentMethodData {
|
||||
class CryptoAddressPaymentMethod implements PaymentMethodData {
|
||||
@override
|
||||
final PaymentType type = PaymentType.cryptoAddress;
|
||||
|
||||
final PaymentType type = PaymentType.externalChain;
|
||||
final PaymentAsset? asset;
|
||||
final String address;
|
||||
final String network;
|
||||
final String? destinationTag;
|
||||
final String? memo;
|
||||
@override
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
CryptoAddressPaymentMethod({
|
||||
const CryptoAddressPaymentMethod({
|
||||
this.asset,
|
||||
required this.address,
|
||||
required this.network,
|
||||
this.destinationTag,
|
||||
this.memo,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
abstract class PaymentMethodData {
|
||||
PaymentType get type;
|
||||
Map<String, String>? get metadata;
|
||||
}
|
||||
|
||||
typedef MethodMap = Map<PaymentType, PaymentMethodData?>;
|
||||
@@ -2,7 +2,7 @@ import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
class IbanPaymentMethod extends PaymentMethodData {
|
||||
class IbanPaymentMethod implements PaymentMethodData {
|
||||
@override
|
||||
final PaymentType type = PaymentType.iban;
|
||||
|
||||
@@ -10,11 +10,14 @@ class IbanPaymentMethod extends PaymentMethodData {
|
||||
final String accountHolder; // Full name of the recipient
|
||||
final String? bic; // Optional: for cross-border transfers
|
||||
final String? bankName; // Optional: for UI clarity
|
||||
@override
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
IbanPaymentMethod({
|
||||
const IbanPaymentMethod({
|
||||
required this.iban,
|
||||
required this.accountHolder,
|
||||
this.bic,
|
||||
this.bankName,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
18
frontend/pshared/lib/models/payment/methods/ledger.dart
Normal file
18
frontend/pshared/lib/models/payment/methods/ledger.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
class LedgerPaymentMethod implements PaymentMethodData {
|
||||
@override
|
||||
final PaymentType type = PaymentType.ledger;
|
||||
final String ledgerAccountRef;
|
||||
final String? contraLedgerAccountRef;
|
||||
@override
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
const LedgerPaymentMethod({
|
||||
required this.ledgerAccountRef,
|
||||
this.contraLedgerAccountRef,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
class ManagedWalletPaymentMethod implements PaymentMethodData {
|
||||
@override
|
||||
final PaymentType type = PaymentType.managedWallet;
|
||||
final String managedWalletRef;
|
||||
final PaymentAsset? asset;
|
||||
@override
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
const ManagedWalletPaymentMethod({
|
||||
required this.managedWalletRef,
|
||||
this.asset,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
class RussianBankAccountPaymentMethod extends PaymentMethodData {
|
||||
class RussianBankAccountPaymentMethod implements PaymentMethodData {
|
||||
@override
|
||||
final PaymentType type = PaymentType.bankAccount;
|
||||
|
||||
@@ -13,8 +13,10 @@ class RussianBankAccountPaymentMethod extends PaymentMethodData {
|
||||
final String bik;
|
||||
final String accountNumber;
|
||||
final String correspondentAccount;
|
||||
@override
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
RussianBankAccountPaymentMethod({
|
||||
const RussianBankAccountPaymentMethod({
|
||||
required this.recipientName,
|
||||
required this.inn,
|
||||
required this.kpp,
|
||||
@@ -22,5 +24,6 @@ class RussianBankAccountPaymentMethod extends PaymentMethodData {
|
||||
required this.bik,
|
||||
required this.accountNumber,
|
||||
required this.correspondentAccount,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
class WalletPaymentMethod extends PaymentMethodData {
|
||||
class WalletPaymentMethod implements PaymentMethodData {
|
||||
@override
|
||||
final PaymentType type = PaymentType.wallet;
|
||||
|
||||
final String walletId;
|
||||
|
||||
WalletPaymentMethod({required this.walletId});
|
||||
@override
|
||||
final Map<String, String>? metadata;
|
||||
|
||||
WalletPaymentMethod({required this.walletId, this.metadata});
|
||||
}
|
||||
|
||||
9
frontend/pshared/lib/models/payment/money.dart
Normal file
9
frontend/pshared/lib/models/payment/money.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
class Money {
|
||||
final String amount;
|
||||
final String currency;
|
||||
|
||||
const Money({
|
||||
required this.amount,
|
||||
required this.currency,
|
||||
});
|
||||
}
|
||||
20
frontend/pshared/lib/models/payment/payment.dart
Normal file
20
frontend/pshared/lib/models/payment/payment.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:pshared/models/payment/quote.dart';
|
||||
|
||||
|
||||
class Payment {
|
||||
final String? paymentRef;
|
||||
final String? idempotencyKey;
|
||||
final String? state;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
final PaymentQuote? lastQuote;
|
||||
|
||||
const Payment({
|
||||
required this.paymentRef,
|
||||
required this.idempotencyKey,
|
||||
required this.state,
|
||||
required this.failureCode,
|
||||
required this.failureReason,
|
||||
required this.lastQuote,
|
||||
});
|
||||
}
|
||||
27
frontend/pshared/lib/models/payment/quote.dart
Normal file
27
frontend/pshared/lib/models/payment/quote.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:pshared/models/payment/fees/line.dart';
|
||||
import 'package:pshared/models/payment/fx/quote.dart';
|
||||
import 'package:pshared/models/payment/money.dart';
|
||||
import 'package:pshared/models/payment/fees/network.dart';
|
||||
|
||||
|
||||
class PaymentQuote {
|
||||
final String? quoteRef;
|
||||
final Money? debitAmount;
|
||||
final Money? expectedSettlementAmount;
|
||||
final Money? expectedFeeTotal;
|
||||
final String? feeQuoteToken;
|
||||
final List<FeeLine>? feeLines;
|
||||
final NetworkFee? networkFee;
|
||||
final FxQuote? fxQuote;
|
||||
|
||||
const PaymentQuote({
|
||||
required this.quoteRef,
|
||||
required this.debitAmount,
|
||||
required this.expectedSettlementAmount,
|
||||
required this.expectedFeeTotal,
|
||||
required this.feeQuoteToken,
|
||||
required this.feeLines,
|
||||
required this.networkFee,
|
||||
required this.fxQuote,
|
||||
});
|
||||
}
|
||||
1
frontend/pshared/lib/models/payment/settlement_mode.dart
Normal file
1
frontend/pshared/lib/models/payment/settlement_mode.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum SettlementMode { unspecified, fixSource, fixReceived }
|
||||
@@ -1,7 +1,10 @@
|
||||
enum PaymentType {
|
||||
ledger,
|
||||
managedWallet,
|
||||
externalChain,
|
||||
bankAccount,
|
||||
iban,
|
||||
wallet,
|
||||
card,
|
||||
cryptoAddress,
|
||||
cardToken,
|
||||
}
|
||||
|
||||
54
frontend/pshared/lib/provider/payment/quotation.dart
Normal file
54
frontend/pshared/lib/provider/payment/quotation.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/api/requests/payment/quote.dart';
|
||||
import 'package:pshared/data/mapper/payment/intent/payment.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/quote.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/payment/quotation.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
|
||||
class QuotationProvider extends ChangeNotifier {
|
||||
Resource<PaymentQuote> _quotation = Resource(data: null, isLoading: false, error: null);
|
||||
late OrganizationsProvider _organizations;
|
||||
bool _isLoaded = false;
|
||||
|
||||
void update(OrganizationsProvider venue) {
|
||||
_organizations = venue;
|
||||
}
|
||||
|
||||
PaymentQuote? get quotation => _quotation.data;
|
||||
|
||||
bool get isReady => _isLoaded && !_quotation.isLoading && _quotation.error == null;
|
||||
|
||||
Future<PaymentQuote?> getQuotation(PaymentIntent intent) async {
|
||||
if (!_organizations.isOrganizationSet) throw StateError('Organization is not set');
|
||||
try {
|
||||
_quotation = _quotation.copyWith(isLoading: true, error: null);
|
||||
final response = await QuotationService.getQuotation(
|
||||
_organizations.current.id,
|
||||
QuotePaymentRequest(
|
||||
idempotencyKey: Uuid().v4(),
|
||||
intent: intent.toDTO(),
|
||||
),
|
||||
);
|
||||
_isLoaded = true;
|
||||
_quotation = _quotation.copyWith(data: response, isLoading: false);
|
||||
} catch (e) {
|
||||
_quotation = _quotation.copyWith(
|
||||
error: e is Exception ? e : Exception(e.toString()),
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
return _quotation.data;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_quotation = Resource(data: null, isLoading: false, error: null);
|
||||
_isLoaded = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
20
frontend/pshared/lib/service/payment/quotation.dart
Normal file
20
frontend/pshared/lib/service/payment/quotation.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:pshared/api/requests/payment/quote.dart';
|
||||
import 'package:pshared/api/responses/payment/quotation.dart';
|
||||
import 'package:pshared/data/mapper/payment/payment_quote.dart';
|
||||
import 'package:pshared/models/payment/quote.dart';
|
||||
import 'package:pshared/service/services.dart';
|
||||
import 'package:pshared/utils/http/requests.dart';
|
||||
|
||||
|
||||
class QuotationService {
|
||||
static final _logger = Logger('service.payment.quotation');
|
||||
static const String _objectType = Services.payments;
|
||||
|
||||
static Future<PaymentQuote> getQuotation(String organizationRef, QuotePaymentRequest request) async {
|
||||
_logger.fine('Quoting payment for organization $organizationRef');
|
||||
final response = await getPOSTResponse(_objectType, '/quote/$organizationRef', request.toJson());
|
||||
return PaymentQuoteResponse.fromJson(response).quote.toDomain();
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ class Services {
|
||||
|
||||
static const String recipients = 'recipients';
|
||||
static const String paymentMethods = 'payment_methods';
|
||||
static const String payments = 'payments';
|
||||
|
||||
static const String amplitude = 'amplitude';
|
||||
static const String clients = 'clients';
|
||||
static const String logo = 'logo';
|
||||
static const String notifications = 'notifications';
|
||||
|
||||
22
frontend/pshared/lib/utils/l10n/chain.dart
Normal file
22
frontend/pshared/lib/utils/l10n/chain.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:pshared/generated/i18n/ps_localizations.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
|
||||
/// Localized labels for [ChainNetwork] values.
|
||||
extension ChainNetworkL10n on ChainNetwork {
|
||||
/// Returns a human-readable, localized name for the chain.
|
||||
String localizedName(BuildContext context) {
|
||||
final l10n = PSLocalizations.of(context)!;
|
||||
switch (this) {
|
||||
case ChainNetwork.ethereumMainnet:
|
||||
return l10n.chainNetworkEthereumMainnet;
|
||||
case ChainNetwork.arbitrumOne:
|
||||
return l10n.chainNetworkArbitrumOne;
|
||||
case ChainNetwork.otherEvm:
|
||||
return l10n.chainNetworkOtherEvm;
|
||||
case ChainNetwork.unspecified:
|
||||
return l10n.chainNetworkUnspecified;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||
@@ -92,8 +94,6 @@ class PayoutRoutes {
|
||||
return PayoutDestination.recipients;
|
||||
case addRecipient:
|
||||
return PayoutDestination.addrecipient;
|
||||
case payment:
|
||||
return PayoutDestination.payment;
|
||||
case settings:
|
||||
return PayoutDestination.settings;
|
||||
case reports:
|
||||
|
||||
@@ -312,6 +312,16 @@
|
||||
"paymentTypeIban": "IBAN",
|
||||
"paymentTypeWallet": "Wallet",
|
||||
"paymentTypeCryptoAddress": "Crypto address",
|
||||
"paymentTypeLedger": "Ledger account",
|
||||
"paymentTypeManagedWallet": "Managed wallet",
|
||||
"paymentTypeCardToken": "Card token",
|
||||
|
||||
"cryptoAddressLabel": "Crypto address",
|
||||
"enterCryptoAddress": "Enter a crypto address",
|
||||
"tokenSymbolLabel": "Token symbol",
|
||||
"tokenSymbolRequiredWhenNetwork": "Token symbol is required when a network or contract address is specified",
|
||||
"contractAddressLabel": "Contract address (optional)",
|
||||
"memoLabel": "Destination tag / memo (optional)",
|
||||
|
||||
"cardNumber": "Card Number",
|
||||
"enterCardNumber": "Enter the card number",
|
||||
|
||||
@@ -312,6 +312,16 @@
|
||||
"paymentTypeIban": "IBAN",
|
||||
"paymentTypeWallet": "Кошелек",
|
||||
"paymentTypeCryptoAddress": "Крипто-адрес",
|
||||
"paymentTypeLedger": "Леджер счет",
|
||||
"paymentTypeManagedWallet": "Управляемый кошелек",
|
||||
"paymentTypeCardToken": "Токен карты",
|
||||
|
||||
"cryptoAddressLabel": "Крипто-адрес",
|
||||
"enterCryptoAddress": "Введите крипто-адрес",
|
||||
"tokenSymbolLabel": "Символ токена",
|
||||
"tokenSymbolRequiredWhenNetwork": "Укажите символ токена, если выбрана сеть или указан адрес контракта",
|
||||
"contractAddressLabel": "Адрес контракта (необязательно)",
|
||||
"memoLabel": "Destination tag / memo (необязательно)",
|
||||
|
||||
"cardNumber": "Номер карты",
|
||||
"enterCardNumber": "Введите номер карты",
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:pshared/provider/locale.dart';
|
||||
import 'package:pshared/provider/permissions.dart';
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/payment/quotation.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
|
||||
@@ -24,7 +25,6 @@ import 'package:pweb/providers/two_factor.dart';
|
||||
import 'package:pweb/providers/upload_history.dart';
|
||||
import 'package:pweb/providers/wallets.dart';
|
||||
import 'package:pweb/providers/wallet_transactions.dart';
|
||||
// import 'package:pweb/services/amplitude.dart';
|
||||
import 'package:pweb/services/operations.dart';
|
||||
import 'package:pweb/services/payments/history.dart';
|
||||
import 'package:pweb/services/wallet_transactions.dart';
|
||||
@@ -70,7 +70,6 @@ void main() async {
|
||||
update: (context, orgnization, provider) => provider!..update(orgnization),
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
|
||||
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(),
|
||||
),
|
||||
@@ -92,10 +91,13 @@ void main() async {
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => MockPaymentProvider(),
|
||||
),
|
||||
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => OperationProvider(OperationService())..loadOperations(),
|
||||
),
|
||||
ChangeNotifierProxyProvider<OrganizationsProvider, QuotationProvider>(
|
||||
create: (_) => QuotationProvider(),
|
||||
update: (context, orgnization, provider) => provider!..update(orgnization),
|
||||
),
|
||||
],
|
||||
child: const PayApp(),
|
||||
),
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/form/view.dart';
|
||||
// import 'package:pweb/services/amplitude.dart';
|
||||
import 'package:pweb/utils/error/snackbar.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
import 'package:pweb/utils/snackbar.dart';
|
||||
@@ -54,7 +53,9 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
|
||||
PaymentType.iban => m.ibanData,
|
||||
PaymentType.wallet => m.walletData,
|
||||
PaymentType.bankAccount => m.bankAccountData,
|
||||
PaymentType.cryptoAddress => m.cryptoAddressData,
|
||||
PaymentType.externalChain => m.cryptoAddressData,
|
||||
//TODO: support new payment methods
|
||||
_ => throw UnimplementedError('Payment method ${m.type} is not supported yet'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
import 'package:pshared/utils/l10n/chain.dart';
|
||||
|
||||
import 'package:pweb/utils/text_field_styles.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class CryptoAddressForm extends StatefulWidget {
|
||||
final void Function(CryptoAddressPaymentMethod) onChanged;
|
||||
@@ -22,28 +27,67 @@ class CryptoAddressForm extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CryptoAddressFormState extends State<CryptoAddressForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
late TextEditingController _addressCtrl;
|
||||
late TextEditingController _networkCtrl;
|
||||
late TextEditingController _destinationTagCtrl;
|
||||
late TextEditingController _tokenCtrl;
|
||||
late TextEditingController _contractCtrl;
|
||||
late TextEditingController _memoCtrl;
|
||||
late ChainNetwork _chain;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_addressCtrl = TextEditingController(text: widget.initialData?.address);
|
||||
_networkCtrl = TextEditingController(text: widget.initialData?.network);
|
||||
_destinationTagCtrl = TextEditingController(text: widget.initialData?.destinationTag);
|
||||
final initial = widget.initialData;
|
||||
_chain = initial?.asset?.chain ?? ChainNetwork.unspecified;
|
||||
_addressCtrl = TextEditingController(text: initial?.address ?? '');
|
||||
_tokenCtrl = TextEditingController(text: initial?.asset?.tokenSymbol ?? '');
|
||||
_contractCtrl = TextEditingController(text: initial?.asset?.contractAddress ?? '');
|
||||
_memoCtrl = TextEditingController(text: initial?.memo ?? '');
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
|
||||
}
|
||||
|
||||
void _emit() {
|
||||
if (_addressCtrl.text.isNotEmpty && _networkCtrl.text.isNotEmpty) {
|
||||
widget.onChanged(
|
||||
CryptoAddressPaymentMethod(
|
||||
address: _addressCtrl.text,
|
||||
network: _networkCtrl.text,
|
||||
destinationTag: _destinationTagCtrl.text.isNotEmpty ? _destinationTagCtrl.text : null,
|
||||
),
|
||||
);
|
||||
bool get _hasChainSelection => _chain != ChainNetwork.unspecified;
|
||||
|
||||
String? _validateAddress(AppLocalizations l10n, String? value) {
|
||||
if (value == null || value.trim().isEmpty) return l10n.enterCryptoAddress;
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validateToken(AppLocalizations l10n) {
|
||||
final token = _tokenCtrl.text.trim();
|
||||
final contract = _contractCtrl.text.trim();
|
||||
if ((_hasChainSelection || contract.isNotEmpty) && token.isEmpty) {
|
||||
return l10n.tokenSymbolRequiredWhenNetwork;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
PaymentAsset? _buildAsset() {
|
||||
final token = _tokenCtrl.text.trim();
|
||||
final contract = _contractCtrl.text.trim();
|
||||
|
||||
if (token.isEmpty && contract.isEmpty && !_hasChainSelection) return null;
|
||||
if (token.isEmpty) return null;
|
||||
|
||||
return PaymentAsset(
|
||||
chain: _chain,
|
||||
tokenSymbol: token,
|
||||
contractAddress: contract.isNotEmpty ? contract : null,
|
||||
);
|
||||
}
|
||||
|
||||
void _emitIfValid() {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
widget.onChanged(
|
||||
CryptoAddressPaymentMethod(
|
||||
asset: _buildAsset(),
|
||||
address: _addressCtrl.text.trim(),
|
||||
memo: _memoCtrl.text.trim().isNotEmpty ? _memoCtrl.text.trim() : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -54,48 +98,88 @@ class _CryptoAddressFormState extends State<CryptoAddressForm> {
|
||||
|
||||
if (newData == null && oldData != null) {
|
||||
_addressCtrl.clear();
|
||||
_networkCtrl.clear();
|
||||
_destinationTagCtrl.clear();
|
||||
_tokenCtrl.clear();
|
||||
_contractCtrl.clear();
|
||||
_memoCtrl.clear();
|
||||
_chain = ChainNetwork.unspecified;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newData != null && newData != oldData) {
|
||||
_addressCtrl.text = newData.address;
|
||||
_networkCtrl.text = newData.network;
|
||||
_destinationTagCtrl.text = newData.destinationTag ?? '';
|
||||
_tokenCtrl.text = newData.asset?.tokenSymbol ?? '';
|
||||
_contractCtrl.text = newData.asset?.contractAddress ?? '';
|
||||
_memoCtrl.text = newData.memo ?? '';
|
||||
_chain = newData.asset?.chain ?? ChainNetwork.unspecified;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
readOnly: !widget.isEditable,
|
||||
controller: _addressCtrl,
|
||||
decoration: getInputDecoration(context, 'Crypto address', widget.isEditable),
|
||||
style: getTextFieldStyle(context, widget.isEditable),
|
||||
onChanged: (_) => _emit(),
|
||||
validator: (val) => (val?.isEmpty ?? true) ? 'Enter crypto address' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
readOnly: !widget.isEditable,
|
||||
controller: _networkCtrl,
|
||||
decoration: getInputDecoration(context, 'Network', widget.isEditable),
|
||||
style: getTextFieldStyle(context, widget.isEditable),
|
||||
onChanged: (_) => _emit(),
|
||||
validator: (val) => (val?.isEmpty ?? true) ? 'Enter network' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
readOnly: !widget.isEditable,
|
||||
controller: _destinationTagCtrl,
|
||||
decoration: getInputDecoration(context, 'Destination tag / memo (optional)', widget.isEditable),
|
||||
style: getTextFieldStyle(context, widget.isEditable),
|
||||
onChanged: (_) => _emit(),
|
||||
),
|
||||
],
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
onChanged: _emitIfValid,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
TextFormField(
|
||||
readOnly: !widget.isEditable,
|
||||
controller: _addressCtrl,
|
||||
decoration: getInputDecoration(context, l10n.cryptoAddressLabel, widget.isEditable),
|
||||
style: getTextFieldStyle(context, widget.isEditable),
|
||||
validator: (val) => _validateAddress(l10n, val),
|
||||
),
|
||||
DropdownButtonFormField<ChainNetwork>(
|
||||
initialValue: _chain,
|
||||
decoration: getInputDecoration(context, l10n.walletTopUpNetworkLabel, widget.isEditable),
|
||||
items: ChainNetwork.values
|
||||
.map((chain) => DropdownMenuItem(
|
||||
value: chain,
|
||||
child: Text(chain.localizedName(context)),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: widget.isEditable
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
setState(() => _chain = value);
|
||||
_emitIfValid();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
TextFormField(
|
||||
readOnly: !widget.isEditable,
|
||||
controller: _tokenCtrl,
|
||||
decoration: getInputDecoration(context, l10n.tokenSymbolLabel, widget.isEditable),
|
||||
style: getTextFieldStyle(context, widget.isEditable),
|
||||
validator: (_) => _validateToken(l10n),
|
||||
),
|
||||
TextFormField(
|
||||
readOnly: !widget.isEditable,
|
||||
controller: _contractCtrl,
|
||||
decoration: getInputDecoration(context, l10n.contractAddressLabel, widget.isEditable),
|
||||
style: getTextFieldStyle(context, widget.isEditable),
|
||||
),
|
||||
TextFormField(
|
||||
readOnly: !widget.isEditable,
|
||||
controller: _memoCtrl,
|
||||
decoration: getInputDecoration(context, l10n.memoLabel, widget.isEditable),
|
||||
style: getTextFieldStyle(context, widget.isEditable),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_addressCtrl.dispose();
|
||||
_tokenCtrl.dispose();
|
||||
_contractCtrl.dispose();
|
||||
_memoCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class PaymentMethodForm extends StatelessWidget {
|
||||
initialData: initialData as RussianBankAccountPaymentMethod?,
|
||||
isEditable: isEditable,
|
||||
),
|
||||
PaymentType.cryptoAddress => CryptoAddressForm(
|
||||
PaymentType.externalChain => CryptoAddressForm(
|
||||
onChanged: onChanged,
|
||||
initialData: initialData as CryptoAddressPaymentMethod?,
|
||||
isEditable: isEditable,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user