+ quotation provider
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
|
||||
|
||||
Reference in New Issue
Block a user