From 2e08ec9b9b6da27771e8d4fb0cba26cb61f3d1cd Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 24 Feb 2026 16:39:08 +0100 Subject: [PATCH] fee treatment added --- .../service/gateway/confirmation_flow.go | 2 +- .../internal/service/gateway/service.go | 24 ------ .../service/orchestrationv2/prepo/document.go | 5 +- .../service/orchestrator/service_v2.go | 3 +- .../interface/api/srequest/payment_enums.go | 9 ++ .../interface/api/srequest/payment_intent.go | 1 + .../internal/server/paymentapiimp/mapper.go | 18 ++++ .../mapper_fee_treatment_test.go | 82 +++++++++++++++++++ .../lib/data/dto/payment/intent/payment.dart | 4 + .../lib/data/mapper/payment/enums.dart | 1 + .../payment/{fee_line.dart => fees/line.dart} | 0 .../data/mapper/payment/fees/treatment.dart | 26 ++++++ .../data/mapper/payment/intent/payment.dart | 3 + .../lib/models/payment/fees/treatment.dart | 5 ++ .../pshared/lib/models/payment/intent.dart | 3 + .../payment/quotation/intent_builder.dart | 4 +- .../payment/multiple_intent_builder.dart | 2 + 17 files changed, 162 insertions(+), 30 deletions(-) create mode 100644 api/server/internal/server/paymentapiimp/mapper_fee_treatment_test.go rename frontend/pshared/lib/data/mapper/payment/{fee_line.dart => fees/line.dart} (100%) create mode 100644 frontend/pshared/lib/data/mapper/payment/fees/treatment.dart create mode 100644 frontend/pshared/lib/models/payment/fees/treatment.dart diff --git a/api/gateway/tgsettle/internal/service/gateway/confirmation_flow.go b/api/gateway/tgsettle/internal/service/gateway/confirmation_flow.go index 150a4868..5750a20e 100644 --- a/api/gateway/tgsettle/internal/service/gateway/confirmation_flow.go +++ b/api/gateway/tgsettle/internal/service/gateway/confirmation_flow.go @@ -313,7 +313,7 @@ func (s *Service) publishPendingConfirmationResult(pending *storagemodel.Pending return nil } -func (s *Service) sendTelegramText(ctx context.Context, request *model.TelegramTextRequest) error { +func (s *Service) sendTelegramText(_ context.Context, request *model.TelegramTextRequest) error { if request == nil { return merrors.InvalidArgument("telegram text request is nil", "request") } diff --git a/api/gateway/tgsettle/internal/service/gateway/service.go b/api/gateway/tgsettle/internal/service/gateway/service.go index 2a63a075..3d5392f1 100644 --- a/api/gateway/tgsettle/internal/service/gateway/service.go +++ b/api/gateway/tgsettle/internal/service/gateway/service.go @@ -756,27 +756,3 @@ func readEnv(env string) string { } var _ grpcapp.Service = (*Service)(nil) - -func statusFromConfirmationResult(r *model.ConfirmationResult) storagemodel.PaymentStatus { - if r == nil { - return storagemodel.PaymentStatusWaiting - } - - switch r.Status { - - case model.ConfirmationStatusConfirmed: - return storagemodel.PaymentStatusProcessing - - case model.ConfirmationStatusClarified: - return storagemodel.PaymentStatusWaiting - - case model.ConfirmationStatusRejected: - return storagemodel.PaymentStatusFailed - - case model.ConfirmationStatusTimeout: - return storagemodel.PaymentStatusFailed - - default: - return storagemodel.PaymentStatusFailed - } -} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go index d669758b..c06f6956 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go @@ -8,11 +8,10 @@ import ( "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/db/storable" pm "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" "go.mongodb.org/mongo-driver/v2/bson" ) -const paymentsV2Collection = "payments_v2" - type paymentDocument struct { storable.Base `bson:",inline"` pm.OrganizationBoundBase `bson:",inline"` @@ -28,7 +27,7 @@ type paymentDocument struct { } func (*paymentDocument) Collection() string { - return paymentsV2Collection + return mservice.Payments } func toDocument(payment *agg.Payment) (*paymentDocument, error) { diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_v2.go b/api/payments/orchestrator/internal/service/orchestrator/service_v2.go index ed6281a4..3df78c32 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_v2.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_v2.go @@ -9,6 +9,7 @@ import ( "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc" "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" "go.mongodb.org/mongo-driver/v2/mongo" "go.uber.org/zap" @@ -81,7 +82,7 @@ func buildPaymentRepositoryV2(repo storage.Repository, logger mlogger.Logger) pr return nil } paymentRepo, err := prepo.NewMongo( - db.Collection("payments_v2"), + db.Collection(mservice.Payments), prepo.Dependencies{Logger: logger.Named("orchestration_v2_prepo")}, ) if err != nil { diff --git a/api/server/interface/api/srequest/payment_enums.go b/api/server/interface/api/srequest/payment_enums.go index 267bc449..ab3fe6af 100644 --- a/api/server/interface/api/srequest/payment_enums.go +++ b/api/server/interface/api/srequest/payment_enums.go @@ -20,6 +20,15 @@ const ( SettlementModeFixReceived SettlementMode = "fix_received" ) +// FeeTreatment controls where fee impact is applied by quotation. +type FeeTreatment string + +const ( + FeeTreatmentUnspecified FeeTreatment = "unspecified" + FeeTreatmentAddToSource FeeTreatment = "add_to_source" + FeeTreatmentDeductFromDestination FeeTreatment = "deduct_from_destination" +) + // FXSide mirrors the common FX side enum. type FXSide string diff --git a/api/server/interface/api/srequest/payment_intent.go b/api/server/interface/api/srequest/payment_intent.go index 13c24d97..272dd311 100644 --- a/api/server/interface/api/srequest/payment_intent.go +++ b/api/server/interface/api/srequest/payment_intent.go @@ -14,6 +14,7 @@ type PaymentIntent struct { Amount *paymenttypes.Money `json:"amount,omitempty"` FX *FXIntent `json:"fx,omitempty"` SettlementMode SettlementMode `json:"settlement_mode,omitempty"` + FeeTreatment FeeTreatment `json:"fee_treatment,omitempty"` SettlementCurrency string `json:"settlement_currency,omitempty"` Attributes map[string]string `json:"attributes,omitempty"` Customer *Customer `json:"customer,omitempty"` diff --git a/api/server/internal/server/paymentapiimp/mapper.go b/api/server/internal/server/paymentapiimp/mapper.go index a9be4fff..2620c71e 100644 --- a/api/server/internal/server/paymentapiimp/mapper.go +++ b/api/server/internal/server/paymentapiimp/mapper.go @@ -28,6 +28,10 @@ func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, e if err != nil { return nil, err } + feeTreatment, err := mapFeeTreatment(intent.FeeTreatment) + if err != nil { + return nil, err + } settlementCurrency := strings.TrimSpace(intent.SettlementCurrency) if settlementCurrency == "" { settlementCurrency = resolveSettlementCurrency(intent) @@ -47,6 +51,7 @@ func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, e Destination: destination, Amount: mapMoney(intent.Amount), SettlementMode: settlementMode, + FeeTreatment: feeTreatment, SettlementCurrency: settlementCurrency, } if comment := strings.TrimSpace(intent.Attributes["comment"]); comment != "" { @@ -250,6 +255,19 @@ func mapSettlementMode(mode srequest.SettlementMode) (paymentv1.SettlementMode, } } +func mapFeeTreatment(treatment srequest.FeeTreatment) (quotationv2.FeeTreatment, error) { + switch strings.TrimSpace(string(treatment)) { + case "", string(srequest.FeeTreatmentUnspecified): + return quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED, nil + case string(srequest.FeeTreatmentAddToSource): + return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE, nil + case string(srequest.FeeTreatmentDeductFromDestination): + return quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION, nil + default: + return quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED, merrors.InvalidArgument("unsupported fee treatment: " + string(treatment)) + } +} + func mapChainNetwork(chain srequest.ChainNetwork) (chainv1.ChainNetwork, error) { switch strings.TrimSpace(string(chain)) { case "", string(srequest.ChainNetworkUnspecified): diff --git a/api/server/internal/server/paymentapiimp/mapper_fee_treatment_test.go b/api/server/internal/server/paymentapiimp/mapper_fee_treatment_test.go new file mode 100644 index 00000000..3df16d71 --- /dev/null +++ b/api/server/internal/server/paymentapiimp/mapper_fee_treatment_test.go @@ -0,0 +1,82 @@ +package paymentapiimp + +import ( + "testing" + + paymenttypes "github.com/tech/sendico/pkg/payments/types" + quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2" + "github.com/tech/sendico/server/interface/api/srequest" +) + +func TestMapQuoteIntent_PropagatesFeeTreatment(t *testing.T) { + source, err := srequest.NewManagedWalletEndpointDTO(srequest.ManagedWalletEndpoint{ + ManagedWalletRef: "wallet-source-1", + }, nil) + if err != nil { + t.Fatalf("failed to build source endpoint: %v", err) + } + + destination, err := srequest.NewCardEndpointDTO(srequest.CardEndpoint{ + Pan: "2200700142860161", + FirstName: "John", + LastName: "Doe", + ExpMonth: 3, + ExpYear: 2030, + }, nil) + if err != nil { + t.Fatalf("failed to build destination endpoint: %v", err) + } + + intent := &srequest.PaymentIntent{ + Kind: srequest.PaymentKindPayout, + Source: &source, + Destination: &destination, + Amount: &paymenttypes.Money{Amount: "10", Currency: "USDT"}, + SettlementMode: srequest.SettlementModeFixReceived, + FeeTreatment: srequest.FeeTreatmentDeductFromDestination, + } + + got, err := mapQuoteIntent(intent) + if err != nil { + t.Fatalf("mapQuoteIntent returned error: %v", err) + } + if got == nil { + t.Fatalf("expected mapped quote intent") + } + if got.GetFeeTreatment() != quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION { + t.Fatalf("unexpected fee treatment: got=%s", got.GetFeeTreatment().String()) + } +} + +func TestMapQuoteIntent_InvalidFeeTreatmentFails(t *testing.T) { + source, err := srequest.NewManagedWalletEndpointDTO(srequest.ManagedWalletEndpoint{ + ManagedWalletRef: "wallet-source-1", + }, nil) + if err != nil { + t.Fatalf("failed to build source endpoint: %v", err) + } + + destination, err := srequest.NewCardEndpointDTO(srequest.CardEndpoint{ + Pan: "2200700142860161", + FirstName: "John", + LastName: "Doe", + ExpMonth: 3, + ExpYear: 2030, + }, nil) + if err != nil { + t.Fatalf("failed to build destination endpoint: %v", err) + } + + intent := &srequest.PaymentIntent{ + Kind: srequest.PaymentKindPayout, + Source: &source, + Destination: &destination, + Amount: &paymenttypes.Money{Amount: "10", Currency: "USDT"}, + SettlementMode: srequest.SettlementModeFixSource, + FeeTreatment: srequest.FeeTreatment("wrong_value"), + } + + if _, err := mapQuoteIntent(intent); err == nil { + t.Fatalf("expected error for invalid fee treatment") + } +} diff --git a/frontend/pshared/lib/data/dto/payment/intent/payment.dart b/frontend/pshared/lib/data/dto/payment/intent/payment.dart index ebb57fdc..997b5f9d 100644 --- a/frontend/pshared/lib/data/dto/payment/intent/payment.dart +++ b/frontend/pshared/lib/data/dto/payment/intent/payment.dart @@ -23,6 +23,9 @@ class PaymentIntentDTO { @JsonKey(name: 'settlement_currency') final String? settlementCurrency; + @JsonKey(name: "fee_treatment") + final String? feeTreatment; + final Map? attributes; final CustomerDTO? customer; @@ -36,6 +39,7 @@ class PaymentIntentDTO { this.settlementCurrency, this.attributes, this.customer, + this.feeTreatment, }); factory PaymentIntentDTO.fromJson(Map json) => _$PaymentIntentDTOFromJson(json); diff --git a/frontend/pshared/lib/data/mapper/payment/enums.dart b/frontend/pshared/lib/data/mapper/payment/enums.dart index 65581395..6be3e33d 100644 --- a/frontend/pshared/lib/data/mapper/payment/enums.dart +++ b/frontend/pshared/lib/data/mapper/payment/enums.dart @@ -5,6 +5,7 @@ import 'package:pshared/models/payment/kind.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/payment/settlement_mode.dart'; + PaymentKind paymentKindFromValue(String? value) { switch (value) { case 'payout': diff --git a/frontend/pshared/lib/data/mapper/payment/fee_line.dart b/frontend/pshared/lib/data/mapper/payment/fees/line.dart similarity index 100% rename from frontend/pshared/lib/data/mapper/payment/fee_line.dart rename to frontend/pshared/lib/data/mapper/payment/fees/line.dart diff --git a/frontend/pshared/lib/data/mapper/payment/fees/treatment.dart b/frontend/pshared/lib/data/mapper/payment/fees/treatment.dart new file mode 100644 index 00000000..395930d3 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/payment/fees/treatment.dart @@ -0,0 +1,26 @@ +import 'package:pshared/models/payment/fees/treatment.dart'; + + +FeeTreatment feeTreatmentFromValue(String? value) { + switch (value) { + case 'add_to_source': + return FeeTreatment.addToSource; + case 'deduct_from_destination': + return FeeTreatment.deductFromDestination; + case 'unspecified': + return FeeTreatment.unspecified; + default: + throw ArgumentError('Unknown FeeTreatment value: $value'); + } +} + +String feeTreatmentToValue(FeeTreatment value) { + switch (value) { + case FeeTreatment.addToSource: + return 'add_to_source'; + case FeeTreatment.deductFromDestination: + return 'deduct_from_destination'; + case FeeTreatment.unspecified: + return 'unspecified'; + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/mapper/payment/intent/payment.dart b/frontend/pshared/lib/data/mapper/payment/intent/payment.dart index bab5acbd..2170e7f9 100644 --- a/frontend/pshared/lib/data/mapper/payment/intent/payment.dart +++ b/frontend/pshared/lib/data/mapper/payment/intent/payment.dart @@ -1,4 +1,5 @@ import 'package:pshared/data/dto/payment/intent/payment.dart'; +import 'package:pshared/data/mapper/payment/fees/treatment.dart'; import 'package:pshared/data/mapper/payment/payment.dart'; import 'package:pshared/data/mapper/payment/enums.dart'; import 'package:pshared/data/mapper/payment/intent/customer.dart'; @@ -18,6 +19,7 @@ extension PaymentIntentMapper on PaymentIntent { settlementCurrency: settlementCurrency, attributes: attributes, customer: customer?.toDTO(), + feeTreatment: feeTreatmentToValue(feeTreatment), ); } @@ -32,5 +34,6 @@ extension PaymentIntentDTOMapper on PaymentIntentDTO { settlementCurrency: settlementCurrency, attributes: attributes, customer: customer?.toDomain(), + feeTreatment: feeTreatmentFromValue(feeTreatment), ); } diff --git a/frontend/pshared/lib/models/payment/fees/treatment.dart b/frontend/pshared/lib/models/payment/fees/treatment.dart new file mode 100644 index 00000000..9ae7c9b7 --- /dev/null +++ b/frontend/pshared/lib/models/payment/fees/treatment.dart @@ -0,0 +1,5 @@ +enum FeeTreatment { + addToSource, + deductFromDestination, + unspecified, +} diff --git a/frontend/pshared/lib/models/payment/intent.dart b/frontend/pshared/lib/models/payment/intent.dart index aeef6177..d611c477 100644 --- a/frontend/pshared/lib/models/payment/intent.dart +++ b/frontend/pshared/lib/models/payment/intent.dart @@ -1,3 +1,4 @@ +import 'package:pshared/models/payment/fees/treatment.dart'; import 'package:pshared/models/payment/fx/intent.dart'; import 'package:pshared/models/payment/kind.dart'; import 'package:pshared/models/payment/customer.dart'; @@ -12,6 +13,7 @@ class PaymentIntent { final PaymentMethodData? destination; final Money? amount; final FxIntent? fx; + final FeeTreatment feeTreatment; final SettlementMode settlementMode; final String? settlementCurrency; final Map? attributes; @@ -27,5 +29,6 @@ class PaymentIntent { this.settlementCurrency, this.attributes, this.customer, + required this.feeTreatment, }); } diff --git a/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart b/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart index 70b71e10..8d4fef4a 100644 --- a/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart +++ b/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart @@ -2,6 +2,7 @@ import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/models/payment/asset.dart'; import 'package:pshared/models/payment/chain_network.dart'; import 'package:pshared/models/payment/customer.dart'; +import 'package:pshared/models/payment/fees/treatment.dart'; import 'package:pshared/models/payment/kind.dart'; import 'package:pshared/models/payment/methods/card.dart'; import 'package:pshared/models/payment/methods/crypto_address.dart'; @@ -63,7 +64,8 @@ class QuotationIntentBuilder { ) ), fx: fxIntent, - settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource, + feeTreatment: payment.payerCoversFee ? FeeTreatment.addToSource : FeeTreatment.deductFromDestination, + settlementMode: SettlementMode.fixSource, settlementCurrency: FxIntentHelper.resolveSettlementCurrency( amount: amount, fx: fxIntent, diff --git a/frontend/pweb/lib/utils/payment/multiple_intent_builder.dart b/frontend/pweb/lib/utils/payment/multiple_intent_builder.dart index ea615a07..c08bc471 100644 --- a/frontend/pweb/lib/utils/payment/multiple_intent_builder.dart +++ b/frontend/pweb/lib/utils/payment/multiple_intent_builder.dart @@ -1,6 +1,7 @@ import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/asset.dart'; import 'package:pshared/models/payment/chain_network.dart'; +import 'package:pshared/models/payment/fees/treatment.dart'; import 'package:pshared/models/payment/intent.dart'; import 'package:pshared/models/payment/kind.dart'; import 'package:pshared/models/payment/methods/card.dart'; @@ -52,6 +53,7 @@ class MultipleIntentBuilder { expYear: row.expYear, ), amount: amount, + feeTreatment: FeeTreatment.addToSource, settlementMode: SettlementMode.fixReceived, settlementCurrency: FxIntentHelper.resolveSettlementCurrency( amount: amount,