+source +destination in payments
This commit is contained in:
@@ -14,8 +14,11 @@ import (
|
|||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
|
"github.com/tech/sendico/server/interface/api/srequest"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,16 +70,24 @@ type PaymentQuotes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Payment struct {
|
type Payment struct {
|
||||||
PaymentRef string `json:"paymentRef,omitempty"`
|
PaymentRef string `json:"paymentRef,omitempty"`
|
||||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
State string `json:"state,omitempty"`
|
||||||
State string `json:"state,omitempty"`
|
Comment string `json:"comment,omitempty"`
|
||||||
Comment string `json:"comment,omitempty"`
|
Source *PaymentEndpoint `json:"source"`
|
||||||
FailureCode string `json:"failureCode,omitempty"`
|
Destination *PaymentEndpoint `json:"destination"`
|
||||||
FailureReason string `json:"failureReason,omitempty"`
|
FailureCode string `json:"failureCode,omitempty"`
|
||||||
Operations []PaymentOperation `json:"operations,omitempty"`
|
FailureReason string `json:"failureReason,omitempty"`
|
||||||
LastQuote *PaymentQuote `json:"lastQuote,omitempty"`
|
Operations []PaymentOperation `json:"operations,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
LastQuote *PaymentQuote `json:"lastQuote,omitempty"`
|
||||||
Meta map[string]string `json:"meta,omitempty"`
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||||
|
Meta map[string]string `json:"meta,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentEndpoint struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
PaymentMethodRef string `json:"paymentMethodRef,omitempty"`
|
||||||
|
PayeeRef string `json:"payeeRef,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentOperation struct {
|
type PaymentOperation struct {
|
||||||
@@ -290,22 +301,257 @@ func toPayment(p *orchestrationv2.Payment) *Payment {
|
|||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
intent := p.GetIntentSnapshot()
|
||||||
operations := toUserVisibleOperations(p.GetStepExecutions(), p.GetQuoteSnapshot())
|
operations := toUserVisibleOperations(p.GetStepExecutions(), p.GetQuoteSnapshot())
|
||||||
failureCode, failureReason := firstFailure(operations)
|
failureCode, failureReason := firstFailure(operations)
|
||||||
return &Payment{
|
return &Payment{
|
||||||
PaymentRef: p.GetPaymentRef(),
|
PaymentRef: p.GetPaymentRef(),
|
||||||
State: enumJSONName(p.GetState().String()),
|
State: enumJSONName(p.GetState().String()),
|
||||||
Comment: strings.TrimSpace(p.GetIntentSnapshot().GetComment()),
|
Comment: strings.TrimSpace(intent.GetComment()),
|
||||||
FailureCode: failureCode,
|
Source: toPaymentEndpoint(intent.GetSource()),
|
||||||
FailureReason: failureReason,
|
Destination: toPaymentEndpoint(intent.GetDestination()),
|
||||||
Operations: operations,
|
FailureCode: failureCode,
|
||||||
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
|
FailureReason: failureReason,
|
||||||
CreatedAt: timestampAsTime(p.GetCreatedAt()),
|
Operations: operations,
|
||||||
Meta: paymentMeta(p),
|
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
|
||||||
IdempotencyKey: "",
|
CreatedAt: timestampAsTime(p.GetCreatedAt()),
|
||||||
|
Meta: paymentMeta(p),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toPaymentEndpoint(endpoint *endpointv1.PaymentEndpoint) *PaymentEndpoint {
|
||||||
|
if endpoint == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if paymentMethodRef := strings.TrimSpace(endpoint.GetPaymentMethodRef()); paymentMethodRef != "" {
|
||||||
|
return &PaymentEndpoint{PaymentMethodRef: paymentMethodRef}
|
||||||
|
}
|
||||||
|
if payeeRef := strings.TrimSpace(endpoint.GetPayeeRef()); payeeRef != "" {
|
||||||
|
return &PaymentEndpoint{PayeeRef: payeeRef}
|
||||||
|
}
|
||||||
|
method := endpoint.GetPaymentMethod()
|
||||||
|
if method == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &PaymentEndpoint{
|
||||||
|
Type: paymentEndpointType(method.GetType()),
|
||||||
|
Data: paymentEndpointData(method),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func paymentEndpointType(methodType endpointv1.PaymentMethodType) string {
|
||||||
|
switch methodType {
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
|
||||||
|
return string(srequest.EndpointTypeIBAN)
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
|
||||||
|
return string(srequest.EndpointTypeCard)
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
|
||||||
|
return string(srequest.EndpointTypeCardToken)
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
|
||||||
|
return string(srequest.EndpointTypeBankAccount)
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
|
||||||
|
return string(srequest.EndpointTypeWallet)
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
|
||||||
|
return string(srequest.EndpointTypeExternalChain)
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
|
||||||
|
return string(srequest.EndpointTypeLedger)
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_ACCOUNT:
|
||||||
|
return "account"
|
||||||
|
default:
|
||||||
|
return "unspecified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func paymentEndpointData(method *endpointv1.PaymentMethod) any {
|
||||||
|
if method == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch method.GetType() {
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
|
||||||
|
type ledgerMethodData struct {
|
||||||
|
LedgerAccountRef string `bson:"ledgerAccountRef"`
|
||||||
|
ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty"`
|
||||||
|
}
|
||||||
|
var payload ledgerMethodData
|
||||||
|
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||||
|
return toRawBSON(method.GetData())
|
||||||
|
}
|
||||||
|
return srequest.LedgerEndpoint{
|
||||||
|
LedgerAccountRef: strings.TrimSpace(payload.LedgerAccountRef),
|
||||||
|
ContraLedgerAccountRef: strings.TrimSpace(payload.ContraLedgerAccountRef),
|
||||||
|
}
|
||||||
|
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
|
||||||
|
type walletMethodData struct {
|
||||||
|
WalletID string `bson:"walletId"`
|
||||||
|
}
|
||||||
|
var payload walletMethodData
|
||||||
|
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||||
|
return toRawBSON(method.GetData())
|
||||||
|
}
|
||||||
|
return srequest.WalletEndpoint{
|
||||||
|
WalletID: strings.TrimSpace(payload.WalletID),
|
||||||
|
}
|
||||||
|
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
|
||||||
|
type cryptoMethodData struct {
|
||||||
|
Currency string `bson:"currency"`
|
||||||
|
Address string `bson:"address"`
|
||||||
|
Network string `bson:"network"`
|
||||||
|
DestinationTag *string `bson:"destinationTag,omitempty"`
|
||||||
|
}
|
||||||
|
var payload cryptoMethodData
|
||||||
|
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||||
|
return toRawBSON(method.GetData())
|
||||||
|
}
|
||||||
|
endpoint := srequest.ExternalChainEndpoint{
|
||||||
|
Asset: &srequest.Asset{
|
||||||
|
Chain: parseChainNetwork(payload.Network),
|
||||||
|
TokenSymbol: strings.ToUpper(strings.TrimSpace(payload.Currency)),
|
||||||
|
},
|
||||||
|
Address: strings.TrimSpace(payload.Address),
|
||||||
|
}
|
||||||
|
if memo := strings.TrimSpace(strPtr(payload.DestinationTag)); memo != "" {
|
||||||
|
endpoint.Memo = memo
|
||||||
|
}
|
||||||
|
if endpoint.Asset.Chain == srequest.ChainNetworkUnspecified && endpoint.Asset.TokenSymbol == "" {
|
||||||
|
endpoint.Asset = nil
|
||||||
|
}
|
||||||
|
return endpoint
|
||||||
|
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
|
||||||
|
type cardMethodData struct {
|
||||||
|
Pan string `bson:"pan"`
|
||||||
|
FirstName string `bson:"firstName"`
|
||||||
|
LastName string `bson:"lastName"`
|
||||||
|
ExpMonth string `bson:"expMonth"`
|
||||||
|
ExpYear string `bson:"expYear"`
|
||||||
|
Country string `bson:"country,omitempty"`
|
||||||
|
}
|
||||||
|
var payload cardMethodData
|
||||||
|
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||||
|
return toRawBSON(method.GetData())
|
||||||
|
}
|
||||||
|
return srequest.CardEndpoint{
|
||||||
|
Pan: strings.TrimSpace(payload.Pan),
|
||||||
|
FirstName: strings.TrimSpace(payload.FirstName),
|
||||||
|
LastName: strings.TrimSpace(payload.LastName),
|
||||||
|
ExpMonth: parseUint32(payload.ExpMonth),
|
||||||
|
ExpYear: parseUint32(payload.ExpYear),
|
||||||
|
Country: strings.TrimSpace(payload.Country),
|
||||||
|
}
|
||||||
|
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
|
||||||
|
type cardTokenMethodData struct {
|
||||||
|
Token string `bson:"token"`
|
||||||
|
Last4 string `bson:"last4,omitempty"`
|
||||||
|
}
|
||||||
|
var payload cardTokenMethodData
|
||||||
|
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||||
|
return toRawBSON(method.GetData())
|
||||||
|
}
|
||||||
|
return srequest.CardTokenEndpoint{
|
||||||
|
Token: strings.TrimSpace(payload.Token),
|
||||||
|
MaskedPan: strings.TrimSpace(payload.Last4),
|
||||||
|
}
|
||||||
|
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
|
||||||
|
type bankAccountMethodData struct {
|
||||||
|
RecipientName string `bson:"recipientName"`
|
||||||
|
Inn string `bson:"inn"`
|
||||||
|
Kpp string `bson:"kpp"`
|
||||||
|
BankName string `bson:"bankName"`
|
||||||
|
Bik string `bson:"bik"`
|
||||||
|
AccountNumber string `bson:"accountNumber"`
|
||||||
|
CorrespondentAccount string `bson:"correspondentAccount"`
|
||||||
|
}
|
||||||
|
var payload bankAccountMethodData
|
||||||
|
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||||
|
return toRawBSON(method.GetData())
|
||||||
|
}
|
||||||
|
return srequest.BankAccountEndpoint{
|
||||||
|
RecipientName: strings.TrimSpace(payload.RecipientName),
|
||||||
|
Inn: strings.TrimSpace(payload.Inn),
|
||||||
|
Kpp: strings.TrimSpace(payload.Kpp),
|
||||||
|
BankName: strings.TrimSpace(payload.BankName),
|
||||||
|
Bik: strings.TrimSpace(payload.Bik),
|
||||||
|
AccountNumber: strings.TrimSpace(payload.AccountNumber),
|
||||||
|
CorrespondentAccount: strings.TrimSpace(payload.CorrespondentAccount),
|
||||||
|
}
|
||||||
|
|
||||||
|
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
|
||||||
|
type ibanMethodData struct {
|
||||||
|
IBAN string `bson:"iban"`
|
||||||
|
AccountHolder string `bson:"accountHolder"`
|
||||||
|
BIC *string `bson:"bic,omitempty"`
|
||||||
|
BankName *string `bson:"bankName,omitempty"`
|
||||||
|
}
|
||||||
|
var payload ibanMethodData
|
||||||
|
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||||
|
return toRawBSON(method.GetData())
|
||||||
|
}
|
||||||
|
return srequest.IBANEndpoint{
|
||||||
|
IBAN: strings.TrimSpace(payload.IBAN),
|
||||||
|
AccountHolder: strings.TrimSpace(payload.AccountHolder),
|
||||||
|
BIC: strings.TrimSpace(strPtr(payload.BIC)),
|
||||||
|
BankName: strings.TrimSpace(strPtr(payload.BankName)),
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return toRawBSON(method.GetData())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRawBSON(raw []byte) map[string]any {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var data map[string]any
|
||||||
|
if err := bson.Unmarshal(raw, &data); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseChainNetwork(value string) srequest.ChainNetwork {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "ETHEREUM_MAINNET":
|
||||||
|
return srequest.ChainNetworkEthereumMainnet
|
||||||
|
case "ARBITRUM_ONE":
|
||||||
|
return srequest.ChainNetworkArbitrumOne
|
||||||
|
case "TRON_MAINNET":
|
||||||
|
return srequest.ChainNetworkTronMainnet
|
||||||
|
case "TRON_NILE":
|
||||||
|
return srequest.ChainNetworkTronNile
|
||||||
|
case "", "UNSPECIFIED":
|
||||||
|
return srequest.ChainNetworkUnspecified
|
||||||
|
default:
|
||||||
|
return srequest.ChainNetwork(strings.ToLower(strings.TrimSpace(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUint32(value string) uint32 {
|
||||||
|
clean := strings.TrimSpace(value)
|
||||||
|
if clean == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseUint(clean, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint32(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func strPtr(v *string) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
|
||||||
func firstFailure(operations []PaymentOperation) (string, string) {
|
func firstFailure(operations []PaymentOperation) (string, string) {
|
||||||
for _, op := range operations {
|
for _, op := range operations {
|
||||||
if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" {
|
if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" {
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import (
|
|||||||
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||||
|
"github.com/tech/sendico/server/interface/api/srequest"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestToUserVisibleOperationsFiltersByVisibility(t *testing.T) {
|
func TestToUserVisibleOperationsFiltersByVisibility(t *testing.T) {
|
||||||
@@ -137,6 +140,125 @@ func TestToPaymentMapsIntentComment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToPaymentMapsSourceAndDestination(t *testing.T) {
|
||||||
|
sourceRaw, err := bson.Marshal(struct {
|
||||||
|
WalletID string `bson:"walletId"`
|
||||||
|
}{
|
||||||
|
WalletID: "wallet-src-1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal source method data: %v", err)
|
||||||
|
}
|
||||||
|
destinationRaw, err := bson.Marshal(struct {
|
||||||
|
Currency string `bson:"currency"`
|
||||||
|
Address string `bson:"address"`
|
||||||
|
Network string `bson:"network"`
|
||||||
|
DestinationTag *string `bson:"destinationTag,omitempty"`
|
||||||
|
}{
|
||||||
|
Currency: "USDT",
|
||||||
|
Address: "TXabc",
|
||||||
|
Network: "TRON_MAINNET",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal destination method data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dto := toPayment(&orchestrationv2.Payment{
|
||||||
|
PaymentRef: "pay-src-dst",
|
||||||
|
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
|
||||||
|
IntentSnapshot: "ationv2.QuoteIntent{
|
||||||
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
|
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||||
|
PaymentMethod: &endpointv1.PaymentMethod{
|
||||||
|
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
|
||||||
|
Data: sourceRaw,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &endpointv1.PaymentEndpoint{
|
||||||
|
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||||
|
PaymentMethod: &endpointv1.PaymentMethod{
|
||||||
|
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS,
|
||||||
|
Data: destinationRaw,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if dto == nil {
|
||||||
|
t.Fatal("expected non-nil payment dto")
|
||||||
|
}
|
||||||
|
if dto.Source == nil {
|
||||||
|
t.Fatal("expected source endpoint")
|
||||||
|
}
|
||||||
|
if got, want := dto.Source.Type, string(srequest.EndpointTypeWallet); got != want {
|
||||||
|
t.Fatalf("source type mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
sourceEndpoint, ok := dto.Source.Data.(srequest.WalletEndpoint)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("source endpoint payload type mismatch: got=%T", dto.Source.Data)
|
||||||
|
}
|
||||||
|
if got, want := sourceEndpoint.WalletID, "wallet-src-1"; got != want {
|
||||||
|
t.Fatalf("source wallet id mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if dto.Destination == nil {
|
||||||
|
t.Fatal("expected destination endpoint")
|
||||||
|
}
|
||||||
|
if got, want := dto.Destination.Type, string(srequest.EndpointTypeExternalChain); got != want {
|
||||||
|
t.Fatalf("destination type mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
destinationEndpoint, ok := dto.Destination.Data.(srequest.ExternalChainEndpoint)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("destination endpoint payload type mismatch: got=%T", dto.Destination.Data)
|
||||||
|
}
|
||||||
|
if got, want := destinationEndpoint.Address, "TXabc"; got != want {
|
||||||
|
t.Fatalf("destination address mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if destinationEndpoint.Asset == nil {
|
||||||
|
t.Fatal("expected destination asset")
|
||||||
|
}
|
||||||
|
if got, want := destinationEndpoint.Asset.TokenSymbol, "USDT"; got != want {
|
||||||
|
t.Fatalf("destination token mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := destinationEndpoint.Asset.Chain, srequest.ChainNetworkTronMainnet; got != want {
|
||||||
|
t.Fatalf("destination chain mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToPaymentMapsEndpointRefs(t *testing.T) {
|
||||||
|
dto := toPayment(&orchestrationv2.Payment{
|
||||||
|
PaymentRef: "pay-refs",
|
||||||
|
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
|
||||||
|
IntentSnapshot: "ationv2.QuoteIntent{
|
||||||
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
|
Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{
|
||||||
|
PaymentMethodRef: "pm-123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &endpointv1.PaymentEndpoint{
|
||||||
|
Source: &endpointv1.PaymentEndpoint_PayeeRef{
|
||||||
|
PayeeRef: "payee-777",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if dto == nil {
|
||||||
|
t.Fatal("expected non-nil payment dto")
|
||||||
|
}
|
||||||
|
if dto.Source == nil {
|
||||||
|
t.Fatal("expected source endpoint")
|
||||||
|
}
|
||||||
|
if got, want := dto.Source.PaymentMethodRef, "pm-123"; got != want {
|
||||||
|
t.Fatalf("source payment_method_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if dto.Destination == nil {
|
||||||
|
t.Fatal("expected destination endpoint")
|
||||||
|
}
|
||||||
|
if got, want := dto.Destination.PayeeRef, "payee-777"; got != want {
|
||||||
|
t.Fatalf("destination payee_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
|
func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
|
||||||
dto := toPaymentQuote("ationv2.PaymentQuote{
|
dto := toPaymentQuote("ationv2.PaymentQuote{
|
||||||
QuoteRef: "quote-1",
|
QuoteRef: "quote-1",
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import 'package:json_annotation/json_annotation.dart';
|
|||||||
|
|
||||||
import 'package:pshared/data/dto/payment/operation.dart';
|
import 'package:pshared/data/dto/payment/operation.dart';
|
||||||
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
||||||
|
import 'package:pshared/data/dto/payment/response_endpoint.dart';
|
||||||
|
|
||||||
part 'payment.g.dart';
|
part 'payment.g.dart';
|
||||||
|
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class PaymentDTO {
|
class PaymentDTO {
|
||||||
final String? paymentRef;
|
final String? paymentRef;
|
||||||
final String? idempotencyKey;
|
|
||||||
final String? state;
|
final String? state;
|
||||||
|
final PaymentResponseEndpointDTO? source;
|
||||||
|
final PaymentResponseEndpointDTO? destination;
|
||||||
final String? failureCode;
|
final String? failureCode;
|
||||||
final String? failureReason;
|
final String? failureReason;
|
||||||
final List<PaymentOperationDTO> operations;
|
final List<PaymentOperationDTO> operations;
|
||||||
@@ -20,8 +21,9 @@ class PaymentDTO {
|
|||||||
|
|
||||||
const PaymentDTO({
|
const PaymentDTO({
|
||||||
this.paymentRef,
|
this.paymentRef,
|
||||||
this.idempotencyKey,
|
|
||||||
this.state,
|
this.state,
|
||||||
|
this.source,
|
||||||
|
this.destination,
|
||||||
this.failureCode,
|
this.failureCode,
|
||||||
this.failureReason,
|
this.failureReason,
|
||||||
this.operations = const <PaymentOperationDTO>[],
|
this.operations = const <PaymentOperationDTO>[],
|
||||||
|
|||||||
22
frontend/pshared/lib/data/dto/payment/response_endpoint.dart
Normal file
22
frontend/pshared/lib/data/dto/payment/response_endpoint.dart
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'response_endpoint.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class PaymentResponseEndpointDTO {
|
||||||
|
final String? type;
|
||||||
|
final Map<String, dynamic>? data;
|
||||||
|
final String? paymentMethodRef;
|
||||||
|
final String? payeeRef;
|
||||||
|
|
||||||
|
const PaymentResponseEndpointDTO({
|
||||||
|
this.type,
|
||||||
|
this.data,
|
||||||
|
this.paymentMethodRef,
|
||||||
|
this.payeeRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PaymentResponseEndpointDTO.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PaymentResponseEndpointDTOFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$PaymentResponseEndpointDTOToJson(this);
|
||||||
|
}
|
||||||
@@ -2,20 +2,24 @@ import 'package:pshared/data/dto/payment/card.dart';
|
|||||||
import 'package:pshared/data/dto/payment/card_token.dart';
|
import 'package:pshared/data/dto/payment/card_token.dart';
|
||||||
import 'package:pshared/data/dto/payment/endpoint.dart';
|
import 'package:pshared/data/dto/payment/endpoint.dart';
|
||||||
import 'package:pshared/data/dto/payment/external_chain.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/ledger.dart';
|
||||||
import 'package:pshared/data/dto/payment/managed_wallet.dart';
|
import 'package:pshared/data/dto/payment/managed_wallet.dart';
|
||||||
|
import 'package:pshared/data/dto/payment/russian_bank.dart';
|
||||||
|
import 'package:pshared/data/dto/payment/wallet.dart';
|
||||||
import 'package:pshared/data/mapper/payment/asset.dart';
|
import 'package:pshared/data/mapper/payment/asset.dart';
|
||||||
import 'package:pshared/data/mapper/payment/enums.dart';
|
import 'package:pshared/data/mapper/payment/enums.dart';
|
||||||
import 'package:pshared/data/mapper/payment/type.dart';
|
|
||||||
import 'package:pshared/models/payment/methods/card.dart';
|
import 'package:pshared/models/payment/methods/card.dart';
|
||||||
import 'package:pshared/models/payment/methods/card_token.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/crypto_address.dart';
|
||||||
import 'package:pshared/models/payment/methods/data.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/ledger.dart';
|
||||||
import 'package:pshared/models/payment/methods/managed_wallet.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/wallet.dart';
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:pshared/models/payment/type.dart';
|
||||||
|
|
||||||
|
|
||||||
extension PaymentMethodDataEndpointMapper on PaymentMethodData {
|
extension PaymentMethodDataEndpointMapper on PaymentMethodData {
|
||||||
PaymentEndpointDTO toDTO() {
|
PaymentEndpointDTO toDTO() {
|
||||||
final metadata = this.metadata;
|
final metadata = this.metadata;
|
||||||
@@ -76,8 +80,40 @@ extension PaymentMethodDataEndpointMapper on PaymentMethodData {
|
|||||||
).toJson(),
|
).toJson(),
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
);
|
);
|
||||||
default:
|
case PaymentType.wallet:
|
||||||
throw UnsupportedError('Unsupported payment endpoint type: $type');
|
final payload = this as WalletPaymentMethod;
|
||||||
|
return PaymentEndpointDTO(
|
||||||
|
type: endpointTypeToValue(type),
|
||||||
|
data: WalletPaymentDataDTO(walletId: payload.walletId).toJson(),
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
case PaymentType.bankAccount:
|
||||||
|
final payload = this as RussianBankAccountPaymentMethod;
|
||||||
|
return PaymentEndpointDTO(
|
||||||
|
type: endpointTypeToValue(type),
|
||||||
|
data: RussianBankAccountPaymentDataDTO(
|
||||||
|
recipientName: payload.recipientName,
|
||||||
|
inn: payload.inn,
|
||||||
|
kpp: payload.kpp,
|
||||||
|
bankName: payload.bankName,
|
||||||
|
bik: payload.bik,
|
||||||
|
accountNumber: payload.accountNumber,
|
||||||
|
correspondentAccount: payload.correspondentAccount,
|
||||||
|
).toJson(),
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
case PaymentType.iban:
|
||||||
|
final payload = this as IbanPaymentMethod;
|
||||||
|
return PaymentEndpointDTO(
|
||||||
|
type: endpointTypeToValue(type),
|
||||||
|
data: IbanPaymentDataDTO(
|
||||||
|
iban: payload.iban,
|
||||||
|
accountHolder: payload.accountHolder,
|
||||||
|
bic: payload.bic,
|
||||||
|
bankName: payload.bankName,
|
||||||
|
).toJson(),
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,14 +163,40 @@ extension PaymentEndpointDTOMapper on PaymentEndpointDTO {
|
|||||||
maskedPan: payload.maskedPan,
|
maskedPan: payload.maskedPan,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
);
|
);
|
||||||
default:
|
case PaymentType.wallet:
|
||||||
throw UnsupportedError('Unsupported payment endpoint type: ${paymentTypeFromValue(type)}');
|
final payload = WalletPaymentDataDTO.fromJson(data);
|
||||||
|
return WalletPaymentMethod(
|
||||||
|
walletId: payload.walletId,
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
case PaymentType.bankAccount:
|
||||||
|
final payload = RussianBankAccountPaymentDataDTO.fromJson(data);
|
||||||
|
return RussianBankAccountPaymentMethod(
|
||||||
|
recipientName: payload.recipientName,
|
||||||
|
inn: payload.inn,
|
||||||
|
kpp: payload.kpp,
|
||||||
|
bankName: payload.bankName,
|
||||||
|
bik: payload.bik,
|
||||||
|
accountNumber: payload.accountNumber,
|
||||||
|
correspondentAccount: payload.correspondentAccount,
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
case PaymentType.iban:
|
||||||
|
final payload = IbanPaymentDataDTO.fromJson(data);
|
||||||
|
return IbanPaymentMethod(
|
||||||
|
iban: payload.iban,
|
||||||
|
accountHolder: payload.accountHolder,
|
||||||
|
bic: payload.bic,
|
||||||
|
bankName: payload.bankName,
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PaymentType _resolveEndpointType(String type, Map<String, dynamic> data) {
|
PaymentType _resolveEndpointType(String type, Map<String, dynamic> data) {
|
||||||
if (type == 'card' && (data.containsKey('token') || data.containsKey('masked_pan'))) {
|
if (type == 'card' &&
|
||||||
|
(data.containsKey('token') || data.containsKey('masked_pan'))) {
|
||||||
return PaymentType.cardToken;
|
return PaymentType.cardToken;
|
||||||
}
|
}
|
||||||
return endpointTypeFromValue(type);
|
return endpointTypeFromValue(type);
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import 'package:pshared/data/dto/payment/payment.dart';
|
import 'package:pshared/data/dto/payment/payment.dart';
|
||||||
import 'package:pshared/data/mapper/payment/operation.dart';
|
import 'package:pshared/data/mapper/payment/operation.dart';
|
||||||
import 'package:pshared/data/mapper/payment/quote.dart';
|
import 'package:pshared/data/mapper/payment/quote.dart';
|
||||||
|
import 'package:pshared/data/mapper/payment/response_endpoint.dart';
|
||||||
import 'package:pshared/models/payment/payment.dart';
|
import 'package:pshared/models/payment/payment.dart';
|
||||||
import 'package:pshared/models/payment/state.dart';
|
import 'package:pshared/models/payment/state.dart';
|
||||||
|
|
||||||
|
|
||||||
extension PaymentDTOMapper on PaymentDTO {
|
extension PaymentDTOMapper on PaymentDTO {
|
||||||
Payment toDomain() => Payment(
|
Payment toDomain() => Payment(
|
||||||
paymentRef: paymentRef,
|
paymentRef: paymentRef,
|
||||||
idempotencyKey: idempotencyKey,
|
|
||||||
state: state,
|
state: state,
|
||||||
|
source: source?.toDomain(),
|
||||||
|
destination: destination?.toDomain(),
|
||||||
orchestrationState: paymentOrchestrationStateFromValue(state),
|
orchestrationState: paymentOrchestrationStateFromValue(state),
|
||||||
failureCode: failureCode,
|
failureCode: failureCode,
|
||||||
failureReason: failureReason,
|
failureReason: failureReason,
|
||||||
@@ -23,8 +24,9 @@ extension PaymentDTOMapper on PaymentDTO {
|
|||||||
extension PaymentMapper on Payment {
|
extension PaymentMapper on Payment {
|
||||||
PaymentDTO toDTO() => PaymentDTO(
|
PaymentDTO toDTO() => PaymentDTO(
|
||||||
paymentRef: paymentRef,
|
paymentRef: paymentRef,
|
||||||
idempotencyKey: idempotencyKey,
|
|
||||||
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
||||||
|
source: source?.toDTO(),
|
||||||
|
destination: destination?.toDTO(),
|
||||||
failureCode: failureCode,
|
failureCode: failureCode,
|
||||||
failureReason: failureReason,
|
failureReason: failureReason,
|
||||||
operations: operations.map((item) => item.toDTO()).toList(),
|
operations: operations.map((item) => item.toDTO()).toList(),
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:pshared/data/dto/payment/endpoint.dart';
|
||||||
|
import 'package:pshared/data/dto/payment/response_endpoint.dart';
|
||||||
|
import 'package:pshared/data/mapper/payment/payment.dart';
|
||||||
|
import 'package:pshared/models/payment/endpoint.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
|
|
||||||
|
extension PaymentResponseEndpointDTOMapper on PaymentResponseEndpointDTO {
|
||||||
|
PaymentEndpoint toDomain() {
|
||||||
|
final normalizedType = _normalize(type);
|
||||||
|
final normalizedData = _cloneData(data);
|
||||||
|
|
||||||
|
return PaymentEndpoint(
|
||||||
|
method: _tryParseMethod(normalizedType, normalizedData),
|
||||||
|
paymentMethodRef: _normalize(paymentMethodRef),
|
||||||
|
payeeRef: _normalize(payeeRef),
|
||||||
|
type: normalizedType,
|
||||||
|
rawData: normalizedData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PaymentEndpointMapper on PaymentEndpoint {
|
||||||
|
PaymentResponseEndpointDTO toDTO() {
|
||||||
|
final methodData = method;
|
||||||
|
if (methodData != null) {
|
||||||
|
final endpoint = methodData.toDTO();
|
||||||
|
return PaymentResponseEndpointDTO(
|
||||||
|
type: endpoint.type,
|
||||||
|
data: endpoint.data,
|
||||||
|
paymentMethodRef: _normalize(paymentMethodRef),
|
||||||
|
payeeRef: _normalize(payeeRef),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaymentResponseEndpointDTO(
|
||||||
|
type: _normalize(type),
|
||||||
|
data: _cloneData(rawData),
|
||||||
|
paymentMethodRef: _normalize(paymentMethodRef),
|
||||||
|
payeeRef: _normalize(payeeRef),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentMethodData? _tryParseMethod(String? type, Map<String, dynamic>? data) {
|
||||||
|
if (type == null || data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return PaymentEndpointDTO(type: type, data: data).toDomain();
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _normalize(String? value) {
|
||||||
|
final trimmed = value?.trim();
|
||||||
|
if (trimmed == null || trimmed.isEmpty) return null;
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? _cloneData(Map<String, dynamic>? data) {
|
||||||
|
if (data == null) return null;
|
||||||
|
if (data.isEmpty) return <String, dynamic>{};
|
||||||
|
return Map<String, dynamic>.from(data);
|
||||||
|
}
|
||||||
17
frontend/pshared/lib/models/payment/endpoint.dart
Normal file
17
frontend/pshared/lib/models/payment/endpoint.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
|
|
||||||
|
class PaymentEndpoint {
|
||||||
|
final PaymentMethodData? method;
|
||||||
|
final String? paymentMethodRef;
|
||||||
|
final String? payeeRef;
|
||||||
|
final String? type;
|
||||||
|
final Map<String, dynamic>? rawData;
|
||||||
|
|
||||||
|
const PaymentEndpoint({
|
||||||
|
required this.method,
|
||||||
|
required this.paymentMethodRef,
|
||||||
|
required this.payeeRef,
|
||||||
|
required this.type,
|
||||||
|
required this.rawData,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import 'package:pshared/models/payment/endpoint.dart';
|
||||||
import 'package:pshared/models/payment/execution_operation.dart';
|
import 'package:pshared/models/payment/execution_operation.dart';
|
||||||
import 'package:pshared/models/payment/quote/quote.dart';
|
import 'package:pshared/models/payment/quote/quote.dart';
|
||||||
import 'package:pshared/models/payment/state.dart';
|
import 'package:pshared/models/payment/state.dart';
|
||||||
|
|
||||||
class Payment {
|
class Payment {
|
||||||
final String? paymentRef;
|
final String? paymentRef;
|
||||||
final String? idempotencyKey;
|
|
||||||
final String? state;
|
final String? state;
|
||||||
|
final PaymentEndpoint? source;
|
||||||
|
final PaymentEndpoint? destination;
|
||||||
final PaymentOrchestrationState orchestrationState;
|
final PaymentOrchestrationState orchestrationState;
|
||||||
final String? failureCode;
|
final String? failureCode;
|
||||||
final String? failureReason;
|
final String? failureReason;
|
||||||
@@ -16,8 +18,9 @@ class Payment {
|
|||||||
|
|
||||||
const Payment({
|
const Payment({
|
||||||
required this.paymentRef,
|
required this.paymentRef,
|
||||||
required this.idempotencyKey,
|
|
||||||
required this.state,
|
required this.state,
|
||||||
|
required this.source,
|
||||||
|
required this.destination,
|
||||||
required this.orchestrationState,
|
required this.orchestrationState,
|
||||||
required this.failureCode,
|
required this.failureCode,
|
||||||
required this.failureReason,
|
required this.failureReason,
|
||||||
|
|||||||
@@ -254,9 +254,7 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? _paymentKey(Payment payment) {
|
String? _paymentKey(Payment payment) {
|
||||||
final ref = _normalize(payment.paymentRef);
|
return _normalize(payment.paymentRef);
|
||||||
if (ref != null) return ref;
|
|
||||||
return _normalize(payment.idempotencyKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String>? _normalizeStates(List<String>? states) {
|
List<String>? _normalizeStates(List<String>? states) {
|
||||||
|
|||||||
@@ -79,8 +79,6 @@ class PaymentsUpdatesProvider extends ChangeNotifier {
|
|||||||
String? _key(Payment payment) {
|
String? _key(Payment payment) {
|
||||||
final ref = payment.paymentRef?.trim();
|
final ref = payment.paymentRef?.trim();
|
||||||
if (ref != null && ref.isNotEmpty) return ref;
|
if (ref != null && ref.isNotEmpty) return ref;
|
||||||
final idempotency = payment.idempotencyKey?.trim();
|
|
||||||
if (idempotency != null && idempotency.isNotEmpty) return idempotency;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ void main() {
|
|||||||
test('isPending and isTerminal are derived from typed state', () {
|
test('isPending and isTerminal are derived from typed state', () {
|
||||||
const created = Payment(
|
const created = Payment(
|
||||||
paymentRef: 'p-1',
|
paymentRef: 'p-1',
|
||||||
idempotencyKey: 'idem-1',
|
|
||||||
state: 'orchestration_state_created',
|
state: 'orchestration_state_created',
|
||||||
|
source: null,
|
||||||
|
destination: null,
|
||||||
orchestrationState: PaymentOrchestrationState.created,
|
orchestrationState: PaymentOrchestrationState.created,
|
||||||
failureCode: null,
|
failureCode: null,
|
||||||
failureReason: null,
|
failureReason: null,
|
||||||
@@ -76,8 +77,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
const settled = Payment(
|
const settled = Payment(
|
||||||
paymentRef: 'p-2',
|
paymentRef: 'p-2',
|
||||||
idempotencyKey: 'idem-2',
|
|
||||||
state: 'orchestration_state_settled',
|
state: 'orchestration_state_settled',
|
||||||
|
source: null,
|
||||||
|
destination: null,
|
||||||
orchestrationState: PaymentOrchestrationState.settled,
|
orchestrationState: PaymentOrchestrationState.settled,
|
||||||
failureCode: null,
|
failureCode: null,
|
||||||
failureReason: null,
|
failureReason: null,
|
||||||
@@ -96,8 +98,9 @@ void main() {
|
|||||||
test('isFailure handles both explicit code and failed state', () {
|
test('isFailure handles both explicit code and failed state', () {
|
||||||
const withFailureCode = Payment(
|
const withFailureCode = Payment(
|
||||||
paymentRef: 'p-3',
|
paymentRef: 'p-3',
|
||||||
idempotencyKey: 'idem-3',
|
|
||||||
state: 'orchestration_state_executing',
|
state: 'orchestration_state_executing',
|
||||||
|
source: null,
|
||||||
|
destination: null,
|
||||||
orchestrationState: PaymentOrchestrationState.executing,
|
orchestrationState: PaymentOrchestrationState.executing,
|
||||||
failureCode: 'failure_ledger',
|
failureCode: 'failure_ledger',
|
||||||
failureReason: 'ledger failed',
|
failureReason: 'ledger failed',
|
||||||
@@ -108,8 +111,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
const failedState = Payment(
|
const failedState = Payment(
|
||||||
paymentRef: 'p-4',
|
paymentRef: 'p-4',
|
||||||
idempotencyKey: 'idem-4',
|
|
||||||
state: 'orchestration_state_failed',
|
state: 'orchestration_state_failed',
|
||||||
|
source: null,
|
||||||
|
destination: null,
|
||||||
orchestrationState: PaymentOrchestrationState.failed,
|
orchestrationState: PaymentOrchestrationState.failed,
|
||||||
failureCode: null,
|
failureCode: null,
|
||||||
failureReason: null,
|
failureReason: null,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:test/test.dart';
|
|||||||
import 'package:pshared/api/requests/payment/initiate.dart';
|
import 'package:pshared/api/requests/payment/initiate.dart';
|
||||||
import 'package:pshared/api/requests/payment/initiate_payments.dart';
|
import 'package:pshared/api/requests/payment/initiate_payments.dart';
|
||||||
import 'package:pshared/api/requests/payment/quote.dart';
|
import 'package:pshared/api/requests/payment/quote.dart';
|
||||||
|
import 'package:pshared/api/responses/payment/payment.dart';
|
||||||
import 'package:pshared/api/responses/payment/quotation.dart';
|
import 'package:pshared/api/responses/payment/quotation.dart';
|
||||||
import 'package:pshared/data/dto/money.dart';
|
import 'package:pshared/data/dto/money.dart';
|
||||||
import 'package:pshared/data/dto/payment/currency_pair.dart';
|
import 'package:pshared/data/dto/payment/currency_pair.dart';
|
||||||
@@ -12,11 +13,13 @@ import 'package:pshared/data/dto/payment/endpoint.dart';
|
|||||||
import 'package:pshared/data/dto/payment/intent/fx.dart';
|
import 'package:pshared/data/dto/payment/intent/fx.dart';
|
||||||
import 'package:pshared/data/dto/payment/intent/payment.dart';
|
import 'package:pshared/data/dto/payment/intent/payment.dart';
|
||||||
import 'package:pshared/data/mapper/payment/payment.dart';
|
import 'package:pshared/data/mapper/payment/payment.dart';
|
||||||
|
import 'package:pshared/data/mapper/payment/payment_response.dart';
|
||||||
import 'package:pshared/models/payment/asset.dart';
|
import 'package:pshared/models/payment/asset.dart';
|
||||||
import 'package:pshared/models/payment/chain_network.dart';
|
import 'package:pshared/models/payment/chain_network.dart';
|
||||||
import 'package:pshared/models/payment/methods/card_token.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/crypto_address.dart';
|
||||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Payment request DTO contract', () {
|
group('Payment request DTO contract', () {
|
||||||
@@ -185,5 +188,38 @@ void main() {
|
|||||||
expect(json.containsKey('intentRef'), isFalse);
|
expect(json.containsKey('intentRef'), isFalse);
|
||||||
expect(json.containsKey('intentRefs'), isFalse);
|
expect(json.containsKey('intentRefs'), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'payment response parses source and destination endpoint snapshots',
|
||||||
|
() {
|
||||||
|
final response = PaymentResponse.fromJson({
|
||||||
|
'accessToken': {
|
||||||
|
'token': 'token',
|
||||||
|
'expiration': '2026-02-25T00:00:00Z',
|
||||||
|
},
|
||||||
|
'payment': {
|
||||||
|
'paymentRef': 'pay-1',
|
||||||
|
'state': 'orchestration_state_created',
|
||||||
|
'source': {
|
||||||
|
'type': 'wallet',
|
||||||
|
'data': {'walletId': 'wallet-1'},
|
||||||
|
},
|
||||||
|
'destination': {'paymentMethodRef': 'pm-123'},
|
||||||
|
'operations': [],
|
||||||
|
'meta': {'quotationRef': 'quote-1'},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
final payment = response.payment.toDomain();
|
||||||
|
expect(payment.paymentRef, equals('pay-1'));
|
||||||
|
expect(payment.source, isNotNull);
|
||||||
|
expect(payment.destination, isNotNull);
|
||||||
|
expect(payment.destination?.paymentMethodRef, equals('pm-123'));
|
||||||
|
expect(payment.source?.method, isA<WalletPaymentMethod>());
|
||||||
|
|
||||||
|
final sourceMethod = payment.source?.method as WalletPaymentMethod;
|
||||||
|
expect(sourceMethod.walletId, equals('wallet-1'));
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class PaymentDetailsController extends ChangeNotifier {
|
|||||||
if (trimmed.isEmpty) return null;
|
if (trimmed.isEmpty) return null;
|
||||||
for (final payment in payments) {
|
for (final payment in payments) {
|
||||||
if (payment.paymentRef == trimmed) return payment;
|
if (payment.paymentRef == trimmed) return payment;
|
||||||
if (payment.idempotencyKey == trimmed) return payment;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,9 @@ OperationItem mapPaymentToOperation(Payment payment) {
|
|||||||
: parseMoneyAmount(settlement.amount);
|
: parseMoneyAmount(settlement.amount);
|
||||||
final toCurrency = settlement?.currency ?? currency;
|
final toCurrency = settlement?.currency ?? currency;
|
||||||
|
|
||||||
final payId =
|
final payId = _firstNonEmpty([payment.paymentRef]) ?? '-';
|
||||||
_firstNonEmpty([payment.paymentRef, payment.idempotencyKey]) ?? '-';
|
|
||||||
final name =
|
final name =
|
||||||
_firstNonEmpty([
|
_firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef]) ?? '-';
|
||||||
payment.lastQuote?.quoteRef,
|
|
||||||
payment.paymentRef,
|
|
||||||
payment.idempotencyKey,
|
|
||||||
]) ??
|
|
||||||
'-';
|
|
||||||
final comment =
|
final comment =
|
||||||
_firstNonEmpty([
|
_firstNonEmpty([
|
||||||
payment.failureReason,
|
payment.failureReason,
|
||||||
|
|||||||
@@ -116,6 +116,51 @@ components:
|
|||||||
description: Unique identifier of the wallet.
|
description: Unique identifier of the wallet.
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
BankAccountEndpoint:
|
||||||
|
description: Domestic bank-account payout endpoint.
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- recipientName
|
||||||
|
- inn
|
||||||
|
- kpp
|
||||||
|
- bankName
|
||||||
|
- bik
|
||||||
|
- accountNumber
|
||||||
|
- correspondentAccount
|
||||||
|
properties:
|
||||||
|
recipientName:
|
||||||
|
type: string
|
||||||
|
inn:
|
||||||
|
type: string
|
||||||
|
kpp:
|
||||||
|
type: string
|
||||||
|
bankName:
|
||||||
|
type: string
|
||||||
|
bik:
|
||||||
|
type: string
|
||||||
|
accountNumber:
|
||||||
|
type: string
|
||||||
|
correspondentAccount:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
IBANEndpoint:
|
||||||
|
description: International bank-account payout endpoint.
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- iban
|
||||||
|
- accountHolder
|
||||||
|
properties:
|
||||||
|
iban:
|
||||||
|
type: string
|
||||||
|
accountHolder:
|
||||||
|
type: string
|
||||||
|
bic:
|
||||||
|
type: string
|
||||||
|
bankName:
|
||||||
|
type: string
|
||||||
|
|
||||||
Endpoint:
|
Endpoint:
|
||||||
description: Polymorphic payment endpoint definition.
|
description: Polymorphic payment endpoint definition.
|
||||||
type: object
|
type: object
|
||||||
@@ -142,6 +187,41 @@ components:
|
|||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
PaymentResponseEndpoint:
|
||||||
|
description: Endpoint snapshot attached to a persisted payment.
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
paymentMethodRef:
|
||||||
|
description: Reference to a stored payment method when endpoint is referenced by ID.
|
||||||
|
type: string
|
||||||
|
payeeRef:
|
||||||
|
description: Reference to a payee profile when endpoint is resolved by payee.
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: Endpoint type for inline endpoint snapshots.
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- ledger
|
||||||
|
- managedWallet
|
||||||
|
- cryptoAddress
|
||||||
|
- card
|
||||||
|
- cardToken
|
||||||
|
- wallet
|
||||||
|
- bankAccount
|
||||||
|
- iban
|
||||||
|
data:
|
||||||
|
description: Inline endpoint payload snapshot; shape depends on `type`.
|
||||||
|
oneOf:
|
||||||
|
- $ref: ./payment.yaml#/components/schemas/LedgerEndpoint
|
||||||
|
- $ref: ./payment.yaml#/components/schemas/ManagedWalletEndpoint
|
||||||
|
- $ref: ./payment.yaml#/components/schemas/ExternalChainEndpoint
|
||||||
|
- $ref: ./payment.yaml#/components/schemas/CardEndpoint
|
||||||
|
- $ref: ./payment.yaml#/components/schemas/CardTokenEndpoint
|
||||||
|
- $ref: ./payment.yaml#/components/schemas/WalletEndpoint
|
||||||
|
- $ref: ./payment.yaml#/components/schemas/BankAccountEndpoint
|
||||||
|
- $ref: ./payment.yaml#/components/schemas/IBANEndpoint
|
||||||
|
|
||||||
Customer:
|
Customer:
|
||||||
description: Customer identity and address attributes for compliance and routing.
|
description: Customer identity and address attributes for compliance and routing.
|
||||||
type: object
|
type: object
|
||||||
@@ -262,6 +342,9 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
|
comment:
|
||||||
|
description: Optional free-form comment attached to the payment intent.
|
||||||
|
type: string
|
||||||
customer:
|
customer:
|
||||||
description: Optional customer information attached to the payment intent.
|
description: Optional customer information attached to the payment intent.
|
||||||
$ref: ./payment.yaml#/components/schemas/Customer
|
$ref: ./payment.yaml#/components/schemas/Customer
|
||||||
@@ -432,12 +515,18 @@ components:
|
|||||||
paymentRef:
|
paymentRef:
|
||||||
description: Unique payment reference identifier.
|
description: Unique payment reference identifier.
|
||||||
type: string
|
type: string
|
||||||
idempotencyKey:
|
|
||||||
description: Idempotency key used to safely deduplicate create requests.
|
|
||||||
type: string
|
|
||||||
state:
|
state:
|
||||||
description: Current lifecycle state of the payment.
|
description: Current lifecycle state of the payment.
|
||||||
$ref: ../../external/payment_state.yaml#/components/schemas/PaymentState
|
$ref: ../../external/payment_state.yaml#/components/schemas/PaymentState
|
||||||
|
comment:
|
||||||
|
description: Optional comment copied from the original payment intent.
|
||||||
|
type: string
|
||||||
|
source:
|
||||||
|
description: Source endpoint snapshot captured from intent.
|
||||||
|
$ref: ./payment.yaml#/components/schemas/PaymentResponseEndpoint
|
||||||
|
destination:
|
||||||
|
description: Destination endpoint snapshot captured from intent.
|
||||||
|
$ref: ./payment.yaml#/components/schemas/PaymentResponseEndpoint
|
||||||
failureCode:
|
failureCode:
|
||||||
description: Failure code set when the payment cannot be completed.
|
description: Failure code set when the payment cannot be completed.
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
Reference in New Issue
Block a user