refactor of money utils with new money2 package

This commit is contained in:
Arseni
2026-03-13 03:17:29 +03:00
parent b4eb1437f6
commit 0091191d97
72 changed files with 453 additions and 982 deletions

View File

@@ -1,102 +1,78 @@
import 'package:flutter/material.dart';
import 'package:money2/money2.dart';
import 'package:pshared/models/asset.dart';
import 'package:pshared/models/currency.dart';
const nonBreakingSpace = '\u00A0';
final Currency _usdtCurrency = Currency.create(
'USDT',
6,
symbol: '',
isIso: false,
country: 'Digital',
unit: 'Tether',
name: 'Tether',
);
String withTrailingNonBreakingSpace(String value) {
return '$value$nonBreakingSpace';
}
final Currency _usdcCurrency = Currency.create(
'USDC',
6,
symbol: r'($)',
isIso: false,
country: 'Digital',
unit: 'USD Coin',
name: 'USD Coin',
);
String joinWithNonBreakingSpace(String left, String right) {
return '$left$nonBreakingSpace$right';
}
final Map<String, Currency> _commonCurrenciesByCode =
<String, Currency>{
for (final currency in CommonCurrencies().asList())
currency.isoCode: currency,
_usdtCurrency.isoCode: _usdtCurrency,
_usdcCurrency.isoCode: _usdcCurrency,
};
String currencyCodeToSymbol(Currency currencyCode) {
switch (currencyCode) {
case Currency.usd:
return '\$';
case Currency.usdt:
return '';
case Currency.usdc:
return '\$';
case Currency.rub:
return '';
case Currency.eur:
return '';
String currencyCodeToSymbol(CurrencyCode currencyCode) {
final symbol = currencySymbolFromCode(currencyCodeToString(currencyCode));
if (symbol == null || symbol.trim().isEmpty) {
return currencyCodeToString(currencyCode);
}
return symbol;
}
String amountToString(double amount) {
return amount.toStringAsFixed(2);
}
String currencyToString(Currency currencyCode, double amount) {
return joinWithNonBreakingSpace(
currencyCodeToSymbol(currencyCode),
amountToString(amount),
);
}
String assetToString(Asset asset) {
return currencyToString(asset.currency, asset.amount);
}
Currency currencyStringToCode(String currencyCode) {
switch (currencyCode) {
case 'USD':
return Currency.usd;
case 'USDT':
return Currency.usdt;
case 'USDC':
return Currency.usdc;
case 'RUB':
return Currency.rub;
case 'EUR':
return Currency.eur;
default:
throw ArgumentError('Unknown currency code: $currencyCode');
String currencyToString(CurrencyCode currencyCode, double amount) {
final code = currencyCodeToString(currencyCode);
final currency = money2CurrencyFromCode(code);
if (currency == null) {
return '$amount $code';
}
final money = Money.fromNumWithCurrency(amount, currency);
return money.toString();
}
String currencyCodeToString(Currency currencyCode) {
switch (currencyCode) {
case Currency.usd:
return 'USD';
case Currency.usdt:
return 'USDT';
case Currency.usdc:
return 'USDC';
case Currency.rub:
return 'RUB';
case Currency.eur:
return 'EUR';
CurrencyCode currencyStringToCode(String currencyCode) {
final normalized = currencyCode.trim().toUpperCase();
for (final value in CurrencyCode.values) {
if (currencyCodeToString(value) == normalized) {
return value;
}
}
throw ArgumentError('Unknown currency code: $currencyCode');
}
IconData iconForCurrencyType(Currency currencyCode) {
switch (currencyCode) {
case Currency.usd:
return Icons.currency_exchange;
case Currency.eur:
return Icons.currency_exchange;
case Currency.rub:
return Icons.currency_ruble;
case Currency.usdt:
return Icons.currency_exchange;
case Currency.usdc:
return Icons.money;
}
String currencyCodeToString(CurrencyCode currencyCode) {
return currencyCode.name.toUpperCase();
}
String? currencySymbolFromCode(String? code) {
final normalized = code?.trim();
if (normalized == null || normalized.isEmpty) return null;
try {
return currencyCodeToSymbol(currencyStringToCode(normalized.toUpperCase()));
} catch (_) {
return null;
}
final currency = money2CurrencyFromCode(code);
if (currency == null) return null;
final symbol = currency.symbol.trim();
return symbol.isEmpty ? null : symbol;
}
Currency? money2CurrencyFromCode(String? code) {
final normalized = code?.trim().toUpperCase();
if (normalized == null || normalized.isEmpty) return null;
return _commonCurrenciesByCode[normalized] ?? Currencies().find(normalized);
}

View File

@@ -1,41 +1,35 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
import 'package:pshared/utils/currency.dart';
const String _decimalMoneyPattern = '0.##################';
double parseMoneyAmount(String? raw, {double fallback = 0}) {
final trimmed = raw?.trim();
if (trimmed == null || trimmed.isEmpty) return fallback;
return double.tryParse(trimmed) ?? fallback;
Money? parseMoneyWithCurrency(String? amount, Currency? currency) {
if (currency == null) return null;
final value = _normalizeMoneyAmount(amount);
if (value == null || value.isEmpty) return null;
try {
return Money.parseWithCurrency(
value,
currency,
pattern: _decimalMoneyPattern,
);
} catch (_) {
return null;
}
}
String formatMoneyDisplay(
Money? money, {
String fallback = '--',
String separator = ' ',
String invalidAmountFallback = '',
}) {
if (money == null) return fallback;
final rawAmount = money.amount.trim();
final rawCurrency = money.currency.trim();
final parsedAmount = parseMoneyAmount(rawAmount, fallback: double.nan);
final amountToken = parsedAmount.isNaN
? (rawAmount.isEmpty ? invalidAmountFallback : rawAmount)
: amountToString(parsedAmount);
final symbol = currencySymbolFromCode(rawCurrency);
final normalizedSymbol = symbol?.trim() ?? '';
final hasSymbol = normalizedSymbol.isNotEmpty;
final currencyToken = hasSymbol ? normalizedSymbol : rawCurrency;
final first = amountToken;
final second = currencyToken;
if (first.isEmpty && second.isEmpty) return fallback;
if (first.isEmpty) return second;
if (second.isEmpty) return first;
return '$first$separator$second';
Money? parseMoneyWithCurrencyCode(String? amount, String? currencyCode) {
return parseMoneyWithCurrency(amount, money2CurrencyFromCode(currencyCode));
}
extension MoneyAmountX on Money {
double get amountValue => parseMoneyAmount(amount);
String? _normalizeMoneyAmount(String? value) {
final normalized = value?.trim();
if (normalized == null || normalized.isEmpty) return null;
if (normalized.contains(',') && !normalized.contains('.')) {
return normalized.replaceAll(',', '.');
}
return normalized;
}

View File

@@ -1,7 +1,8 @@
import 'package:money2/money2.dart';
import 'package:pshared/models/payment/currency_pair.dart';
import 'package:pshared/models/payment/fx/intent.dart';
import 'package:pshared/models/payment/fx/side.dart';
import 'package:pshared/models/money.dart';
class FxIntentHelper {
@@ -37,11 +38,15 @@ class FxIntentHelper {
case FxSide.unspecified:
break;
}
if (amount.currency == pair.base && pair.quote.isNotEmpty) return pair.quote;
if (amount.currency == pair.quote && pair.base.isNotEmpty) return pair.base;
if (amount.currency.isoCode == pair.base && pair.quote.isNotEmpty) {
return pair.quote;
}
if (amount.currency.isoCode == pair.quote && pair.base.isNotEmpty) {
return pair.base;
}
if (pair.quote.isNotEmpty) return pair.quote;
if (pair.base.isNotEmpty) return pair.base;
}
return amount.currency;
return amount.currency.isoCode;
}
}

View File

@@ -12,7 +12,7 @@ class PaymentQuotationCurrencyResolver {
PaymentMethodData? paymentData,
}) {
final quoteCurrency = _normalizeCurrency(
quote?.amounts?.destinationSettlement?.currency,
quote?.amounts?.destinationSettlement?.currency.isoCode,
);
if (quoteCurrency != null) return quoteCurrency;

View File

@@ -1,17 +1,16 @@
import 'package:pshared/models/money.dart';
import 'package:money2/money2.dart';
import 'package:pshared/models/payment/fees/line.dart';
import 'package:pshared/models/payment/quote/quote.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
Money? quoteFeeTotal(PaymentQuote? quote) {
final preferredCurrency =
quote?.amounts?.sourcePrincipal?.currency ??
quote?.amounts?.sourceDebitTotal?.currency;
return quoteFeeTotalFromLines(
quote?.fees?.lines,
preferredCurrency: preferredCurrency,
preferredCurrency:
quote?.amounts?.sourcePrincipal?.currency.isoCode ??
quote?.amounts?.sourceDebitTotal?.currency.isoCode,
);
}
@@ -21,7 +20,8 @@ Money? quoteSourceDebitTotal(
}) {
final sourceDebitTotal = quote?.amounts?.sourceDebitTotal;
final preferredCurrency = _normalizeCurrency(
preferredSourceCurrency ?? quote?.amounts?.sourcePrincipal?.currency,
preferredSourceCurrency ??
quote?.amounts?.sourcePrincipal?.currency.isoCode,
);
if (sourceDebitTotal == null) {
@@ -31,10 +31,9 @@ Money? quoteSourceDebitTotal(
);
}
final debitCurrency = _normalizeCurrency(sourceDebitTotal.currency);
if (preferredCurrency == null ||
debitCurrency == null ||
debitCurrency == preferredCurrency) {
_normalizeCurrency(sourceDebitTotal.currency.isoCode) ==
preferredCurrency) {
return sourceDebitTotal;
}
@@ -52,22 +51,18 @@ Money? quoteFeeTotalFromLines(
if (lines == null || lines.isEmpty) return null;
final normalizedPreferred = _normalizeCurrency(preferredCurrency);
final totalsByCurrency = <String, double>{};
final totalsByCurrency = <String, Money>{};
for (final line in lines) {
final money = line.amount;
if (money == null) continue;
final parsedAmount = line.amount;
if (parsedAmount == null) continue;
final currency = _normalizeCurrency(money.currency);
if (currency == null) continue;
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
if (amount.isNaN) continue;
final sign = _lineSign(line.side);
final signedAmount = sign * amount.abs();
totalsByCurrency[currency] =
(totalsByCurrency[currency] ?? 0) + signedAmount;
final currencyCode = parsedAmount.currency.isoCode;
final signedAmount = _isCreditLine(line.side) ? -parsedAmount : parsedAmount;
final current = totalsByCurrency[currencyCode];
totalsByCurrency[currencyCode] = current == null
? signedAmount
: current + signedAmount;
}
if (totalsByCurrency.isEmpty) return null;
@@ -77,85 +72,59 @@ Money? quoteFeeTotalFromLines(
totalsByCurrency.containsKey(normalizedPreferred)
? normalizedPreferred
: totalsByCurrency.keys.first;
final total = totalsByCurrency[selectedCurrency];
if (total == null) return null;
return Money(amount: amountToString(total), currency: selectedCurrency);
return totalsByCurrency[selectedCurrency];
}
List<Money> aggregateMoneyByCurrency(Iterable<Money?> values) {
final totals = <String, double>{};
final totals = <String, Money>{};
for (final value in values) {
if (value == null) continue;
final currency = _normalizeCurrency(value.currency);
if (currency == null) continue;
final amount = parseMoneyAmount(value.amount, fallback: double.nan);
if (amount.isNaN) continue;
totals[currency] = (totals[currency] ?? 0) + amount;
final currency = value.currency.isoCode;
final current = totals[currency];
totals[currency] = current == null ? value : current + value;
}
return totals.entries
.map(
(entry) =>
Money(amount: amountToString(entry.value), currency: entry.key),
)
.toList();
return totals.values.toList();
}
Money? _rebuildSourceDebitTotal(
PaymentQuote? quote, {
String? preferredSourceCurrency,
}) {
final sourcePrincipal = quote?.amounts?.sourcePrincipal;
if (sourcePrincipal == null) return null;
final principal = quote?.amounts?.sourcePrincipal;
if (principal == null) return null;
final principalCurrency = _normalizeCurrency(sourcePrincipal.currency);
if (principalCurrency == null) return null;
final principalCurrency = principal.currency.isoCode;
if (preferredSourceCurrency != null &&
principalCurrency != preferredSourceCurrency) {
return null;
}
final principalAmount = parseMoneyAmount(
sourcePrincipal.amount,
fallback: double.nan,
);
if (principalAmount.isNaN) return null;
double totalAmount = principalAmount;
var totalAmount = principal;
final fee = quoteFeeTotalFromLines(
quote?.fees?.lines,
preferredCurrency: principalCurrency,
);
if (fee != null && _normalizeCurrency(fee.currency) == principalCurrency) {
final feeAmount = parseMoneyAmount(fee.amount, fallback: double.nan);
if (!feeAmount.isNaN) {
totalAmount += feeAmount;
}
if (fee != null && fee.currency.isoCode == principalCurrency) {
totalAmount += fee;
}
return Money(
amount: amountToString(totalAmount),
currency: principalCurrency,
);
return totalAmount;
}
double _lineSign(String? side) {
bool _isCreditLine(String? side) {
final normalized = side?.trim().toLowerCase() ?? '';
switch (normalized) {
case 'entry_side_credit':
case 'credit':
return -1;
return true;
default:
return 1;
return false;
}
}
String? _normalizeCurrency(String? currency) {
final normalized = currency?.trim().toUpperCase();
final normalized = currency?.trim();
if (normalized == null || normalized.isEmpty) return null;
return normalized;
return money2CurrencyFromCode(normalized)?.isoCode ?? normalized.toUpperCase();
}