Compare commits
6 Commits
bc76cfe063
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b481de9ffc | |||
|
|
0c29e7686d | ||
| 5b26a70a15 | |||
|
|
b832c2a7c4 | ||
| 15393765b9 | |||
|
|
440b6a2553 |
41
Makefile
41
Makefile
@@ -1,10 +1,31 @@
|
||||
# Sendico Development Environment - Makefile
|
||||
# Docker Compose + Makefile build system
|
||||
|
||||
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend
|
||||
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend backend-up backend-down backend-rebuild
|
||||
|
||||
COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev
|
||||
SERVICE ?=
|
||||
BACKEND_SERVICES := \
|
||||
dev-discovery \
|
||||
dev-fx-oracle \
|
||||
dev-fx-ingestor \
|
||||
dev-billing-fees \
|
||||
dev-billing-documents \
|
||||
dev-ledger \
|
||||
dev-payments-orchestrator \
|
||||
dev-payments-quotation \
|
||||
dev-payments-methods \
|
||||
dev-chain-gateway-vault-agent \
|
||||
dev-chain-gateway \
|
||||
dev-tron-gateway-vault-agent \
|
||||
dev-tron-gateway \
|
||||
dev-aurora-gateway \
|
||||
dev-tgsettle-gateway \
|
||||
dev-notification \
|
||||
dev-callbacks-vault-agent \
|
||||
dev-callbacks \
|
||||
dev-bff-vault-agent \
|
||||
dev-bff
|
||||
|
||||
# Colors
|
||||
GREEN := \033[0;32m
|
||||
@@ -31,6 +52,9 @@ help:
|
||||
@echo "$(YELLOW)Selective Operations:$(NC)"
|
||||
@echo " make infra-up Start infrastructure only (mongo, nats, vault)"
|
||||
@echo " make services-up Start application services only"
|
||||
@echo " make backend-up Start backend services only (no infrastructure/frontend)"
|
||||
@echo " make backend-down Stop backend services only"
|
||||
@echo " make backend-rebuild Rebuild and restart backend services only"
|
||||
@echo " make list-services List all available services"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Build Groups:$(NC)"
|
||||
@@ -229,6 +253,21 @@ services-up:
|
||||
dev-bff \
|
||||
dev-frontend
|
||||
|
||||
# Backend services only (no infrastructure, no frontend)
|
||||
backend-up:
|
||||
@echo "$(GREEN)Starting backend services only (no infra changes)...$(NC)"
|
||||
@$(COMPOSE) up -d --no-deps $(BACKEND_SERVICES)
|
||||
|
||||
backend-down:
|
||||
@echo "$(YELLOW)Stopping backend services only...$(NC)"
|
||||
@$(COMPOSE) stop $(BACKEND_SERVICES)
|
||||
|
||||
backend-rebuild:
|
||||
@echo "$(GREEN)Rebuilding backend services only (no infra changes)...$(NC)"
|
||||
@$(COMPOSE) build $(BACKEND_SERVICES)
|
||||
@$(COMPOSE) up -d --no-deps --force-recreate $(BACKEND_SERVICES)
|
||||
@echo "$(GREEN)✅ Backend services rebuilt$(NC)"
|
||||
|
||||
# Status check
|
||||
status:
|
||||
@$(COMPOSE) ps
|
||||
|
||||
21
README.md
21
README.md
@@ -24,6 +24,7 @@ Financial services platform providing payment orchestration, ledger accounting,
|
||||
| FX Ingestor | `api/fx/ingestor/` | FX rate ingestion |
|
||||
| Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway |
|
||||
| Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway |
|
||||
| Gateway Aurora | `api/gateway/aurora/` | Card payouts simulator |
|
||||
| Gateway MNTX | `api/gateway/mntx/` | Card payouts |
|
||||
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX |
|
||||
| Notification | `api/notification/` | Notifications |
|
||||
@@ -31,6 +32,16 @@ Financial services platform providing payment orchestration, ledger accounting,
|
||||
| Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery |
|
||||
| Frontend | `frontend/pweb/` | Flutter web UI |
|
||||
|
||||
Gateway note: current dev compose workflows (`make services-up`, `make build-gateways`) use Aurora for card-payout flows (`chain`, `tron`, `aurora`, `tgsettle`). The MNTX gateway codebase is retained separately for Monetix-specific integration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker with Docker Compose plugin
|
||||
- GNU Make
|
||||
- Go toolchain
|
||||
- Dart SDK
|
||||
- Flutter SDK
|
||||
|
||||
## Development
|
||||
|
||||
Development uses Docker Compose via the Makefile. Run `make help` for all available commands.
|
||||
@@ -54,6 +65,8 @@ make status # Show service status
|
||||
make logs # View all logs
|
||||
make logs SERVICE=dev-ledger # View logs for a specific service
|
||||
make rebuild SERVICE=dev-ledger # Rebuild and restart a specific service
|
||||
make list-services # List all services and ports
|
||||
make health # Check service health
|
||||
make clean # Remove all containers and volumes
|
||||
```
|
||||
|
||||
@@ -62,6 +75,10 @@ make clean # Remove all containers and volumes
|
||||
```bash
|
||||
make infra-up # Start infrastructure only (MongoDB, NATS, Vault)
|
||||
make services-up # Start application services only (assumes infra is running)
|
||||
make backend-up # Start backend services only (no infrastructure/frontend changes)
|
||||
make backend-down # Stop backend services only
|
||||
make backend-rebuild # Rebuild and restart backend services only
|
||||
make list-services # Show service names, ports, and descriptions
|
||||
```
|
||||
|
||||
### Build Groups
|
||||
@@ -69,8 +86,8 @@ make services-up # Start application services only (assumes infra is running)
|
||||
```bash
|
||||
make build-core # discovery, ledger, fees, documents
|
||||
make build-fx # oracle, ingestor
|
||||
make build-payments # orchestrator
|
||||
make build-gateways # chain, tron, mntx, tgsettle
|
||||
make build-payments # orchestrator, quotation, methods
|
||||
make build-gateways # chain, tron, aurora, tgsettle
|
||||
make build-api # notification, callbacks, bff
|
||||
make build-frontend # Flutter web UI
|
||||
```
|
||||
|
||||
@@ -14,6 +14,7 @@ type PaymentIntent struct {
|
||||
SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
|
||||
FeeTreatment FeeTreatment `json:"fee_treatment,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Customer *Customer `json:"customer,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ type Payment struct {
|
||||
PaymentRef string `json:"paymentRef,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
Operations []PaymentOperation `json:"operations,omitempty"`
|
||||
@@ -294,6 +295,7 @@ func toPayment(p *orchestrationv2.Payment) *Payment {
|
||||
return &Payment{
|
||||
PaymentRef: p.GetPaymentRef(),
|
||||
State: enumJSONName(p.GetState().String()),
|
||||
Comment: strings.TrimSpace(p.GetIntentSnapshot().GetComment()),
|
||||
FailureCode: failureCode,
|
||||
FailureReason: failureReason,
|
||||
Operations: operations,
|
||||
|
||||
@@ -121,6 +121,22 @@ func TestToPaymentIgnoresHiddenFailures(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentMapsIntentComment(t *testing.T) {
|
||||
dto := toPayment(&orchestrationv2.Payment{
|
||||
PaymentRef: "pay-3",
|
||||
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
|
||||
IntentSnapshot: "ationv2.QuoteIntent{
|
||||
Comment: " invoice-7 ",
|
||||
},
|
||||
})
|
||||
if dto == nil {
|
||||
t.Fatal("expected non-nil payment dto")
|
||||
}
|
||||
if got, want := dto.Comment, "invoice-7"; got != want {
|
||||
t.Fatalf("comment mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
|
||||
dto := toPaymentQuote("ationv2.PaymentQuote{
|
||||
QuoteRef: "quote-1",
|
||||
|
||||
@@ -61,9 +61,7 @@ func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, e
|
||||
FeeTreatment: resolvedFeeTreatment,
|
||||
SettlementCurrency: settlementCurrency,
|
||||
Fx: mapFXIntent(intent),
|
||||
}
|
||||
if comment := strings.TrimSpace(intent.Attributes["comment"]); comment != "" {
|
||||
quoteIntent.Comment = comment
|
||||
Comment: strings.TrimSpace(intent.Comment),
|
||||
}
|
||||
return quoteIntent, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ replace github.com/tech/sendico/gateway/common => ../common
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/tech/sendico/gateway/common v0.1.0
|
||||
github.com/tech/sendico/pkg v0.1.0
|
||||
@@ -36,6 +35,7 @@ require (
|
||||
github.com/nats-io/nats.go v1.49.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.15 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
|
||||
@@ -23,6 +23,7 @@ class PaymentIntentDTO {
|
||||
final String? feeTreatment;
|
||||
|
||||
final Map<String, String>? attributes;
|
||||
final String? comment;
|
||||
final CustomerDTO? customer;
|
||||
|
||||
const PaymentIntentDTO({
|
||||
@@ -33,10 +34,12 @@ class PaymentIntentDTO {
|
||||
this.fx,
|
||||
this.settlementMode,
|
||||
this.attributes,
|
||||
this.comment,
|
||||
this.customer,
|
||||
this.feeTreatment,
|
||||
});
|
||||
|
||||
factory PaymentIntentDTO.fromJson(Map<String, dynamic> json) => _$PaymentIntentDTOFromJson(json);
|
||||
factory PaymentIntentDTO.fromJson(Map<String, dynamic> json) =>
|
||||
_$PaymentIntentDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PaymentIntentDTOToJson(this);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ extension PaymentIntentMapper on PaymentIntent {
|
||||
fx: fx?.toDTO(),
|
||||
settlementMode: settlementModeToValue(settlementMode),
|
||||
attributes: attributes,
|
||||
comment: comment,
|
||||
customer: customer?.toDTO(),
|
||||
feeTreatment: feeTreatmentToValue(feeTreatment),
|
||||
);
|
||||
@@ -30,6 +31,7 @@ extension PaymentIntentDTOMapper on PaymentIntentDTO {
|
||||
fx: fx?.toDomain(),
|
||||
settlementMode: settlementModeFromValue(settlementMode),
|
||||
attributes: attributes,
|
||||
comment: comment,
|
||||
customer: customer?.toDomain(),
|
||||
feeTreatment: feeTreatmentFromValue(feeTreatment),
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ class PaymentIntent {
|
||||
final FeeTreatment feeTreatment;
|
||||
final SettlementMode settlementMode;
|
||||
final Map<String, String>? attributes;
|
||||
final String? comment;
|
||||
final Customer? customer;
|
||||
|
||||
const PaymentIntent({
|
||||
@@ -29,6 +30,7 @@ class PaymentIntent {
|
||||
this.fx,
|
||||
this.settlementMode = SettlementMode.unspecified,
|
||||
this.attributes,
|
||||
this.comment,
|
||||
this.customer,
|
||||
required this.feeTreatment,
|
||||
});
|
||||
|
||||
@@ -57,6 +57,7 @@ void main() {
|
||||
),
|
||||
amount: MoneyDTO(amount: '10', currency: 'USD'),
|
||||
settlementMode: 'fix_received',
|
||||
comment: 'invoice-7',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -70,6 +71,7 @@ void main() {
|
||||
final intent = json['intent'] as Map<String, dynamic>;
|
||||
expect(intent['kind'], equals('payout'));
|
||||
expect(intent['settlement_mode'], equals('fix_received'));
|
||||
expect(intent['comment'], equals('invoice-7'));
|
||||
expect(intent.containsKey('settlement_currency'), isFalse);
|
||||
|
||||
final source = intent['source'] as Map<String, dynamic>;
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
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/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
import 'package:pweb/models/payment/multiple_payouts/state.dart';
|
||||
import 'package:pweb/providers/multiple_payouts.dart';
|
||||
import 'package:pweb/services/payments/csv_input.dart';
|
||||
|
||||
|
||||
class MultiplePayoutsController extends ChangeNotifier {
|
||||
final CsvInputService _csvInput;
|
||||
MultiplePayoutsProvider? _provider;
|
||||
PaymentSourceController? _sourceController;
|
||||
_PickState _pickState = _PickState.idle;
|
||||
Exception? _uiError;
|
||||
String? _lastSourceKey;
|
||||
|
||||
MultiplePayoutsController({required CsvInputService csvInput})
|
||||
: _csvInput = csvInput;
|
||||
@@ -37,6 +43,7 @@ class MultiplePayoutsController extends ChangeNotifier {
|
||||
_sourceController?.removeListener(_onSourceChanged);
|
||||
_sourceController = sourceController;
|
||||
_sourceController?.addListener(_onSourceChanged);
|
||||
_lastSourceKey = _currentSourceKey;
|
||||
shouldNotify = true;
|
||||
}
|
||||
if (shouldNotify) {
|
||||
@@ -60,16 +67,16 @@ class MultiplePayoutsController extends ChangeNotifier {
|
||||
_provider?.quoteStatusType ?? QuoteStatusType.missing;
|
||||
Duration? get quoteTimeLeft => _provider?.quoteTimeLeft;
|
||||
|
||||
bool get canSend => (_provider?.canSend ?? false) && _selectedWallet != null;
|
||||
bool get canSend => (_provider?.canSend ?? false) && _selectedSource != null;
|
||||
Money? get aggregateDebitAmount =>
|
||||
_provider?.aggregateDebitAmountFor(_selectedWallet);
|
||||
_provider?.aggregateDebitAmountForCurrency(_selectedSourceCurrencyCode);
|
||||
Money? get requestedSentAmount => _provider?.requestedSentAmount;
|
||||
Money? get aggregateSettlementAmount =>
|
||||
_provider?.aggregateSettlementAmountFor(_selectedWallet);
|
||||
Money? get aggregateSettlementAmount => _provider
|
||||
?.aggregateSettlementAmountForCurrency(_selectedSourceCurrencyCode);
|
||||
Money? get aggregateFeeAmount =>
|
||||
_provider?.aggregateFeeAmountFor(_selectedWallet);
|
||||
_provider?.aggregateFeeAmountForCurrency(_selectedSourceCurrencyCode);
|
||||
double? get aggregateFeePercent =>
|
||||
_provider?.aggregateFeePercentFor(_selectedWallet);
|
||||
_provider?.aggregateFeePercentForCurrency(_selectedSourceCurrencyCode);
|
||||
|
||||
Future<void> pickAndQuote() async {
|
||||
if (_pickState == _PickState.picking) return;
|
||||
@@ -84,15 +91,16 @@ class MultiplePayoutsController extends ChangeNotifier {
|
||||
try {
|
||||
final picked = await _csvInput.pickCsv();
|
||||
if (picked == null) return;
|
||||
final wallet = _selectedWallet;
|
||||
if (wallet == null) {
|
||||
_setUiError(StateError('Select source wallet first'));
|
||||
final source = _selectedSource;
|
||||
if (source == null) {
|
||||
_setUiError(StateError('Select source of funds first'));
|
||||
return;
|
||||
}
|
||||
await provider.quoteFromCsv(
|
||||
fileName: picked.name,
|
||||
content: picked.content,
|
||||
sourceWallet: wallet,
|
||||
sourceMethod: source.method,
|
||||
sourceCurrencyCode: source.currencyCode,
|
||||
);
|
||||
} catch (e) {
|
||||
_setUiError(e);
|
||||
@@ -131,10 +139,78 @@ class MultiplePayoutsController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _onSourceChanged() {
|
||||
final currentSourceKey = _currentSourceKey;
|
||||
final sourceChanged = currentSourceKey != _lastSourceKey;
|
||||
_lastSourceKey = currentSourceKey;
|
||||
if (sourceChanged) {
|
||||
unawaited(_requoteWithUploadedRows());
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Wallet? get _selectedWallet => _sourceController?.selectedWallet;
|
||||
String? get _selectedSourceCurrencyCode =>
|
||||
_sourceController?.selectedCurrencyCode;
|
||||
String? get _currentSourceKey {
|
||||
final source = _sourceController;
|
||||
if (source == null ||
|
||||
source.selectedType == null ||
|
||||
source.selectedRef == null) {
|
||||
return null;
|
||||
}
|
||||
return '${source.selectedType!.name}:${source.selectedRef!}';
|
||||
}
|
||||
|
||||
({PaymentMethodData method, String currencyCode})? get _selectedSource {
|
||||
final source = _sourceController;
|
||||
if (source == null) return null;
|
||||
|
||||
final currencyCode = source.selectedCurrencyCode;
|
||||
if (currencyCode == null || currencyCode.isEmpty) return null;
|
||||
|
||||
final wallet = source.selectedWallet;
|
||||
if (wallet != null) {
|
||||
final hasAsset = (wallet.tokenSymbol ?? '').isNotEmpty;
|
||||
final asset = hasAsset
|
||||
? PaymentAsset(
|
||||
chain: wallet.network ?? ChainNetwork.unspecified,
|
||||
tokenSymbol: wallet.tokenSymbol!,
|
||||
contractAddress: wallet.contractAddress,
|
||||
)
|
||||
: null;
|
||||
return (
|
||||
method: ManagedWalletPaymentMethod(
|
||||
managedWalletRef: wallet.id,
|
||||
asset: asset,
|
||||
),
|
||||
currencyCode: currencyCode,
|
||||
);
|
||||
}
|
||||
|
||||
final ledger = source.selectedLedgerAccount;
|
||||
if (ledger != null) {
|
||||
return (
|
||||
method: LedgerPaymentMethod(ledgerAccountRef: ledger.ledgerAccountRef),
|
||||
currencyCode: currencyCode,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _requoteWithUploadedRows() async {
|
||||
final provider = _provider;
|
||||
if (provider == null) return;
|
||||
if (provider.selectedFileName == null || provider.rows.isEmpty) return;
|
||||
|
||||
final source = _selectedSource;
|
||||
if (source == null) return;
|
||||
|
||||
_clearUiError(notify: false);
|
||||
await provider.requoteUploadedRows(
|
||||
sourceMethod: source.method,
|
||||
sourceCurrencyCode: source.currencyCode,
|
||||
);
|
||||
}
|
||||
|
||||
void _setUiError(Object error) {
|
||||
_uiError = error is Exception ? error : Exception(error.toString());
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:pshared/utils/money.dart';
|
||||
|
||||
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
String moneyLabel(Money? money) {
|
||||
if (money == null) return 'N/A';
|
||||
@@ -12,10 +14,7 @@ String moneyLabel(Money? money) {
|
||||
if (amount.isNaN) return '${money.amount} ${money.currency}';
|
||||
try {
|
||||
return assetToString(
|
||||
Asset(
|
||||
currency: currencyStringToCode(money.currency),
|
||||
amount: amount,
|
||||
),
|
||||
Asset(currency: currencyStringToCode(money.currency), amount: amount),
|
||||
);
|
||||
} catch (_) {
|
||||
return '${money.amount} ${money.currency}';
|
||||
@@ -31,6 +30,8 @@ String sentAmountLabel(MultiplePayoutsController controller) {
|
||||
return moneyLabel(requested);
|
||||
}
|
||||
|
||||
String feeLabel(MultiplePayoutsController controller) {
|
||||
return moneyLabel(controller.aggregateFeeAmount);
|
||||
String feeLabel(MultiplePayoutsController controller, AppLocalizations l10n) {
|
||||
final fee = controller.aggregateFeeAmount;
|
||||
if (fee == null) return l10n.noFee;
|
||||
return moneyLabel(fee);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
||||
import 'package:pweb/models/dashboard/summary_values.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/summary/widget.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
class SourceQuoteSummary extends StatelessWidget {
|
||||
const SourceQuoteSummary({
|
||||
@@ -21,7 +21,7 @@ class SourceQuoteSummary extends StatelessWidget {
|
||||
return PaymentSummary(
|
||||
spacing: spacing,
|
||||
values: PaymentSummaryValues(
|
||||
fee: feeLabel(controller),
|
||||
fee: feeLabel(controller, AppLocalizations.of(context)!),
|
||||
recipientReceives: moneyLabel(controller.aggregateSettlementAmount),
|
||||
total: moneyLabel(controller.aggregateDebitAmount),
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:pshared/models/money.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/quote/quote.dart';
|
||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/provider/payment/multiple/provider.dart';
|
||||
import 'package:pshared/provider/payment/multiple/quotation.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
@@ -76,12 +76,12 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
return quoteRef != null && quoteRef.isNotEmpty;
|
||||
}
|
||||
|
||||
Money? aggregateDebitAmountFor(Wallet? sourceWallet) {
|
||||
Money? aggregateDebitAmountForCurrency(String? sourceCurrencyCode) {
|
||||
if (_rows.isEmpty) return null;
|
||||
final totals = aggregateMoneyByCurrency(
|
||||
_quoteItems().map((quote) => quote.amounts?.sourceDebitTotal),
|
||||
);
|
||||
return _moneyForSourceCurrency(totals, sourceWallet);
|
||||
return _moneyForSourceCurrency(totals, sourceCurrencyCode);
|
||||
}
|
||||
|
||||
Money? get requestedSentAmount {
|
||||
@@ -97,23 +97,23 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
return Money(amount: amountToString(total), currency: currency);
|
||||
}
|
||||
|
||||
Money? aggregateSettlementAmountFor(Wallet? sourceWallet) {
|
||||
Money? aggregateSettlementAmountForCurrency(String? sourceCurrencyCode) {
|
||||
if (_rows.isEmpty) return null;
|
||||
final totals = aggregateMoneyByCurrency(
|
||||
_quoteItems().map((quote) => quote.amounts?.destinationSettlement),
|
||||
);
|
||||
return _moneyForSourceCurrency(totals, sourceWallet);
|
||||
return _moneyForSourceCurrency(totals, sourceCurrencyCode);
|
||||
}
|
||||
|
||||
Money? aggregateFeeAmountFor(Wallet? sourceWallet) {
|
||||
Money? aggregateFeeAmountForCurrency(String? sourceCurrencyCode) {
|
||||
if (_rows.isEmpty) return null;
|
||||
final totals = aggregateMoneyByCurrency(_quoteItems().map(quoteFeeTotal));
|
||||
return _moneyForSourceCurrency(totals, sourceWallet);
|
||||
return _moneyForSourceCurrency(totals, sourceCurrencyCode);
|
||||
}
|
||||
|
||||
double? aggregateFeePercentFor(Wallet? sourceWallet) {
|
||||
final debit = aggregateDebitAmountFor(sourceWallet);
|
||||
final fee = aggregateFeeAmountFor(sourceWallet);
|
||||
double? aggregateFeePercentForCurrency(String? sourceCurrencyCode) {
|
||||
final debit = aggregateDebitAmountForCurrency(sourceCurrencyCode);
|
||||
final fee = aggregateFeeAmountForCurrency(sourceCurrencyCode);
|
||||
if (debit == null || fee == null) return null;
|
||||
|
||||
final debitValue = parseMoneyAmount(debit.amount, fallback: double.nan);
|
||||
@@ -126,7 +126,8 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
Future<void> quoteFromCsv({
|
||||
required String fileName,
|
||||
required String content,
|
||||
required Wallet sourceWallet,
|
||||
required PaymentMethodData sourceMethod,
|
||||
required String sourceCurrencyCode,
|
||||
}) async {
|
||||
if (isBusy) return;
|
||||
|
||||
@@ -144,18 +145,43 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
_sentCount = 0;
|
||||
|
||||
final rows = _csvParser.parseRows(content);
|
||||
final intents = _intentBuilder.buildIntents(sourceWallet, rows);
|
||||
await _quoteRows(
|
||||
quotation: quotation,
|
||||
fileName: fileName,
|
||||
rows: rows,
|
||||
sourceMethod: sourceMethod,
|
||||
sourceCurrencyCode: sourceCurrencyCode,
|
||||
);
|
||||
|
||||
_selectedFileName = fileName;
|
||||
_rows = rows;
|
||||
if (quotation.error != null) {
|
||||
_setErrorObject(quotation.error!);
|
||||
}
|
||||
} catch (e) {
|
||||
_setErrorObject(e);
|
||||
} finally {
|
||||
_setState(MultiplePayoutsState.idle);
|
||||
}
|
||||
}
|
||||
|
||||
await quotation.quotePayments(
|
||||
intents,
|
||||
metadata: <String, String>{
|
||||
'upload_filename': fileName,
|
||||
'upload_rows': rows.length.toString(),
|
||||
...?_uploadAmountMetadata(),
|
||||
},
|
||||
Future<void> requoteUploadedRows({
|
||||
required PaymentMethodData sourceMethod,
|
||||
required String sourceCurrencyCode,
|
||||
}) async {
|
||||
if (isBusy || _rows.isEmpty || _selectedFileName == null) return;
|
||||
final quotation = _quotation;
|
||||
if (quotation == null) return;
|
||||
|
||||
try {
|
||||
_setState(MultiplePayoutsState.quoting);
|
||||
_error = null;
|
||||
_sentCount = 0;
|
||||
|
||||
await _quoteRows(
|
||||
quotation: quotation,
|
||||
fileName: _selectedFileName!,
|
||||
rows: _rows,
|
||||
sourceMethod: sourceMethod,
|
||||
sourceCurrencyCode: sourceCurrencyCode,
|
||||
);
|
||||
|
||||
if (quotation.error != null) {
|
||||
@@ -254,13 +280,16 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
};
|
||||
}
|
||||
|
||||
Money? _moneyForSourceCurrency(List<Money>? values, Wallet? sourceWallet) {
|
||||
Money? _moneyForSourceCurrency(
|
||||
List<Money>? values,
|
||||
String? sourceCurrencyCode,
|
||||
) {
|
||||
if (values == null || values.isEmpty) return null;
|
||||
|
||||
if (sourceWallet != null) {
|
||||
final sourceCurrency = currencyCodeToString(sourceWallet.currency);
|
||||
if (sourceCurrencyCode != null && sourceCurrencyCode.isNotEmpty) {
|
||||
final sourceCurrency = sourceCurrencyCode.trim().toUpperCase();
|
||||
for (final value in values) {
|
||||
if (value.currency.toUpperCase() == sourceCurrency.toUpperCase()) {
|
||||
if (value.currency.toUpperCase() == sourceCurrency) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -272,6 +301,32 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
List<PaymentQuote> _quoteItems() =>
|
||||
_quotation?.quotation?.items ?? const <PaymentQuote>[];
|
||||
|
||||
Future<void> _quoteRows({
|
||||
required MultiQuotationProvider quotation,
|
||||
required String fileName,
|
||||
required List<CsvPayoutRow> rows,
|
||||
required PaymentMethodData sourceMethod,
|
||||
required String sourceCurrencyCode,
|
||||
}) async {
|
||||
final intents = _intentBuilder.buildIntents(
|
||||
sourceMethod: sourceMethod,
|
||||
sourceCurrency: sourceCurrencyCode,
|
||||
rows: rows,
|
||||
);
|
||||
|
||||
_selectedFileName = fileName;
|
||||
_rows = rows;
|
||||
|
||||
await quotation.quotePayments(
|
||||
intents,
|
||||
metadata: <String, String>{
|
||||
'upload_filename': fileName,
|
||||
'upload_rows': rows.length.toString(),
|
||||
...?_uploadAmountMetadata(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_quotation?.removeListener(_onQuotationChanged);
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
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';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
import 'package:pshared/utils/payment/fx_helpers.dart';
|
||||
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
@@ -16,19 +12,11 @@ import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
class MultipleIntentBuilder {
|
||||
static const String _currency = 'RUB';
|
||||
|
||||
List<PaymentIntent> buildIntents(
|
||||
Wallet sourceWallet,
|
||||
List<CsvPayoutRow> rows,
|
||||
) {
|
||||
final sourceCurrency = currencyCodeToString(sourceWallet.currency);
|
||||
final hasAsset = (sourceWallet.tokenSymbol ?? '').isNotEmpty;
|
||||
final sourceAsset = hasAsset
|
||||
? PaymentAsset(
|
||||
chain: sourceWallet.network ?? ChainNetwork.unspecified,
|
||||
tokenSymbol: sourceWallet.tokenSymbol!,
|
||||
contractAddress: sourceWallet.contractAddress,
|
||||
)
|
||||
: null;
|
||||
List<PaymentIntent> buildIntents({
|
||||
required PaymentMethodData sourceMethod,
|
||||
required String sourceCurrency,
|
||||
required List<CsvPayoutRow> rows,
|
||||
}) {
|
||||
final fxIntent = FxIntentHelper.buildSellBaseBuyQuote(
|
||||
baseCurrency: sourceCurrency,
|
||||
quoteCurrency: _currency,
|
||||
@@ -39,10 +27,7 @@ class MultipleIntentBuilder {
|
||||
final amount = Money(amount: row.amount, currency: _currency);
|
||||
return PaymentIntent(
|
||||
kind: PaymentKind.payout,
|
||||
source: ManagedWalletPaymentMethod(
|
||||
managedWalletRef: sourceWallet.id,
|
||||
asset: sourceAsset,
|
||||
),
|
||||
source: sourceMethod,
|
||||
destination: CardPaymentMethod(
|
||||
pan: row.pan,
|
||||
firstName: row.firstName,
|
||||
|
||||
Reference in New Issue
Block a user