quotation v2 service
This commit is contained in:
4
Makefile
4
Makefile
@@ -160,8 +160,8 @@ generate-api:
|
|||||||
# Generate Flutter code (json_serializable, etc.)
|
# Generate Flutter code (json_serializable, etc.)
|
||||||
generate-frontend:
|
generate-frontend:
|
||||||
@echo "$(GREEN)Generating Flutter code...$(NC)"
|
@echo "$(GREEN)Generating Flutter code...$(NC)"
|
||||||
@cd frontend/pshared && flutter pub run build_runner build --delete-conflicting-outputs
|
@cd frontend/pshared && dart run build_runner build --delete-conflicting-outputs
|
||||||
@cd frontend/pweb && flutter pub run build_runner build --delete-conflicting-outputs
|
@cd frontend/pweb && dart run build_runner build --delete-conflicting-outputs
|
||||||
@echo "$(GREEN)✅ Flutter code generation complete$(NC)"
|
@echo "$(GREEN)✅ Flutter code generation complete$(NC)"
|
||||||
|
|
||||||
# Clean everything
|
# Clean everything
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ replace github.com/tech/sendico/pkg => ../../pkg
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.1
|
github.com/aws/aws-sdk-go-v2 v1.41.1
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.8
|
github.com/aws/aws-sdk-go-v2/config v1.32.9
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.9
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
|
||||||
github.com/jung-kurt/gofpdf v1.16.2
|
github.com/jung-kurt/gofpdf v1.16.2
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
@@ -31,7 +31,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||||
github.com/aws/smithy-go v1.24.0 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6ce
|
|||||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs=
|
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI=
|
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8 h1:Jp2JYH1lRT3KhX4mshHPvVYsR5qqRec3hGvEarNYoR0=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8/go.mod h1:fZG9tuvyVfxknv1rKibIz3DobRaFw1Poe8IKtXB3XYY=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||||
@@ -34,8 +34,8 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIM
|
|||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
@@ -73,21 +74,36 @@ func canonicalFromSnapshot(
|
|||||||
PricedAt: pricedAt,
|
PricedAt: pricedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
resolvedSettlementMode := resolvedSettlementModeFromSnapshot(snapshot)
|
||||||
return quote_response_mapper_v2.CanonicalQuote{
|
return quote_response_mapper_v2.CanonicalQuote{
|
||||||
QuoteRef: firstNonEmpty(snapshot.QuoteRef, fallbackQuoteRef),
|
QuoteRef: firstNonEmpty(snapshot.QuoteRef, fallbackQuoteRef),
|
||||||
DebitAmount: protoMoneyFromModel(snapshot.DebitAmount),
|
TransferPrincipalAmount: protoMoneyFromModel(snapshot.DebitAmount),
|
||||||
CreditAmount: protoMoneyFromModel(snapshot.ExpectedSettlementAmount),
|
DestinationAmount: protoMoneyFromModel(snapshot.ExpectedSettlementAmount),
|
||||||
TotalCost: protoMoneyFromModel(snapshot.TotalCost),
|
PayerTotalDebitAmount: protoMoneyFromModel(snapshot.TotalCost),
|
||||||
FeeLines: feeLinesToProto(snapshot.FeeLines),
|
FeeLines: feeLinesToProto(snapshot.FeeLines),
|
||||||
FeeRules: feeRulesToProto(snapshot.FeeRules),
|
FeeRules: feeRulesToProto(snapshot.FeeRules),
|
||||||
Route: protoRouteFromModel(snapshot.Route),
|
Route: protoRouteFromModel(snapshot.Route),
|
||||||
Conditions: protoExecutionConditionsFromModel(snapshot.ExecutionConditions),
|
Conditions: protoExecutionConditionsFromModel(snapshot.ExecutionConditions),
|
||||||
FXQuote: protoFXQuoteFromModel(snapshot.FXQuote),
|
FXQuote: protoFXQuoteFromModel(snapshot.FXQuote),
|
||||||
|
ResolvedSettlementMode: resolvedSettlementMode,
|
||||||
|
ResolvedFeeTreatment: resolvedFeeTreatmentForSettlementMode(resolvedSettlementMode),
|
||||||
ExpiresAt: expiresAt,
|
ExpiresAt: expiresAt,
|
||||||
PricedAt: pricedAt,
|
PricedAt: pricedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolvedSettlementModeFromSnapshot(snapshot *model.PaymentQuoteSnapshot) paymentv1.SettlementMode {
|
||||||
|
if snapshot == nil || snapshot.Route == nil || snapshot.Route.Settlement == nil {
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(snapshot.Route.Settlement.Model)) {
|
||||||
|
case "FIX_RECEIVED", "SETTLEMENT_FIX_RECEIVED", "SETTLEMENT_MODE_FIX_RECEIVED":
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED
|
||||||
|
default:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func modelMoneyFromProto(src *moneyv1.Money) *paymenttypes.Money {
|
func modelMoneyFromProto(src *moneyv1.Money) *paymenttypes.Money {
|
||||||
if src == nil {
|
if src == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/payments/storage/model"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func minExpiry(values []time.Time) (time.Time, bool) {
|
func minExpiry(values []time.Time) (time.Time, bool) {
|
||||||
@@ -42,3 +45,23 @@ func cloneProtoMoney(src *moneyv1.Money) *moneyv1.Money {
|
|||||||
Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())),
|
Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolvedSettlementModeFromModel(mode model.SettlementMode) paymentv1.SettlementMode {
|
||||||
|
switch mode {
|
||||||
|
case model.SettlementModeFixReceived:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED
|
||||||
|
case model.SettlementModeFixSource:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
default:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvedFeeTreatmentForSettlementMode(mode paymentv1.SettlementMode) quotationv2.FeeTreatment {
|
||||||
|
switch mode {
|
||||||
|
case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION
|
||||||
|
default:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||||
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"
|
||||||
transferv1 "github.com/tech/sendico/pkg/proto/payments/transfer/v1"
|
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -74,23 +73,23 @@ func TestQuotePayment_USDTToRUB_EndToEnd(t *testing.T) {
|
|||||||
if got := quote.GetBlockReason(); got != quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_UNSPECIFIED {
|
if got := quote.GetBlockReason(); got != quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_UNSPECIFIED {
|
||||||
t.Fatalf("expected empty block reason, got=%s", got.String())
|
t.Fatalf("expected empty block reason, got=%s", got.String())
|
||||||
}
|
}
|
||||||
if got, want := quote.GetDebitAmount().GetAmount(), "100"; got != want {
|
if got, want := quote.GetTransferPrincipalAmount().GetAmount(), "100"; got != want {
|
||||||
t.Fatalf("unexpected debit amount: got=%q want=%q", got, want)
|
t.Fatalf("unexpected principal amount: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := quote.GetDebitAmount().GetCurrency(), "USDT"; got != want {
|
if got, want := quote.GetTransferPrincipalAmount().GetCurrency(), "USDT"; got != want {
|
||||||
t.Fatalf("unexpected debit currency: got=%q want=%q", got, want)
|
t.Fatalf("unexpected principal currency: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := quote.GetCreditAmount().GetAmount(), "9150"; got != want {
|
if got, want := quote.GetDestinationAmount().GetAmount(), "9150"; got != want {
|
||||||
t.Fatalf("unexpected credit amount: got=%q want=%q", got, want)
|
t.Fatalf("unexpected destination amount: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := quote.GetCreditAmount().GetCurrency(), "RUB"; got != want {
|
if got, want := quote.GetDestinationAmount().GetCurrency(), "RUB"; got != want {
|
||||||
t.Fatalf("unexpected credit currency: got=%q want=%q", got, want)
|
t.Fatalf("unexpected destination currency: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := quote.GetTotalCost().GetAmount(), "101.8"; got != want {
|
if got, want := quote.GetPayerTotalDebitAmount().GetAmount(), "101.8"; got != want {
|
||||||
t.Fatalf("unexpected total_cost amount: got=%q want=%q", got, want)
|
t.Fatalf("unexpected payer_total_debit_amount: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := quote.GetTotalCost().GetCurrency(), "USDT"; got != want {
|
if got, want := quote.GetPayerTotalDebitAmount().GetCurrency(), "USDT"; got != want {
|
||||||
t.Fatalf("unexpected total_cost currency: got=%q want=%q", got, want)
|
t.Fatalf("unexpected payer_total_debit_amount currency: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := len(quote.GetFeeLines()), 2; got != want {
|
if got, want := len(quote.GetFeeLines()), 2; got != want {
|
||||||
t.Fatalf("unexpected fee lines count: got=%d want=%d", got, want)
|
t.Fatalf("unexpected fee lines count: got=%d want=%d", got, want)
|
||||||
@@ -113,6 +112,9 @@ func TestQuotePayment_USDTToRUB_EndToEnd(t *testing.T) {
|
|||||||
if got, want := quote.GetFxQuote().GetPair().GetQuote(), "RUB"; got != want {
|
if got, want := quote.GetFxQuote().GetPair().GetQuote(), "RUB"; got != want {
|
||||||
t.Fatalf("unexpected fx quote currency: got=%q want=%q", got, want)
|
t.Fatalf("unexpected fx quote currency: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
|
if got, want := quote.GetFxQuote().GetSide(), fxv1.Side_SELL_BASE_BUY_QUOTE; got != want {
|
||||||
|
t.Fatalf("unexpected fx side: got=%s want=%s", got.String(), want.String())
|
||||||
|
}
|
||||||
if quote.GetRoute() == nil {
|
if quote.GetRoute() == nil {
|
||||||
t.Fatalf("expected route specification")
|
t.Fatalf("expected route specification")
|
||||||
}
|
}
|
||||||
@@ -137,6 +139,9 @@ func TestQuotePayment_USDTToRUB_EndToEnd(t *testing.T) {
|
|||||||
if got, want := quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USDT"; got != want {
|
if got, want := quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USDT"; got != want {
|
||||||
t.Fatalf("unexpected route settlement token: got=%q want=%q", got, want)
|
t.Fatalf("unexpected route settlement token: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
|
if got, want := quote.GetRoute().GetSettlement().GetModel(), "fix_source"; got != want {
|
||||||
|
t.Fatalf("unexpected route settlement model: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
if quote.GetExecutionConditions() == nil {
|
if quote.GetExecutionConditions() == nil {
|
||||||
t.Fatalf("expected execution conditions")
|
t.Fatalf("expected execution conditions")
|
||||||
}
|
}
|
||||||
@@ -146,6 +151,12 @@ func TestQuotePayment_USDTToRUB_EndToEnd(t *testing.T) {
|
|||||||
if quote.GetExecutionConditions().GetPrefundingRequired() {
|
if quote.GetExecutionConditions().GetPrefundingRequired() {
|
||||||
t.Fatalf("expected prefunding_required=false")
|
t.Fatalf("expected prefunding_required=false")
|
||||||
}
|
}
|
||||||
|
if got, want := quote.GetResolvedSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE; got != want {
|
||||||
|
t.Fatalf("unexpected resolved_settlement_mode: got=%s want=%s", got.String(), want.String())
|
||||||
|
}
|
||||||
|
if got, want := quote.GetResolvedFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
|
||||||
|
t.Fatalf("unexpected resolved_fee_treatment: got=%s want=%s", got.String(), want.String())
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that idempotent reuse keeps full quote payload (including fee lines/rules).
|
// Verify that idempotent reuse keeps full quote payload (including fee lines/rules).
|
||||||
reused, err := svc.ProcessQuotePayment(context.Background(), req)
|
reused, err := svc.ProcessQuotePayment(context.Background(), req)
|
||||||
@@ -169,6 +180,67 @@ func TestQuotePayment_USDTToRUB_EndToEnd(t *testing.T) {
|
|||||||
t.Logf("single response:\n%s", mustProtoJSON(t, result.Response))
|
t.Logf("single response:\n%s", mustProtoJSON(t, result.Response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQuotePayment_ClampsQuoteExpiryToFXQuoteExpiry(t *testing.T) {
|
||||||
|
now := time.Unix(1_700_000_000, 0).UTC()
|
||||||
|
orgID := bson.NewObjectID()
|
||||||
|
|
||||||
|
store := newInMemoryQuotesStore()
|
||||||
|
core := &fakeQuoteCore{
|
||||||
|
now: now,
|
||||||
|
resultTTL: 10 * time.Minute,
|
||||||
|
fxTTL: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
svc := New(Dependencies{
|
||||||
|
QuotesStore: store,
|
||||||
|
Hydrator: transfer_intent_hydrator.New(nil, transfer_intent_hydrator.WithRefFactory(func() string {
|
||||||
|
return "q-intent-single"
|
||||||
|
})),
|
||||||
|
Computation: quote_computation_service.New(core),
|
||||||
|
Now: func() time.Time { return now },
|
||||||
|
NewRef: func() string { return "quote-fx-expiry-clamp" },
|
||||||
|
})
|
||||||
|
|
||||||
|
req := "ationv2.QuotePaymentRequest{
|
||||||
|
Meta: &sharedv1.RequestMeta{
|
||||||
|
OrganizationRef: orgID.Hex(),
|
||||||
|
},
|
||||||
|
IdempotencyKey: "idem-fx-expiry-clamp",
|
||||||
|
InitiatorRef: "initiator-42",
|
||||||
|
Intent: makeTransferIntent(t, "100", "USDT", "wallet-usdt-source", "4111111111111111", "RU"),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := svc.ProcessQuotePayment(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessQuotePayment returned error: %v", err)
|
||||||
|
}
|
||||||
|
if result == nil || result.Response == nil || result.Response.GetQuote() == nil {
|
||||||
|
t.Fatalf("expected quote response")
|
||||||
|
}
|
||||||
|
quote := result.Response.GetQuote()
|
||||||
|
if quote.GetFxQuote() == nil {
|
||||||
|
t.Fatalf("expected fx quote")
|
||||||
|
}
|
||||||
|
if quote.GetExpiresAt() == nil {
|
||||||
|
t.Fatalf("expected quote expires_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
gotQuoteExpiry := quote.GetExpiresAt().AsTime().UTC()
|
||||||
|
gotFXExpiry := time.UnixMilli(quote.GetFxQuote().GetExpiresAtUnixMs()).UTC()
|
||||||
|
if gotQuoteExpiry.After(gotFXExpiry) {
|
||||||
|
t.Fatalf("expected quote expires_at <= fx expires_at, got quote=%s fx=%s", gotQuoteExpiry, gotFXExpiry)
|
||||||
|
}
|
||||||
|
if got, want := gotQuoteExpiry.UnixMilli(), gotFXExpiry.UnixMilli(); got != want {
|
||||||
|
t.Fatalf("expected quote expiry to be clamped to fx expiry, got=%d want=%d", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Record == nil {
|
||||||
|
t.Fatalf("expected persisted record")
|
||||||
|
}
|
||||||
|
if got, want := result.Record.ExpiresAt.UTC().UnixMilli(), gotFXExpiry.UnixMilli(); got != want {
|
||||||
|
t.Fatalf("expected persisted expires_at to match clamped expiry, got=%d want=%d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestQuotePayments_USDTToRUB_ThreeItems_EndToEnd(t *testing.T) {
|
func TestQuotePayments_USDTToRUB_ThreeItems_EndToEnd(t *testing.T) {
|
||||||
now := time.Unix(1_700_000_000, 0).UTC()
|
now := time.Unix(1_700_000_000, 0).UTC()
|
||||||
orgID := bson.NewObjectID()
|
orgID := bson.NewObjectID()
|
||||||
@@ -192,7 +264,7 @@ func TestQuotePayments_USDTToRUB_ThreeItems_EndToEnd(t *testing.T) {
|
|||||||
IdempotencyKey: "idem-batch-usdt-rub",
|
IdempotencyKey: "idem-batch-usdt-rub",
|
||||||
InitiatorRef: "initiator-42",
|
InitiatorRef: "initiator-42",
|
||||||
PreviewOnly: false,
|
PreviewOnly: false,
|
||||||
Intents: []*transferv1.TransferIntent{
|
Intents: []*quotationv2.QuoteIntent{
|
||||||
makeTransferIntent(t, "100", "USDT", "wallet-usdt-source", "4111111111111111", "RU"),
|
makeTransferIntent(t, "100", "USDT", "wallet-usdt-source", "4111111111111111", "RU"),
|
||||||
makeTransferIntent(t, "125", "USDT", "wallet-usdt-source", "4222222222222222", "RU"),
|
makeTransferIntent(t, "125", "USDT", "wallet-usdt-source", "4222222222222222", "RU"),
|
||||||
makeTransferIntent(t, "80", "USDT", "wallet-usdt-source", "4333333333333333", "RU"),
|
makeTransferIntent(t, "80", "USDT", "wallet-usdt-source", "4333333333333333", "RU"),
|
||||||
@@ -224,11 +296,11 @@ func TestQuotePayments_USDTToRUB_ThreeItems_EndToEnd(t *testing.T) {
|
|||||||
if got, want := quote.GetState(), quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE; got != want {
|
if got, want := quote.GetState(), quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE; got != want {
|
||||||
t.Fatalf("unexpected quote state for item %d: got=%s want=%s", i, got.String(), want.String())
|
t.Fatalf("unexpected quote state for item %d: got=%s want=%s", i, got.String(), want.String())
|
||||||
}
|
}
|
||||||
if quote.GetDebitAmount().GetCurrency() != "USDT" {
|
if quote.GetTransferPrincipalAmount().GetCurrency() != "USDT" {
|
||||||
t.Fatalf("unexpected debit currency for item %d: %q", i, quote.GetDebitAmount().GetCurrency())
|
t.Fatalf("unexpected principal currency for item %d: %q", i, quote.GetTransferPrincipalAmount().GetCurrency())
|
||||||
}
|
}
|
||||||
if quote.GetCreditAmount().GetCurrency() != "RUB" {
|
if quote.GetDestinationAmount().GetCurrency() != "RUB" {
|
||||||
t.Fatalf("unexpected credit currency for item %d: %q", i, quote.GetCreditAmount().GetCurrency())
|
t.Fatalf("unexpected destination currency for item %d: %q", i, quote.GetDestinationAmount().GetCurrency())
|
||||||
}
|
}
|
||||||
if quote.GetRoute() == nil {
|
if quote.GetRoute() == nil {
|
||||||
t.Fatalf("expected route for item %d", i)
|
t.Fatalf("expected route for item %d", i)
|
||||||
@@ -251,6 +323,9 @@ func TestQuotePayments_USDTToRUB_ThreeItems_EndToEnd(t *testing.T) {
|
|||||||
if got, want := quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USDT"; got != want {
|
if got, want := quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USDT"; got != want {
|
||||||
t.Fatalf("unexpected route settlement token for item %d: got=%q want=%q", i, got, want)
|
t.Fatalf("unexpected route settlement token for item %d: got=%q want=%q", i, got, want)
|
||||||
}
|
}
|
||||||
|
if got, want := quote.GetRoute().GetSettlement().GetModel(), "fix_source"; got != want {
|
||||||
|
t.Fatalf("unexpected route settlement model for item %d: got=%q want=%q", i, got, want)
|
||||||
|
}
|
||||||
if quote.GetExecutionConditions() == nil {
|
if quote.GetExecutionConditions() == nil {
|
||||||
t.Fatalf("expected execution conditions for item %d", i)
|
t.Fatalf("expected execution conditions for item %d", i)
|
||||||
}
|
}
|
||||||
@@ -263,6 +338,18 @@ func TestQuotePayments_USDTToRUB_ThreeItems_EndToEnd(t *testing.T) {
|
|||||||
if got, want := len(quote.GetFeeRules()), 1; got != want {
|
if got, want := len(quote.GetFeeRules()), 1; got != want {
|
||||||
t.Fatalf("unexpected fee rules count for item %d: got=%d want=%d", i, got, want)
|
t.Fatalf("unexpected fee rules count for item %d: got=%d want=%d", i, got, want)
|
||||||
}
|
}
|
||||||
|
if quote.GetFxQuote() == nil {
|
||||||
|
t.Fatalf("expected fx quote for item %d", i)
|
||||||
|
}
|
||||||
|
if got, want := quote.GetFxQuote().GetSide(), fxv1.Side_SELL_BASE_BUY_QUOTE; got != want {
|
||||||
|
t.Fatalf("unexpected fx side for item %d: got=%s want=%s", i, got.String(), want.String())
|
||||||
|
}
|
||||||
|
if got, want := quote.GetResolvedSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE; got != want {
|
||||||
|
t.Fatalf("unexpected resolved_settlement_mode for item %d: got=%s want=%s", i, got.String(), want.String())
|
||||||
|
}
|
||||||
|
if got, want := quote.GetResolvedFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
|
||||||
|
t.Fatalf("unexpected resolved_fee_treatment for item %d: got=%s want=%s", i, got.String(), want.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, want := core.quoteRequestIdempotencyKeys, []string{
|
if got, want := core.quoteRequestIdempotencyKeys, []string{
|
||||||
@@ -415,8 +502,8 @@ func TestQuotePayment_SelectsEligibleGatewaysAndIgnoresIrrelevant(t *testing.T)
|
|||||||
if got, want := quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USDT"; got != want {
|
if got, want := quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USDT"; got != want {
|
||||||
t.Fatalf("unexpected route settlement token: got=%q want=%q", got, want)
|
t.Fatalf("unexpected route settlement token: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := quote.GetTotalCost().GetAmount(), "102.4"; got != want {
|
if got, want := quote.GetPayerTotalDebitAmount().GetAmount(), "102.4"; got != want {
|
||||||
t.Fatalf("unexpected total_cost amount: got=%q want=%q", got, want)
|
t.Fatalf("unexpected payer_total_debit_amount: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +514,7 @@ func makeTransferIntent(
|
|||||||
sourceWalletID string,
|
sourceWalletID string,
|
||||||
destinationPAN string,
|
destinationPAN string,
|
||||||
destinationCountry string,
|
destinationCountry string,
|
||||||
) *transferv1.TransferIntent {
|
) *quotationv2.QuoteIntent {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
walletData, err := bson.Marshal(pkgmodel.WalletPaymentData{WalletID: sourceWalletID})
|
walletData, err := bson.Marshal(pkgmodel.WalletPaymentData{WalletID: sourceWalletID})
|
||||||
@@ -446,7 +533,7 @@ func makeTransferIntent(
|
|||||||
t.Fatalf("failed to marshal card method data: %v", err)
|
t.Fatalf("failed to marshal card method data: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &transferv1.TransferIntent{
|
return "ationv2.QuoteIntent{
|
||||||
Source: &endpointv1.PaymentEndpoint{
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||||
PaymentMethod: &endpointv1.PaymentMethod{
|
PaymentMethod: &endpointv1.PaymentMethod{
|
||||||
@@ -480,6 +567,8 @@ type fakeQuoteCore struct {
|
|||||||
now time.Time
|
now time.Time
|
||||||
|
|
||||||
quoteRequestIdempotencyKeys []string
|
quoteRequestIdempotencyKeys []string
|
||||||
|
resultTTL time.Duration
|
||||||
|
fxTTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeQuoteCore) BuildQuote(_ context.Context, in quote_computation_service.BuildQuoteInput) (*quote_computation_service.ComputedQuote, time.Time, error) {
|
func (f *fakeQuoteCore) BuildQuote(_ context.Context, in quote_computation_service.BuildQuoteInput) (*quote_computation_service.ComputedQuote, time.Time, error) {
|
||||||
@@ -564,18 +653,32 @@ func (f *fakeQuoteCore) BuildQuote(_ context.Context, in quote_computation_servi
|
|||||||
Base: "USDT",
|
Base: "USDT",
|
||||||
Quote: "RUB",
|
Quote: "RUB",
|
||||||
},
|
},
|
||||||
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
|
Side: fxv1.Side_SELL_BASE_BUY_QUOTE,
|
||||||
Price: &moneyv1.Decimal{Value: rate.String()},
|
Price: &moneyv1.Decimal{Value: rate.String()},
|
||||||
BaseAmount: &moneyv1.Money{Amount: baseAmount.String(), Currency: "USDT"},
|
BaseAmount: &moneyv1.Money{Amount: baseAmount.String(), Currency: "USDT"},
|
||||||
QuoteAmount: &moneyv1.Money{Amount: quoteAmount.String(), Currency: "RUB"},
|
QuoteAmount: &moneyv1.Money{Amount: quoteAmount.String(), Currency: "RUB"},
|
||||||
ExpiresAtUnixMs: f.now.Add(5 * time.Minute).UnixMilli(),
|
ExpiresAtUnixMs: f.now.Add(f.fxTTLValue()).UnixMilli(),
|
||||||
Provider: "test-oracle",
|
Provider: "test-oracle",
|
||||||
RateRef: "rate-usdt-rub",
|
RateRef: "rate-usdt-rub",
|
||||||
Firm: true,
|
Firm: true,
|
||||||
PricedAt: timestamppb.New(f.now),
|
PricedAt: timestamppb.New(f.now),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return quote, f.now.Add(5 * time.Minute), nil
|
return quote, f.now.Add(f.resultTTLValue()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeQuoteCore) resultTTLValue() time.Duration {
|
||||||
|
if f == nil || f.resultTTL <= 0 {
|
||||||
|
return 5 * time.Minute
|
||||||
|
}
|
||||||
|
return f.resultTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeQuoteCore) fxTTLValue() time.Duration {
|
||||||
|
if f == nil || f.fxTTL <= 0 {
|
||||||
|
return f.resultTTLValue()
|
||||||
|
}
|
||||||
|
return f.fxTTL
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneRouteSpecForTest(src *quotationv2.RouteSpecification) *quotationv2.RouteSpecification {
|
func cloneRouteSpecForTest(src *quotationv2.RouteSpecification) *quotationv2.RouteSpecification {
|
||||||
|
|||||||
@@ -135,17 +135,25 @@ func (p *singleIntentProcessorV2) Process(
|
|||||||
|
|
||||||
canonical := quote_response_mapper_v2.CanonicalQuote{
|
canonical := quote_response_mapper_v2.CanonicalQuote{
|
||||||
QuoteRef: p.quoteRef,
|
QuoteRef: p.quoteRef,
|
||||||
DebitAmount: cloneProtoMoney(result.Quote.DebitAmount),
|
TransferPrincipalAmount: cloneProtoMoney(result.Quote.DebitAmount),
|
||||||
CreditAmount: cloneProtoMoney(result.Quote.CreditAmount),
|
DestinationAmount: cloneProtoMoney(result.Quote.CreditAmount),
|
||||||
TotalCost: cloneProtoMoney(result.Quote.TotalCost),
|
PayerTotalDebitAmount: cloneProtoMoney(result.Quote.TotalCost),
|
||||||
FeeLines: result.Quote.FeeLines,
|
FeeLines: result.Quote.FeeLines,
|
||||||
FeeRules: result.Quote.FeeRules,
|
FeeRules: result.Quote.FeeRules,
|
||||||
FXQuote: result.Quote.FXQuote,
|
FXQuote: result.Quote.FXQuote,
|
||||||
Route: result.Quote.Route,
|
Route: result.Quote.Route,
|
||||||
Conditions: result.Quote.ExecutionConditions,
|
Conditions: result.Quote.ExecutionConditions,
|
||||||
|
ResolvedSettlementMode: result.Quote.ResolvedSettlementMode,
|
||||||
|
ResolvedFeeTreatment: result.Quote.ResolvedFeeTreatment,
|
||||||
ExpiresAt: result.ExpiresAt,
|
ExpiresAt: result.ExpiresAt,
|
||||||
PricedAt: p.pricedAt,
|
PricedAt: p.pricedAt,
|
||||||
}
|
}
|
||||||
|
if canonical.ResolvedSettlementMode == 0 {
|
||||||
|
canonical.ResolvedSettlementMode = resolvedSettlementModeFromModel(planItem.Intent.SettlementMode)
|
||||||
|
}
|
||||||
|
if canonical.ResolvedFeeTreatment == 0 {
|
||||||
|
canonical.ResolvedFeeTreatment = resolvedFeeTreatmentForSettlementMode(canonical.ResolvedSettlementMode)
|
||||||
|
}
|
||||||
|
|
||||||
mapped, mapErr := p.mapper.Map(quote_response_mapper_v2.MapInput{
|
mapped, mapErr := p.mapper.Map(quote_response_mapper_v2.MapInput{
|
||||||
Quote: canonical,
|
Quote: canonical,
|
||||||
@@ -157,12 +165,16 @@ func (p *singleIntentProcessorV2) Process(
|
|||||||
if mapped == nil || mapped.Quote == nil {
|
if mapped == nil || mapped.Quote == nil {
|
||||||
return nil, merrors.InvalidArgument("mapped quote is required")
|
return nil, merrors.InvalidArgument("mapped quote is required")
|
||||||
}
|
}
|
||||||
|
expiresAt := result.ExpiresAt
|
||||||
|
if quoteExpiresAt := mapped.Quote.GetExpiresAt(); quoteExpiresAt != nil {
|
||||||
|
expiresAt = quoteExpiresAt.AsTime().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
p.collector.Add(&itemProcessDetail{
|
p.collector.Add(&itemProcessDetail{
|
||||||
Index: in.Item.Index,
|
Index: in.Item.Index,
|
||||||
Intent: planItem.Intent,
|
Intent: planItem.Intent,
|
||||||
Quote: result.Quote,
|
Quote: result.Quote,
|
||||||
ExpiresAt: result.ExpiresAt,
|
ExpiresAt: expiresAt,
|
||||||
Status: status,
|
Status: status,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ func ensureComputedQuote(src *ComputedQuote, item *QuoteComputationPlanItem) *Co
|
|||||||
if src.TotalCost == nil {
|
if src.TotalCost == nil {
|
||||||
src.TotalCost = deriveTotalCost(src.DebitAmount, src.FeeLines)
|
src.TotalCost = deriveTotalCost(src.DebitAmount, src.FeeLines)
|
||||||
}
|
}
|
||||||
|
if src.ResolvedSettlementMode == paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED {
|
||||||
|
src.ResolvedSettlementMode = item.ResolvedSettlementMode
|
||||||
|
}
|
||||||
|
if src.ResolvedSettlementMode == paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED {
|
||||||
|
src.ResolvedSettlementMode = paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
}
|
||||||
|
if src.ResolvedFeeTreatment == quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED {
|
||||||
|
src.ResolvedFeeTreatment = item.ResolvedFeeTreatment
|
||||||
|
}
|
||||||
|
if src.ResolvedFeeTreatment == quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED {
|
||||||
|
src.ResolvedFeeTreatment = resolvedFeeTreatmentForSettlementMode(src.ResolvedSettlementMode)
|
||||||
|
}
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package quote_computation_service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/payments/storage/model"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolvedSettlementModeFromModel(mode model.SettlementMode) paymentv1.SettlementMode {
|
||||||
|
switch mode {
|
||||||
|
case model.SettlementModeFixSource:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
case model.SettlementModeFixReceived:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED
|
||||||
|
default:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvedSettlementModeFromRouteModelValue(value string) paymentv1.SettlementMode {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "FIX_RECEIVED", "SETTLEMENT_FIX_RECEIVED", "SETTLEMENT_MODE_FIX_RECEIVED":
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED
|
||||||
|
case "FIX_SOURCE", "SETTLEMENT_FIX_SOURCE", "SETTLEMENT_MODE_FIX_SOURCE":
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
default:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvedFeeTreatmentForSettlementMode(mode paymentv1.SettlementMode) quotationv2.FeeTreatment {
|
||||||
|
switch mode {
|
||||||
|
case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION
|
||||||
|
default:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,6 @@ func modelSettlementMode(mode transfer_intent_hydrator.QuoteSettlementMode) mode
|
|||||||
case transfer_intent_hydrator.QuoteSettlementModeFixReceived:
|
case transfer_intent_hydrator.QuoteSettlementModeFixReceived:
|
||||||
return model.SettlementModeFixReceived
|
return model.SettlementModeFixReceived
|
||||||
default:
|
default:
|
||||||
return model.SettlementModeUnspecified
|
return model.SettlementModeFixSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/model/account_role"
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
@@ -43,6 +44,8 @@ type ComputedQuote struct {
|
|||||||
FXQuote *oraclev1.Quote
|
FXQuote *oraclev1.Quote
|
||||||
Route *quotationv2.RouteSpecification
|
Route *quotationv2.RouteSpecification
|
||||||
ExecutionConditions *quotationv2.ExecutionConditions
|
ExecutionConditions *quotationv2.ExecutionConditions
|
||||||
|
ResolvedSettlementMode paymentv1.SettlementMode
|
||||||
|
ResolvedFeeTreatment quotationv2.FeeTreatment
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuoteComputationPlan is an orchestration plan for quote computations.
|
// QuoteComputationPlan is an orchestration plan for quote computations.
|
||||||
@@ -67,6 +70,8 @@ type QuoteComputationPlanItem struct {
|
|||||||
Funding *gateway_funding_profile.QuoteFundingGate
|
Funding *gateway_funding_profile.QuoteFundingGate
|
||||||
Route *quotationv2.RouteSpecification
|
Route *quotationv2.RouteSpecification
|
||||||
ExecutionConditions *quotationv2.ExecutionConditions
|
ExecutionConditions *quotationv2.ExecutionConditions
|
||||||
|
ResolvedSettlementMode paymentv1.SettlementMode
|
||||||
|
ResolvedFeeTreatment quotationv2.FeeTreatment
|
||||||
BlockReason quotationv2.QuoteBlockReason
|
BlockReason quotationv2.QuoteBlockReason
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ func (s *QuoteComputationService) buildPlanItem(
|
|||||||
Route: cloneRouteSpecification(route),
|
Route: cloneRouteSpecification(route),
|
||||||
ExecutionConditions: cloneExecutionConditions(conditions),
|
ExecutionConditions: cloneExecutionConditions(conditions),
|
||||||
}
|
}
|
||||||
|
resolvedSettlementMode := resolvedSettlementModeFromModel(modelIntent.SettlementMode)
|
||||||
|
resolvedFeeTreatment := resolvedFeeTreatmentForSettlementMode(resolvedSettlementMode)
|
||||||
|
|
||||||
intentRef := strings.TrimSpace(modelIntent.Ref)
|
intentRef := strings.TrimSpace(modelIntent.Ref)
|
||||||
if intentRef == "" {
|
if intentRef == "" {
|
||||||
@@ -168,6 +170,8 @@ func (s *QuoteComputationService) buildPlanItem(
|
|||||||
Funding: funding,
|
Funding: funding,
|
||||||
Route: route,
|
Route: route,
|
||||||
ExecutionConditions: conditions,
|
ExecutionConditions: conditions,
|
||||||
|
ResolvedSettlementMode: resolvedSettlementMode,
|
||||||
|
ResolvedFeeTreatment: resolvedFeeTreatment,
|
||||||
BlockReason: blockReason,
|
BlockReason: blockReason,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func buildRouteSettlement(
|
|||||||
network string,
|
network string,
|
||||||
hops []*quotationv2.RouteHop,
|
hops []*quotationv2.RouteHop,
|
||||||
) *quotationv2.RouteSettlement {
|
) *quotationv2.RouteSettlement {
|
||||||
modelValue := normalizeSettlementModel(settlementModelString(intent.SettlementMode))
|
modelValue := strings.ToLower(strings.TrimSpace(settlementModelString(intent.SettlementMode)))
|
||||||
asset := buildRouteSettlementAsset(intent, network, hops)
|
asset := buildRouteSettlementAsset(intent, network, hops)
|
||||||
if asset == nil && modelValue == "" {
|
if asset == nil && modelValue == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -77,11 +77,11 @@ func buildExecutionConditions(
|
|||||||
func settlementModelString(mode model.SettlementMode) string {
|
func settlementModelString(mode model.SettlementMode) string {
|
||||||
switch mode {
|
switch mode {
|
||||||
case model.SettlementModeFixSource:
|
case model.SettlementModeFixSource:
|
||||||
return "FIX_SOURCE"
|
return "fix_source"
|
||||||
case model.SettlementModeFixReceived:
|
case model.SettlementModeFixReceived:
|
||||||
return "FIX_RECEIVED"
|
return "fix_received"
|
||||||
default:
|
default:
|
||||||
return "UNSPECIFIED"
|
return "fix_source"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||||
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"
|
||||||
transferv1 "github.com/tech/sendico/pkg/proto/payments/transfer/v1"
|
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
@@ -17,7 +17,7 @@ func TestFingerprintQuotePayment_IgnoresTransportFields(t *testing.T) {
|
|||||||
base := "ationv2.QuotePaymentRequest{
|
base := "ationv2.QuotePaymentRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: bson.NewObjectID().Hex()},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: bson.NewObjectID().Hex()},
|
||||||
IdempotencyKey: "idem-a",
|
IdempotencyKey: "idem-a",
|
||||||
Intent: testTransferIntent("10"),
|
Intent: testQuoteIntent("10"),
|
||||||
PreviewOnly: false,
|
PreviewOnly: false,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ func TestFingerprintQuotePayment_DetectsBusinessPayloadChanges(t *testing.T) {
|
|||||||
base := "ationv2.QuotePaymentRequest{
|
base := "ationv2.QuotePaymentRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: bson.NewObjectID().Hex()},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: bson.NewObjectID().Hex()},
|
||||||
IdempotencyKey: "idem-a",
|
IdempotencyKey: "idem-a",
|
||||||
Intent: testTransferIntent("10"),
|
Intent: testQuoteIntent("10"),
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ func TestFingerprintQuotePayment_DetectsBusinessPayloadChanges(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changedAmount := proto.Clone(base).(*quotationv2.QuotePaymentRequest)
|
changedAmount := proto.Clone(base).(*quotationv2.QuotePaymentRequest)
|
||||||
changedAmount.Intent = testTransferIntent("11")
|
changedAmount.Intent = testQuoteIntent("11")
|
||||||
if got, want := svc.FingerprintQuotePayment(base), svc.FingerprintQuotePayment(changedAmount); got == want {
|
if got, want := svc.FingerprintQuotePayment(base), svc.FingerprintQuotePayment(changedAmount); got == want {
|
||||||
t.Fatalf("expected different fingerprint for amount change")
|
t.Fatalf("expected different fingerprint for amount change")
|
||||||
}
|
}
|
||||||
@@ -58,9 +58,9 @@ func TestFingerprintQuotePayments_IgnoresTransportFields(t *testing.T) {
|
|||||||
base := "ationv2.QuotePaymentsRequest{
|
base := "ationv2.QuotePaymentsRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: bson.NewObjectID().Hex()},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: bson.NewObjectID().Hex()},
|
||||||
IdempotencyKey: "idem-a",
|
IdempotencyKey: "idem-a",
|
||||||
Intents: []*transferv1.TransferIntent{
|
Intents: []*quotationv2.QuoteIntent{
|
||||||
testTransferIntent("10"),
|
testQuoteIntent("10"),
|
||||||
testTransferIntent("20"),
|
testQuoteIntent("20"),
|
||||||
},
|
},
|
||||||
PreviewOnly: false,
|
PreviewOnly: false,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
@@ -80,9 +80,9 @@ func TestFingerprintQuotePayments_DetectsBusinessPayloadChanges(t *testing.T) {
|
|||||||
base := "ationv2.QuotePaymentsRequest{
|
base := "ationv2.QuotePaymentsRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: bson.NewObjectID().Hex()},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: bson.NewObjectID().Hex()},
|
||||||
IdempotencyKey: "idem-a",
|
IdempotencyKey: "idem-a",
|
||||||
Intents: []*transferv1.TransferIntent{
|
Intents: []*quotationv2.QuoteIntent{
|
||||||
testTransferIntent("10"),
|
testQuoteIntent("10"),
|
||||||
testTransferIntent("20"),
|
testQuoteIntent("20"),
|
||||||
},
|
},
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
}
|
}
|
||||||
@@ -94,20 +94,21 @@ func TestFingerprintQuotePayments_DetectsBusinessPayloadChanges(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reordered := proto.Clone(base).(*quotationv2.QuotePaymentsRequest)
|
reordered := proto.Clone(base).(*quotationv2.QuotePaymentsRequest)
|
||||||
reordered.Intents = []*transferv1.TransferIntent{
|
reordered.Intents = []*quotationv2.QuoteIntent{
|
||||||
testTransferIntent("20"),
|
testQuoteIntent("20"),
|
||||||
testTransferIntent("10"),
|
testQuoteIntent("10"),
|
||||||
}
|
}
|
||||||
if got, want := svc.FingerprintQuotePayments(base), svc.FingerprintQuotePayments(reordered); got == want {
|
if got, want := svc.FingerprintQuotePayments(base), svc.FingerprintQuotePayments(reordered); got == want {
|
||||||
t.Fatalf("expected different fingerprint for intent order change")
|
t.Fatalf("expected different fingerprint for intent order change")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTransferIntent(amount string) *transferv1.TransferIntent {
|
func testQuoteIntent(amount string) *quotationv2.QuoteIntent {
|
||||||
return &transferv1.TransferIntent{
|
return "ationv2.QuoteIntent{
|
||||||
Source: endpointWithMethodRef("pm-src"),
|
Source: endpointWithMethodRef("pm-src"),
|
||||||
Destination: endpointWithMethodRef("pm-dst"),
|
Destination: endpointWithMethodRef("pm-dst"),
|
||||||
Amount: &moneyv1.Money{Amount: amount, Currency: "USD"},
|
Amount: &moneyv1.Money{Amount: amount, Currency: "USD"},
|
||||||
|
SettlementMode: paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||||
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"
|
||||||
transferv1 "github.com/tech/sendico/pkg/proto/payments/transfer/v1"
|
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ func (v *QuoteRequestValidatorV2) ValidateQuotePayment(req *quotationv2.QuotePay
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := validateTransferIntent(req.GetIntent(), "intent"); err != nil {
|
if err := validateQuoteIntent(req.GetIntent(), "intent"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ func (v *QuoteRequestValidatorV2) ValidateQuotePayments(req *quotationv2.QuotePa
|
|||||||
return nil, merrors.InvalidArgument("intents are required")
|
return nil, merrors.InvalidArgument("intents are required")
|
||||||
}
|
}
|
||||||
for i, intent := range intents {
|
for i, intent := range intents {
|
||||||
if err := validateTransferIntent(intent, fmt.Sprintf("intents[%d]", i)); err != nil {
|
if err := validateQuoteIntent(intent, fmt.Sprintf("intents[%d]", i)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ func validateMeta(meta *sharedv1.RequestMeta) (string, bson.ObjectID, error) {
|
|||||||
return orgRef, orgID, nil
|
return orgRef, orgID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateTransferIntent(intent *transferv1.TransferIntent, field string) error {
|
func validateQuoteIntent(intent *quotationv2.QuoteIntent, field string) error {
|
||||||
if intent == nil {
|
if intent == nil {
|
||||||
return merrors.InvalidArgument(field + " is required")
|
return merrors.InvalidArgument(field + " is required")
|
||||||
}
|
}
|
||||||
@@ -140,9 +140,57 @@ func validateTransferIntent(intent *transferv1.TransferIntent, field string) err
|
|||||||
if intent.GetAmount() == nil {
|
if intent.GetAmount() == nil {
|
||||||
return merrors.InvalidArgument(field + ".amount is required")
|
return merrors.InvalidArgument(field + ".amount is required")
|
||||||
}
|
}
|
||||||
|
if err := validateSettlementAndFeeTreatment(intent.GetSettlementMode(), intent.GetFeeTreatment(), field); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateSettlementAndFeeTreatment(
|
||||||
|
mode paymentv1.SettlementMode,
|
||||||
|
feeTreatment quotationv2.FeeTreatment,
|
||||||
|
field string,
|
||||||
|
) error {
|
||||||
|
if mode == paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED {
|
||||||
|
if feeTreatment != quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED {
|
||||||
|
return merrors.InvalidArgument(field + ".settlement_mode is required when fee_treatment is set")
|
||||||
|
}
|
||||||
|
// Both omitted is allowed and will be normalized by the hydrator.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expected, ok := expectedFeeTreatmentForMode(mode)
|
||||||
|
if !ok {
|
||||||
|
return merrors.InvalidArgument(field + ".settlement_mode is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if feeTreatment == quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if feeTreatment != expected {
|
||||||
|
return merrors.InvalidArgument(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s.fee_treatment conflicts with settlement_mode %s (expected %s)",
|
||||||
|
field,
|
||||||
|
mode.String(),
|
||||||
|
expected.String(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectedFeeTreatmentForMode(mode paymentv1.SettlementMode) (quotationv2.FeeTreatment, bool) {
|
||||||
|
switch mode {
|
||||||
|
case paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE, true
|
||||||
|
case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION, true
|
||||||
|
default:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func hasEndpointValue(endpoint *endpointv1.PaymentEndpoint) bool {
|
func hasEndpointValue(endpoint *endpointv1.PaymentEndpoint) bool {
|
||||||
if endpoint == nil {
|
if endpoint == nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||||
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"
|
||||||
transferv1 "github.com/tech/sendico/pkg/proto/payments/transfer/v1"
|
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ func TestValidateQuotePayment_Success(t *testing.T) {
|
|||||||
req := "ationv2.QuotePaymentRequest{
|
req := "ationv2.QuotePaymentRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgID.Hex()},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgID.Hex()},
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
Intent: validTransferIntent(),
|
Intent: validQuoteIntent(),
|
||||||
PreviewOnly: false,
|
PreviewOnly: false,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ func TestValidateQuotePayment_Rules(t *testing.T) {
|
|||||||
name: "idempotency required for non-preview",
|
name: "idempotency required for non-preview",
|
||||||
req: "ationv2.QuotePaymentRequest{
|
req: "ationv2.QuotePaymentRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
Intent: validTransferIntent(),
|
Intent: validQuoteIntent(),
|
||||||
PreviewOnly: false,
|
PreviewOnly: false,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
},
|
},
|
||||||
@@ -72,7 +72,7 @@ func TestValidateQuotePayment_Rules(t *testing.T) {
|
|||||||
req: "ationv2.QuotePaymentRequest{
|
req: "ationv2.QuotePaymentRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
Intent: validTransferIntent(),
|
Intent: validQuoteIntent(),
|
||||||
PreviewOnly: true,
|
PreviewOnly: true,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
},
|
},
|
||||||
@@ -83,7 +83,7 @@ func TestValidateQuotePayment_Rules(t *testing.T) {
|
|||||||
req: "ationv2.QuotePaymentRequest{
|
req: "ationv2.QuotePaymentRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
Intent: validTransferIntent(),
|
Intent: validQuoteIntent(),
|
||||||
PreviewOnly: false,
|
PreviewOnly: false,
|
||||||
},
|
},
|
||||||
checkErr: func(err error) bool { return errors.Is(err, ErrInitiatorRefRequired) },
|
checkErr: func(err error) bool { return errors.Is(err, ErrInitiatorRefRequired) },
|
||||||
@@ -93,7 +93,7 @@ func TestValidateQuotePayment_Rules(t *testing.T) {
|
|||||||
req: "ationv2.QuotePaymentRequest{
|
req: "ationv2.QuotePaymentRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "bad-org"},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: "bad-org"},
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
Intent: validTransferIntent(),
|
Intent: validQuoteIntent(),
|
||||||
PreviewOnly: false,
|
PreviewOnly: false,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
},
|
},
|
||||||
@@ -106,7 +106,7 @@ func TestValidateQuotePayment_Rules(t *testing.T) {
|
|||||||
req: "ationv2.QuotePaymentRequest{
|
req: "ationv2.QuotePaymentRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
Intent: &transferv1.TransferIntent{
|
Intent: "ationv2.QuoteIntent{
|
||||||
Destination: endpointWithMethodRef("pm-dst"),
|
Destination: endpointWithMethodRef("pm-dst"),
|
||||||
Amount: &moneyv1.Money{Amount: "10", Currency: "USD"},
|
Amount: &moneyv1.Money{Amount: "10", Currency: "USD"},
|
||||||
},
|
},
|
||||||
@@ -115,6 +115,44 @@ func TestValidateQuotePayment_Rules(t *testing.T) {
|
|||||||
},
|
},
|
||||||
checkErr: func(err error) bool { return err != nil && strings.Contains(err.Error(), "intent.source is required") },
|
checkErr: func(err error) bool { return err != nil && strings.Contains(err.Error(), "intent.source is required") },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "fee treatment requires settlement mode",
|
||||||
|
req: "ationv2.QuotePaymentRequest{
|
||||||
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
|
IdempotencyKey: "idem-1",
|
||||||
|
Intent: "ationv2.QuoteIntent{
|
||||||
|
Source: endpointWithMethodRef("pm-src"),
|
||||||
|
Destination: endpointWithMethodRef("pm-dst"),
|
||||||
|
Amount: &moneyv1.Money{Amount: "10", Currency: "USD"},
|
||||||
|
FeeTreatment: quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
|
||||||
|
SettlementMode: paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED,
|
||||||
|
},
|
||||||
|
PreviewOnly: false,
|
||||||
|
InitiatorRef: "actor-1",
|
||||||
|
},
|
||||||
|
checkErr: func(err error) bool {
|
||||||
|
return err != nil && strings.Contains(err.Error(), "intent.settlement_mode is required when fee_treatment is set")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conflicting fee treatment is rejected",
|
||||||
|
req: "ationv2.QuotePaymentRequest{
|
||||||
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
|
IdempotencyKey: "idem-1",
|
||||||
|
Intent: "ationv2.QuoteIntent{
|
||||||
|
Source: endpointWithMethodRef("pm-src"),
|
||||||
|
Destination: endpointWithMethodRef("pm-dst"),
|
||||||
|
Amount: &moneyv1.Money{Amount: "10", Currency: "USD"},
|
||||||
|
SettlementMode: paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
|
||||||
|
FeeTreatment: quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION,
|
||||||
|
},
|
||||||
|
PreviewOnly: false,
|
||||||
|
InitiatorRef: "actor-1",
|
||||||
|
},
|
||||||
|
checkErr: func(err error) bool {
|
||||||
|
return err != nil && strings.Contains(err.Error(), "intent.fee_treatment conflicts with settlement_mode")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -132,7 +170,7 @@ func TestValidateQuotePayments_Success(t *testing.T) {
|
|||||||
orgID := bson.NewObjectID()
|
orgID := bson.NewObjectID()
|
||||||
req := "ationv2.QuotePaymentsRequest{
|
req := "ationv2.QuotePaymentsRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgID.Hex()},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgID.Hex()},
|
||||||
Intents: []*transferv1.TransferIntent{validTransferIntent(), validTransferIntent()},
|
Intents: []*quotationv2.QuoteIntent{validQuoteIntent(), validQuoteIntent()},
|
||||||
PreviewOnly: true,
|
PreviewOnly: true,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
}
|
}
|
||||||
@@ -178,7 +216,7 @@ func TestValidateQuotePayments_Rules(t *testing.T) {
|
|||||||
name: "idempotency required for non-preview",
|
name: "idempotency required for non-preview",
|
||||||
req: "ationv2.QuotePaymentsRequest{
|
req: "ationv2.QuotePaymentsRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
Intents: []*transferv1.TransferIntent{validTransferIntent()},
|
Intents: []*quotationv2.QuoteIntent{validQuoteIntent()},
|
||||||
PreviewOnly: false,
|
PreviewOnly: false,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
},
|
},
|
||||||
@@ -189,7 +227,7 @@ func TestValidateQuotePayments_Rules(t *testing.T) {
|
|||||||
req: "ationv2.QuotePaymentsRequest{
|
req: "ationv2.QuotePaymentsRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
Intents: []*transferv1.TransferIntent{validTransferIntent()},
|
Intents: []*quotationv2.QuoteIntent{validQuoteIntent()},
|
||||||
PreviewOnly: true,
|
PreviewOnly: true,
|
||||||
InitiatorRef: "actor-1",
|
InitiatorRef: "actor-1",
|
||||||
},
|
},
|
||||||
@@ -199,7 +237,7 @@ func TestValidateQuotePayments_Rules(t *testing.T) {
|
|||||||
name: "initiator ref required",
|
name: "initiator ref required",
|
||||||
req: "ationv2.QuotePaymentsRequest{
|
req: "ationv2.QuotePaymentsRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
Intents: []*transferv1.TransferIntent{validTransferIntent()},
|
Intents: []*quotationv2.QuoteIntent{validQuoteIntent()},
|
||||||
PreviewOnly: true,
|
PreviewOnly: true,
|
||||||
},
|
},
|
||||||
checkErr: func(err error) bool { return errors.Is(err, ErrInitiatorRefRequired) },
|
checkErr: func(err error) bool { return errors.Is(err, ErrInitiatorRefRequired) },
|
||||||
@@ -209,7 +247,7 @@ func TestValidateQuotePayments_Rules(t *testing.T) {
|
|||||||
req: "ationv2.QuotePaymentsRequest{
|
req: "ationv2.QuotePaymentsRequest{
|
||||||
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
Meta: &sharedv1.RequestMeta{OrganizationRef: orgHex},
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
Intents: []*transferv1.TransferIntent{
|
Intents: []*quotationv2.QuoteIntent{
|
||||||
{
|
{
|
||||||
Source: endpointWithMethodRef("pm-src"),
|
Source: endpointWithMethodRef("pm-src"),
|
||||||
Destination: endpointWithMethodRef("pm-dst"),
|
Destination: endpointWithMethodRef("pm-dst"),
|
||||||
@@ -234,11 +272,12 @@ func TestValidateQuotePayments_Rules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validTransferIntent() *transferv1.TransferIntent {
|
func validQuoteIntent() *quotationv2.QuoteIntent {
|
||||||
return &transferv1.TransferIntent{
|
return "ationv2.QuoteIntent{
|
||||||
Source: endpointWithMethodRef("pm-src"),
|
Source: endpointWithMethodRef("pm-src"),
|
||||||
Destination: endpointWithMethodRef("pm-dst"),
|
Destination: endpointWithMethodRef("pm-dst"),
|
||||||
Amount: &moneyv1.Money{Amount: "10", Currency: "USD"},
|
Amount: &moneyv1.Money{Amount: "10", Currency: "USD"},
|
||||||
|
SettlementMode: paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
)
|
)
|
||||||
@@ -17,14 +18,16 @@ type QuoteMeta struct {
|
|||||||
|
|
||||||
type CanonicalQuote struct {
|
type CanonicalQuote struct {
|
||||||
QuoteRef string
|
QuoteRef string
|
||||||
DebitAmount *moneyv1.Money
|
TransferPrincipalAmount *moneyv1.Money
|
||||||
CreditAmount *moneyv1.Money
|
DestinationAmount *moneyv1.Money
|
||||||
TotalCost *moneyv1.Money
|
PayerTotalDebitAmount *moneyv1.Money
|
||||||
FeeLines []*feesv1.DerivedPostingLine
|
FeeLines []*feesv1.DerivedPostingLine
|
||||||
FeeRules []*feesv1.AppliedRule
|
FeeRules []*feesv1.AppliedRule
|
||||||
FXQuote *oraclev1.Quote
|
FXQuote *oraclev1.Quote
|
||||||
Route *quotationv2.RouteSpecification
|
Route *quotationv2.RouteSpecification
|
||||||
Conditions *quotationv2.ExecutionConditions
|
Conditions *quotationv2.ExecutionConditions
|
||||||
|
ResolvedSettlementMode paymentv1.SettlementMode
|
||||||
|
ResolvedFeeTreatment quotationv2.FeeTreatment
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
PricedAt time.Time
|
PricedAt time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1"
|
storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1"
|
||||||
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
@@ -20,22 +22,27 @@ func (m *QuoteResponseMapperV2) Map(in MapInput) (*MapOutput, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
expiresAt := normalizeQuoteExpiresAt(in.Quote.ExpiresAt, in.Quote.FXQuote)
|
||||||
|
settlementMode := normalizeResolvedSettlementMode(in.Quote.ResolvedSettlementMode)
|
||||||
|
feeTreatment := normalizeResolvedFeeTreatment(settlementMode, in.Quote.ResolvedFeeTreatment)
|
||||||
|
|
||||||
result := "ationv2.PaymentQuote{
|
result := "ationv2.PaymentQuote{
|
||||||
Storable: mapStorable(in.Meta),
|
Storable: mapStorable(in.Meta),
|
||||||
State: decision.state,
|
State: decision.state,
|
||||||
BlockReason: decision.blockReason,
|
BlockReason: decision.blockReason,
|
||||||
DebitAmount: cloneMoney(in.Quote.DebitAmount),
|
TransferPrincipalAmount: cloneMoney(in.Quote.TransferPrincipalAmount),
|
||||||
CreditAmount: cloneMoney(in.Quote.CreditAmount),
|
DestinationAmount: cloneMoney(in.Quote.DestinationAmount),
|
||||||
TotalCost: cloneMoney(in.Quote.TotalCost),
|
PayerTotalDebitAmount: cloneMoney(in.Quote.PayerTotalDebitAmount),
|
||||||
FeeLines: cloneFeeLines(in.Quote.FeeLines),
|
FeeLines: cloneFeeLines(in.Quote.FeeLines),
|
||||||
FeeRules: cloneFeeRules(in.Quote.FeeRules),
|
FeeRules: cloneFeeRules(in.Quote.FeeRules),
|
||||||
FxQuote: cloneFXQuote(in.Quote.FXQuote),
|
FxQuote: cloneFXQuote(in.Quote.FXQuote),
|
||||||
Route: cloneRoute(in.Quote.Route),
|
Route: cloneRoute(in.Quote.Route),
|
||||||
ExecutionConditions: cloneExecutionConditions(in.Quote.Conditions),
|
ExecutionConditions: cloneExecutionConditions(in.Quote.Conditions),
|
||||||
QuoteRef: strings.TrimSpace(in.Quote.QuoteRef),
|
QuoteRef: strings.TrimSpace(in.Quote.QuoteRef),
|
||||||
ExpiresAt: tsOrNil(in.Quote.ExpiresAt),
|
ExpiresAt: tsOrNil(expiresAt),
|
||||||
PricedAt: tsOrNil(in.Quote.PricedAt),
|
PricedAt: tsOrNil(in.Quote.PricedAt),
|
||||||
|
ResolvedSettlementMode: settlementMode,
|
||||||
|
ResolvedFeeTreatment: feeTreatment,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &MapOutput{
|
return &MapOutput{
|
||||||
@@ -63,3 +70,50 @@ func tsOrNil(value time.Time) *timestamppb.Timestamp {
|
|||||||
}
|
}
|
||||||
return timestamppb.New(value)
|
return timestamppb.New(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeQuoteExpiresAt(expiresAt time.Time, fxQuote *oraclev1.Quote) time.Time {
|
||||||
|
if expiresAt.IsZero() || fxQuote == nil || fxQuote.GetExpiresAtUnixMs() <= 0 {
|
||||||
|
return expiresAt
|
||||||
|
}
|
||||||
|
fxExpiresAt := time.UnixMilli(fxQuote.GetExpiresAtUnixMs()).UTC()
|
||||||
|
if fxExpiresAt.IsZero() {
|
||||||
|
return expiresAt
|
||||||
|
}
|
||||||
|
if expiresAt.After(fxExpiresAt) {
|
||||||
|
return fxExpiresAt
|
||||||
|
}
|
||||||
|
return expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeResolvedSettlementMode(mode paymentv1.SettlementMode) paymentv1.SettlementMode {
|
||||||
|
switch mode {
|
||||||
|
case paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE, paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
|
||||||
|
return mode
|
||||||
|
default:
|
||||||
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeResolvedFeeTreatment(
|
||||||
|
mode paymentv1.SettlementMode,
|
||||||
|
feeTreatment quotationv2.FeeTreatment,
|
||||||
|
) quotationv2.FeeTreatment {
|
||||||
|
expected := expectedFeeTreatmentForMode(mode)
|
||||||
|
switch feeTreatment {
|
||||||
|
case quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
|
||||||
|
quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION:
|
||||||
|
if feeTreatment == expected {
|
||||||
|
return feeTreatment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expected
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectedFeeTreatmentForMode(mode paymentv1.SettlementMode) quotationv2.FeeTreatment {
|
||||||
|
switch mode {
|
||||||
|
case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION
|
||||||
|
default:
|
||||||
|
return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,15 +27,15 @@ func TestMap_ExecutableQuote(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Quote: CanonicalQuote{
|
Quote: CanonicalQuote{
|
||||||
QuoteRef: "q-1",
|
QuoteRef: "q-1",
|
||||||
DebitAmount: &moneyv1.Money{
|
TransferPrincipalAmount: &moneyv1.Money{
|
||||||
Amount: "10",
|
Amount: "10",
|
||||||
Currency: "USD",
|
Currency: "USD",
|
||||||
},
|
},
|
||||||
CreditAmount: &moneyv1.Money{
|
DestinationAmount: &moneyv1.Money{
|
||||||
Amount: "9",
|
Amount: "9",
|
||||||
Currency: "EUR",
|
Currency: "EUR",
|
||||||
},
|
},
|
||||||
TotalCost: &moneyv1.Money{
|
PayerTotalDebitAmount: &moneyv1.Money{
|
||||||
Amount: "10.2",
|
Amount: "10.2",
|
||||||
Currency: "USD",
|
Currency: "USD",
|
||||||
},
|
},
|
||||||
@@ -57,6 +58,8 @@ func TestMap_ExecutableQuote(t *testing.T) {
|
|||||||
PrefundingRequired: false,
|
PrefundingRequired: false,
|
||||||
LiquidityCheckRequiredAtExecution: true,
|
LiquidityCheckRequiredAtExecution: true,
|
||||||
},
|
},
|
||||||
|
ResolvedSettlementMode: paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
|
||||||
|
ResolvedFeeTreatment: quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
|
||||||
ExpiresAt: expiresAt,
|
ExpiresAt: expiresAt,
|
||||||
PricedAt: pricedAt,
|
PricedAt: pricedAt,
|
||||||
},
|
},
|
||||||
@@ -103,8 +106,68 @@ func TestMap_ExecutableQuote(t *testing.T) {
|
|||||||
if got, want := out.Quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USD"; got != want {
|
if got, want := out.Quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USD"; got != want {
|
||||||
t.Fatalf("unexpected settlement token: got=%q want=%q", got, want)
|
t.Fatalf("unexpected settlement token: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := out.Quote.GetTotalCost().GetAmount(), "10.2"; got != want {
|
if got, want := out.Quote.GetPayerTotalDebitAmount().GetAmount(), "10.2"; got != want {
|
||||||
t.Fatalf("unexpected total_cost amount: got=%q want=%q", got, want)
|
t.Fatalf("unexpected payer_total_debit_amount: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := out.Quote.GetResolvedSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE; got != want {
|
||||||
|
t.Fatalf("unexpected resolved_settlement_mode: got=%s want=%s", got.String(), want.String())
|
||||||
|
}
|
||||||
|
if got, want := out.Quote.GetResolvedFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
|
||||||
|
t.Fatalf("unexpected resolved_fee_treatment: got=%s want=%s", got.String(), want.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMap_ClampsQuoteExpiresAtToFXExpiry(t *testing.T) {
|
||||||
|
mapper := New()
|
||||||
|
quoteExpiresAt := time.Unix(300, 0).UTC()
|
||||||
|
fxExpiresAt := time.Unix(200, 0).UTC()
|
||||||
|
|
||||||
|
out, err := mapper.Map(MapInput{
|
||||||
|
Quote: CanonicalQuote{
|
||||||
|
QuoteRef: "q-fx-expiry-clamp",
|
||||||
|
ExpiresAt: quoteExpiresAt,
|
||||||
|
FXQuote: &oraclev1.Quote{ExpiresAtUnixMs: fxExpiresAt.UnixMilli()},
|
||||||
|
TransferPrincipalAmount: &moneyv1.Money{
|
||||||
|
Amount: "1",
|
||||||
|
Currency: "USDT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: QuoteStatus{
|
||||||
|
State: quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if out == nil || out.Quote == nil || out.Quote.GetExpiresAt() == nil {
|
||||||
|
t.Fatalf("expected mapped quote with expires_at")
|
||||||
|
}
|
||||||
|
if got, want := out.Quote.GetExpiresAt().AsTime().UnixMilli(), fxExpiresAt.UnixMilli(); got != want {
|
||||||
|
t.Fatalf("unexpected clamped expires_at: got=%d want=%d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMap_DefaultsResolvedEconomicsWhenUnset(t *testing.T) {
|
||||||
|
mapper := New()
|
||||||
|
out, err := mapper.Map(MapInput{
|
||||||
|
Quote: CanonicalQuote{
|
||||||
|
TransferPrincipalAmount: &moneyv1.Money{Amount: "1", Currency: "USDT"},
|
||||||
|
},
|
||||||
|
Status: QuoteStatus{
|
||||||
|
State: quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if out == nil || out.Quote == nil {
|
||||||
|
t.Fatalf("expected mapped quote")
|
||||||
|
}
|
||||||
|
if got, want := out.Quote.GetResolvedSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE; got != want {
|
||||||
|
t.Fatalf("unexpected default resolved_settlement_mode: got=%s want=%s", got.String(), want.String())
|
||||||
|
}
|
||||||
|
if got, want := out.Quote.GetResolvedFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
|
||||||
|
t.Fatalf("unexpected default resolved_fee_treatment: got=%s want=%s", got.String(), want.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
|
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
|
||||||
transferv1 "github.com/tech/sendico/pkg/proto/payments/transfer/v1"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
@@ -16,13 +17,13 @@ import (
|
|||||||
type HydrateOneInput struct {
|
type HydrateOneInput struct {
|
||||||
OrganizationRef string
|
OrganizationRef string
|
||||||
InitiatorRef string
|
InitiatorRef string
|
||||||
Intent *transferv1.TransferIntent
|
Intent *quotationv2.QuoteIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
type HydrateManyInput struct {
|
type HydrateManyInput struct {
|
||||||
OrganizationRef string
|
OrganizationRef string
|
||||||
InitiatorRef string
|
InitiatorRef string
|
||||||
Intents []*transferv1.TransferIntent
|
Intents []*quotationv2.QuoteIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentMethodsClient interface {
|
type PaymentMethodsClient interface {
|
||||||
@@ -76,6 +77,24 @@ func (h *TransferIntentHydrator) HydrateOne(ctx context.Context, in HydrateOneIn
|
|||||||
return nil, merrors.InvalidArgument("intent.amount is required")
|
return nil, merrors.InvalidArgument("intent.amount is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
amount := &paymenttypes.Money{
|
||||||
|
Amount: strings.TrimSpace(in.Intent.GetAmount().GetAmount()),
|
||||||
|
Currency: strings.TrimSpace(in.Intent.GetAmount().GetCurrency()),
|
||||||
|
}
|
||||||
|
if amount.Amount == "" {
|
||||||
|
return nil, merrors.InvalidArgument("intent.amount.amount is required")
|
||||||
|
}
|
||||||
|
if amount.Currency == "" {
|
||||||
|
return nil, merrors.InvalidArgument("intent.amount.currency is required")
|
||||||
|
}
|
||||||
|
settlementMode, feeTreatment, err := resolveEconomics(
|
||||||
|
in.Intent.GetSettlementMode(),
|
||||||
|
in.Intent.GetFeeTreatment(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
source, err := h.hydrateEndpoint(
|
source, err := h.hydrateEndpoint(
|
||||||
ctx,
|
ctx,
|
||||||
in.OrganizationRef,
|
in.OrganizationRef,
|
||||||
@@ -97,16 +116,11 @@ func (h *TransferIntentHydrator) HydrateOne(ctx context.Context, in HydrateOneIn
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
amount := &paymenttypes.Money{
|
settlementCurrency := strings.ToUpper(strings.TrimSpace(in.Intent.GetSettlementCurrency()))
|
||||||
Amount: strings.TrimSpace(in.Intent.GetAmount().GetAmount()),
|
if settlementCurrency == "" {
|
||||||
Currency: strings.TrimSpace(in.Intent.GetAmount().GetCurrency()),
|
settlementCurrency = strings.ToUpper(strings.TrimSpace(amount.Currency))
|
||||||
}
|
|
||||||
if amount.Amount == "" {
|
|
||||||
return nil, merrors.InvalidArgument("intent.amount.amount is required")
|
|
||||||
}
|
|
||||||
if amount.Currency == "" {
|
|
||||||
return nil, merrors.InvalidArgument("intent.amount.currency is required")
|
|
||||||
}
|
}
|
||||||
|
requiresFX := !strings.EqualFold(amount.Currency, settlementCurrency)
|
||||||
|
|
||||||
intent := &QuoteIntent{
|
intent := &QuoteIntent{
|
||||||
Ref: h.newRef(),
|
Ref: h.newRef(),
|
||||||
@@ -115,11 +129,14 @@ func (h *TransferIntentHydrator) HydrateOne(ctx context.Context, in HydrateOneIn
|
|||||||
Destination: destination,
|
Destination: destination,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
Comment: strings.TrimSpace(in.Intent.GetComment()),
|
Comment: strings.TrimSpace(in.Intent.GetComment()),
|
||||||
SettlementMode: QuoteSettlementModeUnspecified,
|
SettlementMode: settlementMode,
|
||||||
SettlementCurrency: amount.Currency,
|
FeeTreatment: feeTreatment,
|
||||||
RequiresFX: false,
|
SettlementCurrency: settlementCurrency,
|
||||||
|
RequiresFX: requiresFX,
|
||||||
Attributes: map[string]string{
|
Attributes: map[string]string{
|
||||||
"initiator_ref": strings.TrimSpace(in.InitiatorRef),
|
"initiator_ref": strings.TrimSpace(in.InitiatorRef),
|
||||||
|
"settlement_mode": string(settlementMode),
|
||||||
|
"fee_treatment": string(feeTreatment),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if intent.Comment != "" {
|
if intent.Comment != "" {
|
||||||
@@ -146,3 +163,67 @@ func (h *TransferIntentHydrator) HydrateMany(ctx context.Context, in HydrateMany
|
|||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveEconomics(
|
||||||
|
mode paymentv1.SettlementMode,
|
||||||
|
feeTreatment quotationv2.FeeTreatment,
|
||||||
|
) (QuoteSettlementMode, QuoteFeeTreatment, error) {
|
||||||
|
if mode == paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED &&
|
||||||
|
feeTreatment == quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED {
|
||||||
|
return QuoteSettlementModeFixSource, QuoteFeeTreatmentAddToSource, nil
|
||||||
|
}
|
||||||
|
if mode == paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED {
|
||||||
|
return QuoteSettlementModeUnspecified, QuoteFeeTreatmentUnspecified, merrors.InvalidArgument("intent.settlement_mode is required when fee_treatment is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedMode := settlementModeFromProto(mode)
|
||||||
|
if resolvedMode == QuoteSettlementModeUnspecified {
|
||||||
|
return QuoteSettlementModeUnspecified, QuoteFeeTreatmentUnspecified, merrors.InvalidArgument("intent.settlement_mode is invalid")
|
||||||
|
}
|
||||||
|
expectedFeeTreatment := feeTreatmentForMode(resolvedMode)
|
||||||
|
if feeTreatment == quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED {
|
||||||
|
return resolvedMode, expectedFeeTreatment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedFeeTreatment := feeTreatmentFromProto(feeTreatment)
|
||||||
|
if resolvedFeeTreatment == QuoteFeeTreatmentUnspecified {
|
||||||
|
return QuoteSettlementModeUnspecified, QuoteFeeTreatmentUnspecified, merrors.InvalidArgument("intent.fee_treatment is invalid")
|
||||||
|
}
|
||||||
|
if resolvedFeeTreatment != expectedFeeTreatment {
|
||||||
|
return QuoteSettlementModeUnspecified, QuoteFeeTreatmentUnspecified, merrors.InvalidArgument("intent.fee_treatment conflicts with settlement_mode")
|
||||||
|
}
|
||||||
|
return resolvedMode, resolvedFeeTreatment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func settlementModeFromProto(mode paymentv1.SettlementMode) QuoteSettlementMode {
|
||||||
|
switch mode {
|
||||||
|
case paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE:
|
||||||
|
return QuoteSettlementModeFixSource
|
||||||
|
case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
|
||||||
|
return QuoteSettlementModeFixReceived
|
||||||
|
default:
|
||||||
|
return QuoteSettlementModeUnspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func feeTreatmentFromProto(value quotationv2.FeeTreatment) QuoteFeeTreatment {
|
||||||
|
switch value {
|
||||||
|
case quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE:
|
||||||
|
return QuoteFeeTreatmentAddToSource
|
||||||
|
case quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION:
|
||||||
|
return QuoteFeeTreatmentDeductFromDestination
|
||||||
|
default:
|
||||||
|
return QuoteFeeTreatmentUnspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func feeTreatmentForMode(mode QuoteSettlementMode) QuoteFeeTreatment {
|
||||||
|
switch mode {
|
||||||
|
case QuoteSettlementModeFixSource:
|
||||||
|
return QuoteFeeTreatmentAddToSource
|
||||||
|
case QuoteSettlementModeFixReceived:
|
||||||
|
return QuoteFeeTreatmentDeductFromDestination
|
||||||
|
default:
|
||||||
|
return QuoteFeeTreatmentUnspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ const (
|
|||||||
QuoteSettlementModeFixReceived QuoteSettlementMode = "fix_received"
|
QuoteSettlementModeFixReceived QuoteSettlementMode = "fix_received"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type QuoteFeeTreatment string
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuoteFeeTreatmentUnspecified QuoteFeeTreatment = "unspecified"
|
||||||
|
QuoteFeeTreatmentAddToSource QuoteFeeTreatment = "add_to_source"
|
||||||
|
QuoteFeeTreatmentDeductFromDestination QuoteFeeTreatment = "deduct_from_destination"
|
||||||
|
)
|
||||||
|
|
||||||
type QuoteEndpointType string
|
type QuoteEndpointType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -75,6 +83,7 @@ type QuoteIntent struct {
|
|||||||
Amount *paymenttypes.Money
|
Amount *paymenttypes.Money
|
||||||
Comment string
|
Comment string
|
||||||
SettlementMode QuoteSettlementMode
|
SettlementMode QuoteSettlementMode
|
||||||
|
FeeTreatment QuoteFeeTreatment
|
||||||
SettlementCurrency string
|
SettlementCurrency string
|
||||||
RequiresFX bool
|
RequiresFX bool
|
||||||
Attributes map[string]string
|
Attributes map[string]string
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import (
|
|||||||
|
|
||||||
pkgmodel "github.com/tech/sendico/pkg/model"
|
pkgmodel "github.com/tech/sendico/pkg/model"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||||
pboundv1 "github.com/tech/sendico/pkg/proto/common/permission_bound/v1"
|
pboundv1 "github.com/tech/sendico/pkg/proto/common/permission_bound/v1"
|
||||||
storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1"
|
storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1"
|
||||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||||
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
|
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
|
||||||
transferv1 "github.com/tech/sendico/pkg/proto/payments/transfer/v1"
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
@@ -20,7 +21,7 @@ import (
|
|||||||
func TestHydrateOne_SuccessWithInlineMethods(t *testing.T) {
|
func TestHydrateOne_SuccessWithInlineMethods(t *testing.T) {
|
||||||
h := New(nil, WithRefFactory(func() string { return "q-intent-1" }))
|
h := New(nil, WithRefFactory(func() string { return "q-intent-1" }))
|
||||||
tag := "12345"
|
tag := "12345"
|
||||||
intent := &transferv1.TransferIntent{
|
intent := "ationv2.QuoteIntent{
|
||||||
Source: &endpointv1.PaymentEndpoint{
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||||
PaymentMethod: &endpointv1.PaymentMethod{
|
PaymentMethod: &endpointv1.PaymentMethod{
|
||||||
@@ -86,6 +87,12 @@ func TestHydrateOne_SuccessWithInlineMethods(t *testing.T) {
|
|||||||
if got.SettlementCurrency != "USDT" {
|
if got.SettlementCurrency != "USDT" {
|
||||||
t.Fatalf("expected settlement currency USDT, got %q", got.SettlementCurrency)
|
t.Fatalf("expected settlement currency USDT, got %q", got.SettlementCurrency)
|
||||||
}
|
}
|
||||||
|
if got.SettlementMode != QuoteSettlementModeFixSource {
|
||||||
|
t.Fatalf("expected default settlement mode fix_source, got %s", got.SettlementMode)
|
||||||
|
}
|
||||||
|
if got.FeeTreatment != QuoteFeeTreatmentAddToSource {
|
||||||
|
t.Fatalf("expected default fee treatment add_to_source, got %s", got.FeeTreatment)
|
||||||
|
}
|
||||||
if got.Amount == nil || got.Amount.Amount != "10.25" {
|
if got.Amount == nil || got.Amount.Amount != "10.25" {
|
||||||
t.Fatalf("unexpected amount: %#v", got.Amount)
|
t.Fatalf("unexpected amount: %#v", got.Amount)
|
||||||
}
|
}
|
||||||
@@ -120,7 +127,7 @@ func TestHydrateOne_ResolvesPaymentMethodRefViaPrivateMethod(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
intent := &transferv1.TransferIntent{
|
intent := "ationv2.QuoteIntent{
|
||||||
Source: &endpointv1.PaymentEndpoint{
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{PaymentMethodRef: methodRef},
|
Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{PaymentMethodRef: methodRef},
|
||||||
},
|
},
|
||||||
@@ -201,7 +208,7 @@ func TestHydrateOne_ResolvesPayeeRefViaPrivateMethod(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
intent := &transferv1.TransferIntent{
|
intent := "ationv2.QuoteIntent{
|
||||||
Source: &endpointv1.PaymentEndpoint{
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||||
PaymentMethod: &endpointv1.PaymentMethod{
|
PaymentMethod: &endpointv1.PaymentMethod{
|
||||||
@@ -275,7 +282,7 @@ func TestHydrateOne_ResolvesInlineAccountPaymentMethodViaPrivateMethod(t *testin
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
intent := &transferv1.TransferIntent{
|
intent := "ationv2.QuoteIntent{
|
||||||
Source: &endpointv1.PaymentEndpoint{
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||||
PaymentMethod: &endpointv1.PaymentMethod{
|
PaymentMethod: &endpointv1.PaymentMethod{
|
||||||
@@ -327,7 +334,7 @@ func TestHydrateOne_ResolvesInlineAccountPaymentMethodViaPrivateMethod(t *testin
|
|||||||
|
|
||||||
func TestHydrateOne_ReferenceWithoutMethodsClientFails(t *testing.T) {
|
func TestHydrateOne_ReferenceWithoutMethodsClientFails(t *testing.T) {
|
||||||
h := New(nil)
|
h := New(nil)
|
||||||
intent := &transferv1.TransferIntent{
|
intent := "ationv2.QuoteIntent{
|
||||||
Source: &endpointv1.PaymentEndpoint{
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{PaymentMethodRef: bson.NewObjectID().Hex()},
|
Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{PaymentMethodRef: bson.NewObjectID().Hex()},
|
||||||
},
|
},
|
||||||
@@ -356,7 +363,7 @@ func TestHydrateOne_ReferenceWithoutMethodsClientFails(t *testing.T) {
|
|||||||
|
|
||||||
func TestHydrateMany_IndexesError(t *testing.T) {
|
func TestHydrateMany_IndexesError(t *testing.T) {
|
||||||
h := New(nil)
|
h := New(nil)
|
||||||
intents := []*transferv1.TransferIntent{
|
intents := []*quotationv2.QuoteIntent{
|
||||||
{
|
{
|
||||||
Source: &endpointv1.PaymentEndpoint{
|
Source: &endpointv1.PaymentEndpoint{
|
||||||
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||||
@@ -422,6 +429,26 @@ func TestHydrateMany_IndexesError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHydrateOne_RejectsConflictingEconomics(t *testing.T) {
|
||||||
|
h := New(nil)
|
||||||
|
intent := "ationv2.QuoteIntent{
|
||||||
|
Source: endpointWithMethodRef(bson.NewObjectID().Hex()),
|
||||||
|
Destination: endpointWithMethodRef(bson.NewObjectID().Hex()),
|
||||||
|
Amount: newMoney("1", "USD"),
|
||||||
|
SettlementMode: paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
|
||||||
|
FeeTreatment: quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := h.HydrateOne(context.Background(), HydrateOneInput{
|
||||||
|
OrganizationRef: bson.NewObjectID().Hex(),
|
||||||
|
InitiatorRef: bson.NewObjectID().Hex(),
|
||||||
|
Intent: intent,
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "fee_treatment conflicts with settlement_mode") {
|
||||||
|
t.Fatalf("expected settlement/fee conflict error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type fakeMethodsClient struct {
|
type fakeMethodsClient struct {
|
||||||
getPaymentMethodPrivateFn func(context.Context, *methodsv1.GetPaymentMethodPrivateRequest, ...grpc.CallOption) (*methodsv1.GetPaymentMethodPrivateResponse, error)
|
getPaymentMethodPrivateFn func(context.Context, *methodsv1.GetPaymentMethodPrivateRequest, ...grpc.CallOption) (*methodsv1.GetPaymentMethodPrivateResponse, error)
|
||||||
}
|
}
|
||||||
@@ -444,6 +471,14 @@ func newMoney(amount, currency string) *moneyv1.Money {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func endpointWithMethodRef(methodRef string) *endpointv1.PaymentEndpoint {
|
||||||
|
return &endpointv1.PaymentEndpoint{
|
||||||
|
Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{
|
||||||
|
PaymentMethodRef: methodRef,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustMarshalBSON(t *testing.T, value any) []byte {
|
func mustMarshalBSON(t *testing.T, value any) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
data, err := bson.Marshal(value)
|
data, err := bson.Marshal(value)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ package common.payment.v1;
|
|||||||
|
|
||||||
option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1";
|
option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1";
|
||||||
|
|
||||||
import "google/protobuf/wrappers.proto";
|
|
||||||
|
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Card network (payment system)
|
// Card network (payment system)
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;payment
|
|||||||
// SettlementMode defines how to treat fees/FX variance for payouts.
|
// SettlementMode defines how to treat fees/FX variance for payouts.
|
||||||
enum SettlementMode {
|
enum SettlementMode {
|
||||||
SETTLEMENT_UNSPECIFIED = 0;
|
SETTLEMENT_UNSPECIFIED = 0;
|
||||||
SETTLEMENT_FIX_SOURCE = 1; // customer pays fees; sent amount fixed
|
SETTLEMENT_FIX_SOURCE = 1;
|
||||||
SETTLEMENT_FIX_RECEIVED = 2; // receiver gets fixed amount; source flexes
|
SETTLEMENT_FIX_RECEIVED = 2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import "google/protobuf/timestamp.proto";
|
|||||||
import "api/proto/common/storable/v1/storable.proto";
|
import "api/proto/common/storable/v1/storable.proto";
|
||||||
import "api/proto/common/money/v1/money.proto";
|
import "api/proto/common/money/v1/money.proto";
|
||||||
import "api/proto/common/payment/v1/asset.proto";
|
import "api/proto/common/payment/v1/asset.proto";
|
||||||
|
import "api/proto/common/payment/v1/settlement.proto";
|
||||||
import "api/proto/billing/fees/v1/fees.proto";
|
import "api/proto/billing/fees/v1/fees.proto";
|
||||||
import "api/proto/oracle/v1/oracle.proto";
|
import "api/proto/oracle/v1/oracle.proto";
|
||||||
|
|
||||||
enum QuoteState {
|
enum QuoteState {
|
||||||
QUOTE_STATE_UNSPECIFIED = 0;
|
QUOTE_STATE_UNSPECIFIED = 0;
|
||||||
|
|
||||||
QUOTE_STATE_INDICATIVE = 1;
|
QUOTE_STATE_INDICATIVE = 1;
|
||||||
QUOTE_STATE_EXECUTABLE = 2;
|
QUOTE_STATE_EXECUTABLE = 2;
|
||||||
QUOTE_STATE_BLOCKED = 3;
|
QUOTE_STATE_BLOCKED = 3;
|
||||||
@@ -22,7 +22,6 @@ enum QuoteState {
|
|||||||
|
|
||||||
enum QuoteBlockReason {
|
enum QuoteBlockReason {
|
||||||
QUOTE_BLOCK_REASON_UNSPECIFIED = 0;
|
QUOTE_BLOCK_REASON_UNSPECIFIED = 0;
|
||||||
|
|
||||||
QUOTE_BLOCK_REASON_ROUTE_UNAVAILABLE = 1;
|
QUOTE_BLOCK_REASON_ROUTE_UNAVAILABLE = 1;
|
||||||
QUOTE_BLOCK_REASON_LIMIT_BLOCKED = 2;
|
QUOTE_BLOCK_REASON_LIMIT_BLOCKED = 2;
|
||||||
QUOTE_BLOCK_REASON_RISK_BLOCKED = 3;
|
QUOTE_BLOCK_REASON_RISK_BLOCKED = 3;
|
||||||
@@ -46,6 +45,12 @@ enum RouteHopRole {
|
|||||||
ROUTE_HOP_ROLE_DESTINATION = 3;
|
ROUTE_HOP_ROLE_DESTINATION = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FeeTreatment {
|
||||||
|
FEE_TREATMENT_UNSPECIFIED = 0;
|
||||||
|
FEE_TREATMENT_ADD_TO_SOURCE = 1;
|
||||||
|
FEE_TREATMENT_DEDUCT_FROM_DESTINATION = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message RouteHop {
|
message RouteHop {
|
||||||
uint32 index = 1;
|
uint32 index = 1;
|
||||||
string rail = 2;
|
string rail = 2;
|
||||||
@@ -67,13 +72,11 @@ message RouteSpecification {
|
|||||||
string rail = 1;
|
string rail = 1;
|
||||||
string provider = 2;
|
string provider = 2;
|
||||||
string payout_method = 3;
|
string payout_method = 3;
|
||||||
reserved 4, 5;
|
string network = 4;
|
||||||
reserved "settlement_asset", "settlement_model";
|
string route_ref = 5;
|
||||||
string network = 6;
|
string pricing_profile_ref = 6;
|
||||||
string route_ref = 7;
|
repeated RouteHop hops = 7;
|
||||||
string pricing_profile_ref = 8;
|
RouteSettlement settlement = 8;
|
||||||
repeated RouteHop hops = 9;
|
|
||||||
RouteSettlement settlement = 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execution assumptions and constraints evaluated at quotation time.
|
// Execution assumptions and constraints evaluated at quotation time.
|
||||||
@@ -91,24 +94,27 @@ message ExecutionConditions {
|
|||||||
message PaymentQuote {
|
message PaymentQuote {
|
||||||
common.storable.v1.Storable storable = 1;
|
common.storable.v1.Storable storable = 1;
|
||||||
QuoteState state = 2;
|
QuoteState state = 2;
|
||||||
QuoteBlockReason block_reason = 4;
|
QuoteBlockReason block_reason = 3;
|
||||||
reserved 3, 13;
|
|
||||||
reserved "kind", "lifecycle", "executable";
|
|
||||||
|
|
||||||
common.money.v1.Money debit_amount = 5;
|
// Transfer principal amount before fees.
|
||||||
common.money.v1.Money credit_amount = 6;
|
common.money.v1.Money transfer_principal_amount = 4;
|
||||||
|
// Expected destination settlement amount.
|
||||||
|
common.money.v1.Money destination_amount = 5;
|
||||||
|
|
||||||
repeated fees.v1.DerivedPostingLine fee_lines = 7;
|
repeated fees.v1.DerivedPostingLine fee_lines = 6;
|
||||||
repeated fees.v1.AppliedRule fee_rules = 8;
|
repeated fees.v1.AppliedRule fee_rules = 7;
|
||||||
|
|
||||||
oracle.v1.Quote fx_quote = 9;
|
oracle.v1.Quote fx_quote = 8;
|
||||||
|
|
||||||
string quote_ref = 10;
|
string quote_ref = 9;
|
||||||
|
|
||||||
google.protobuf.Timestamp expires_at = 11;
|
google.protobuf.Timestamp expires_at = 10;
|
||||||
google.protobuf.Timestamp priced_at = 12;
|
google.protobuf.Timestamp priced_at = 11;
|
||||||
|
|
||||||
RouteSpecification route = 14;
|
RouteSpecification route = 12;
|
||||||
ExecutionConditions execution_conditions = 15;
|
ExecutionConditions execution_conditions = 13;
|
||||||
common.money.v1.Money total_cost = 16;
|
// Total amount expected to be debited from payer side (principal +/- fee lines in source currency).
|
||||||
|
common.money.v1.Money payer_total_debit_amount = 14;
|
||||||
|
common.payment.v1.SettlementMode resolved_settlement_mode = 15;
|
||||||
|
FeeTreatment resolved_fee_treatment = 16;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,25 @@ package payments.quotation.v2;
|
|||||||
option go_package = "github.com/tech/sendico/pkg/proto/payments/quotation/v2;quotationv2";
|
option go_package = "github.com/tech/sendico/pkg/proto/payments/quotation/v2;quotationv2";
|
||||||
|
|
||||||
import "api/proto/payments/shared/v1/shared.proto";
|
import "api/proto/payments/shared/v1/shared.proto";
|
||||||
import "api/proto/payments/transfer/v1/transfer.proto";
|
import "api/proto/common/money/v1/money.proto";
|
||||||
|
import "api/proto/common/payment/v1/settlement.proto";
|
||||||
|
import "api/proto/payments/endpoint/v1/endpoint.proto";
|
||||||
import "api/proto/payments/quotation/v2/interface.proto";
|
import "api/proto/payments/quotation/v2/interface.proto";
|
||||||
|
|
||||||
|
message QuoteIntent {
|
||||||
|
payments.endpoint.v1.PaymentEndpoint source = 1;
|
||||||
|
payments.endpoint.v1.PaymentEndpoint destination = 2;
|
||||||
|
common.money.v1.Money amount = 3;
|
||||||
|
common.payment.v1.SettlementMode settlement_mode = 4;
|
||||||
|
payments.quotation.v2.FeeTreatment fee_treatment = 5;
|
||||||
|
string settlement_currency = 6;
|
||||||
|
string comment = 7;
|
||||||
|
}
|
||||||
|
|
||||||
message QuotePaymentRequest {
|
message QuotePaymentRequest {
|
||||||
payments.shared.v1.RequestMeta meta = 1;
|
payments.shared.v1.RequestMeta meta = 1;
|
||||||
string idempotency_key = 2;
|
string idempotency_key = 2;
|
||||||
payments.transfer.v1.TransferIntent intent = 3;
|
payments.quotation.v2.QuoteIntent intent = 3;
|
||||||
bool preview_only = 4;
|
bool preview_only = 4;
|
||||||
string initiator_ref = 5;
|
string initiator_ref = 5;
|
||||||
}
|
}
|
||||||
@@ -25,7 +36,7 @@ message QuotePaymentResponse {
|
|||||||
message QuotePaymentsRequest {
|
message QuotePaymentsRequest {
|
||||||
payments.shared.v1.RequestMeta meta = 1;
|
payments.shared.v1.RequestMeta meta = 1;
|
||||||
string idempotency_key = 2;
|
string idempotency_key = 2;
|
||||||
repeated payments.transfer.v1.TransferIntent intents = 3;
|
repeated payments.quotation.v2.QuoteIntent intents = 3;
|
||||||
bool preview_only = 4;
|
bool preview_only = 4;
|
||||||
string initiator_ref = 5;
|
string initiator_ref = 5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ replace github.com/tech/sendico/gateway/tron => ../gateway/tron
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.1
|
github.com/aws/aws-sdk-go-v2 v1.41.1
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.8
|
github.com/aws/aws-sdk-go-v2/config v1.32.9
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.9
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
@@ -64,7 +64,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||||
github.com/aws/smithy-go v1.24.0 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6ce
|
|||||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs=
|
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI=
|
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8 h1:Jp2JYH1lRT3KhX4mshHPvVYsR5qqRec3hGvEarNYoR0=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8/go.mod h1:fZG9tuvyVfxknv1rKibIz3DobRaFw1Poe8IKtXB3XYY=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
||||||
@@ -36,8 +36,8 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIM
|
|||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package sresponse
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
@@ -148,8 +149,8 @@ func toFeeLines(lines []*feesv1.DerivedPostingLine) []FeeLine {
|
|||||||
result = append(result, FeeLine{
|
result = append(result, FeeLine{
|
||||||
LedgerAccountRef: line.GetLedgerAccountRef(),
|
LedgerAccountRef: line.GetLedgerAccountRef(),
|
||||||
Amount: toMoney(line.GetMoney()),
|
Amount: toMoney(line.GetMoney()),
|
||||||
LineType: line.GetLineType().String(),
|
LineType: enumJSONName(line.GetLineType().String()),
|
||||||
Side: line.GetSide().String(),
|
Side: enumJSONName(line.GetSide().String()),
|
||||||
Meta: line.GetMeta(),
|
Meta: line.GetMeta(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -178,7 +179,7 @@ func toFxQuote(q *oraclev1.Quote) *FxQuote {
|
|||||||
QuoteRef: q.GetQuoteRef(),
|
QuoteRef: q.GetQuoteRef(),
|
||||||
BaseCurrency: base,
|
BaseCurrency: base,
|
||||||
QuoteCurrency: quote,
|
QuoteCurrency: quote,
|
||||||
Side: q.GetSide().String(),
|
Side: enumJSONName(q.GetSide().String()),
|
||||||
Price: q.GetPrice().GetValue(),
|
Price: q.GetPrice().GetValue(),
|
||||||
BaseAmount: toMoney(q.GetBaseAmount()),
|
BaseAmount: toMoney(q.GetBaseAmount()),
|
||||||
QuoteAmount: toMoney(q.GetQuoteAmount()),
|
QuoteAmount: toMoney(q.GetQuoteAmount()),
|
||||||
@@ -260,11 +261,15 @@ func toPayment(p *sharedv1.Payment) *Payment {
|
|||||||
return &Payment{
|
return &Payment{
|
||||||
PaymentRef: p.GetPaymentRef(),
|
PaymentRef: p.GetPaymentRef(),
|
||||||
IdempotencyKey: p.GetIdempotencyKey(),
|
IdempotencyKey: p.GetIdempotencyKey(),
|
||||||
State: p.GetState().String(),
|
State: enumJSONName(p.GetState().String()),
|
||||||
FailureCode: p.GetFailureCode().String(),
|
FailureCode: enumJSONName(p.GetFailureCode().String()),
|
||||||
FailureReason: p.GetFailureReason(),
|
FailureReason: p.GetFailureReason(),
|
||||||
LastQuote: toPaymentQuote(p.GetLastQuote()),
|
LastQuote: toPaymentQuote(p.GetLastQuote()),
|
||||||
CreatedAt: p.GetCreatedAt().AsTime(),
|
CreatedAt: p.GetCreatedAt().AsTime(),
|
||||||
Meta: p.GetMetadata(),
|
Meta: p.GetMetadata(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enumJSONName(value string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(value))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user