Merge pull request 'fix for quote and operations addition' (#729) from SEND073 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed

Reviewed-on: #729
This commit was merged in pull request #729.
This commit is contained in:
2026-03-13 13:26:10 +00:00
13 changed files with 200 additions and 359 deletions

View File

@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/money.dart'; import 'package:pshared/data/dto/payment/operation_money.dart';
part 'operation.g.dart'; part 'operation.g.dart';
@@ -13,8 +13,7 @@ class PaymentOperationDTO {
final String? code; final String? code;
final String? state; final String? state;
final String? label; final String? label;
final MoneyDTO? amount; final PaymentOperationMoneyDTO? money;
final MoneyDTO? convertedAmount;
final String? failureCode; final String? failureCode;
final String? failureReason; final String? failureReason;
final String? startedAt; final String? startedAt;
@@ -27,8 +26,7 @@ class PaymentOperationDTO {
this.code, this.code,
this.state, this.state,
this.label, this.label,
this.amount, this.money,
this.convertedAmount,
this.failureCode, this.failureCode,
this.failureReason, this.failureReason,
this.startedAt, this.startedAt,

View File

@@ -0,0 +1,31 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/money.dart';
part 'operation_money.g.dart';
@JsonSerializable()
class PaymentOperationMoneySnapshotDTO {
final MoneyDTO? amount;
final MoneyDTO? convertedAmount;
const PaymentOperationMoneySnapshotDTO({this.amount, this.convertedAmount});
factory PaymentOperationMoneySnapshotDTO.fromJson(
Map<String, dynamic> json,
) => _$PaymentOperationMoneySnapshotDTOFromJson(json);
Map<String, dynamic> toJson() =>
_$PaymentOperationMoneySnapshotDTOToJson(this);
}
@JsonSerializable()
class PaymentOperationMoneyDTO {
final PaymentOperationMoneySnapshotDTO? planned;
final PaymentOperationMoneySnapshotDTO? executed;
const PaymentOperationMoneyDTO({this.planned, this.executed});
factory PaymentOperationMoneyDTO.fromJson(Map<String, dynamic> json) =>
_$PaymentOperationMoneyDTOFromJson(json);
Map<String, dynamic> toJson() => _$PaymentOperationMoneyDTOToJson(this);
}

View File

@@ -1,4 +1,5 @@
import 'package:pshared/data/dto/payment/operation.dart'; import 'package:pshared/data/dto/payment/operation.dart';
import 'package:pshared/data/dto/payment/operation_money.dart';
import 'package:pshared/data/mapper/money.dart'; import 'package:pshared/data/mapper/money.dart';
import 'package:pshared/models/payment/execution_operation.dart'; import 'package:pshared/models/payment/execution_operation.dart';
@@ -11,8 +12,7 @@ extension PaymentOperationDTOMapper on PaymentOperationDTO {
code: code, code: code,
state: state, state: state,
label: label, label: label,
amount: amount?.toDomain(), money: money?.toDomain(),
convertedAmount: convertedAmount?.toDomain(),
failureCode: failureCode, failureCode: failureCode,
failureReason: failureReason, failureReason: failureReason,
startedAt: _parseDateTime(startedAt), startedAt: _parseDateTime(startedAt),
@@ -28,8 +28,7 @@ extension PaymentExecutionOperationMapper on PaymentExecutionOperation {
code: code, code: code,
state: state, state: state,
label: label, label: label,
amount: amount?.toDTO(), money: money?.toDTO(),
convertedAmount: convertedAmount?.toDTO(),
failureCode: failureCode, failureCode: failureCode,
failureReason: failureReason, failureReason: failureReason,
startedAt: startedAt?.toUtc().toIso8601String(), startedAt: startedAt?.toUtc().toIso8601String(),
@@ -37,6 +36,35 @@ extension PaymentExecutionOperationMapper on PaymentExecutionOperation {
); );
} }
extension PaymentOperationMoneyDTOMapper on PaymentOperationMoneyDTO {
PaymentOperationMoney toDomain() => PaymentOperationMoney(
planned: planned?.toDomain(),
executed: executed?.toDomain(),
);
}
extension PaymentOperationMoneyMapper on PaymentOperationMoney {
PaymentOperationMoneyDTO toDTO() => PaymentOperationMoneyDTO(
planned: planned?.toDTO(),
executed: executed?.toDTO(),
);
}
extension PaymentOperationMoneySnapshotDTOMapper
on PaymentOperationMoneySnapshotDTO {
PaymentOperationMoneySnapshot toDomain() => PaymentOperationMoneySnapshot(
amount: amount?.toDomain(),
convertedAmount: convertedAmount?.toDomain(),
);
}
extension PaymentOperationMoneySnapshotMapper on PaymentOperationMoneySnapshot {
PaymentOperationMoneySnapshotDTO toDTO() => PaymentOperationMoneySnapshotDTO(
amount: amount?.toDTO(),
convertedAmount: convertedAmount?.toDTO(),
);
}
DateTime? _parseDateTime(String? value) { DateTime? _parseDateTime(String? value) {
final normalized = value?.trim(); final normalized = value?.trim();
if (normalized == null || normalized.isEmpty) return null; if (normalized == null || normalized.isEmpty) return null;

View File

@@ -1,6 +1,23 @@
import 'package:money2/money2.dart'; import 'package:money2/money2.dart';
class PaymentOperationMoneySnapshot {
final Money? amount;
final Money? convertedAmount;
const PaymentOperationMoneySnapshot({
required this.amount,
required this.convertedAmount,
});
}
class PaymentOperationMoney {
final PaymentOperationMoneySnapshot? planned;
final PaymentOperationMoneySnapshot? executed;
const PaymentOperationMoney({required this.planned, required this.executed});
}
class PaymentExecutionOperation { class PaymentExecutionOperation {
final String? stepRef; final String? stepRef;
final String? operationRef; final String? operationRef;
@@ -8,8 +25,7 @@ class PaymentExecutionOperation {
final String? code; final String? code;
final String? state; final String? state;
final String? label; final String? label;
final Money? amount; final PaymentOperationMoney? money;
final Money? convertedAmount;
final String? failureCode; final String? failureCode;
final String? failureReason; final String? failureReason;
final DateTime? startedAt; final DateTime? startedAt;
@@ -22,11 +38,18 @@ class PaymentExecutionOperation {
required this.code, required this.code,
required this.state, required this.state,
required this.label, required this.label,
required this.amount, required this.money,
required this.convertedAmount,
required this.failureCode, required this.failureCode,
required this.failureReason, required this.failureReason,
required this.startedAt, required this.startedAt,
required this.completedAt, required this.completedAt,
}); });
Money? get amount => money?.executed?.amount ?? money?.planned?.amount;
Money? get convertedAmount =>
money?.executed?.convertedAmount ?? money?.planned?.convertedAmount;
Money? get plannedAmount => money?.planned?.amount;
Money? get plannedConvertedAmount => money?.planned?.convertedAmount;
Money? get executedAmount => money?.executed?.amount;
Money? get executedConvertedAmount => money?.executed?.convertedAmount;
} }

View File

@@ -17,7 +17,7 @@ class OperationItem {
final PaymentMethod? paymentMethod; final PaymentMethod? paymentMethod;
final String name; final String name;
final DateTime date; final DateTime date;
final String comment; // final String comment;
OperationItem({ OperationItem({
required this.status, required this.status,
@@ -34,6 +34,6 @@ class OperationItem {
this.paymentMethod, this.paymentMethod,
required this.name, required this.name,
required this.date, required this.date,
required this.comment, // required this.comment,
}); });
} }

View File

@@ -5,6 +5,7 @@ import 'package:uuid/uuid.dart';
import 'package:pshared/api/requests/payment/quotes.dart'; import 'package:pshared/api/requests/payment/quotes.dart';
import 'package:pshared/data/mapper/payment/intent/payment.dart'; import 'package:pshared/data/mapper/payment/intent/payment.dart';
import 'package:pshared/models/payment/intent.dart'; import 'package:pshared/models/payment/intent.dart';
import 'package:pshared/models/payment/kind.dart';
import 'package:pshared/models/payment/quote/quotes.dart'; import 'package:pshared/models/payment/quote/quotes.dart';
import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/payment/auto_refresh.dart'; import 'package:pshared/provider/payment/auto_refresh.dart';
@@ -12,6 +13,7 @@ import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/payment/multiple.dart'; import 'package:pshared/service/payment/multiple.dart';
import 'package:pshared/utils/exception.dart'; import 'package:pshared/utils/exception.dart';
class MultiQuotationProvider extends ChangeNotifier { class MultiQuotationProvider extends ChangeNotifier {
static const Duration _autoRefreshLead = Duration(seconds: 5); static const Duration _autoRefreshLead = Duration(seconds: 5);
@@ -77,6 +79,11 @@ class MultiQuotationProvider extends ChangeNotifier {
if (intents.isEmpty) { if (intents.isEmpty) {
throw StateError('At least one payment intent is required'); throw StateError('At least one payment intent is required');
} }
if (intents.any((intent) => !_isQuotable(intent))) {
throw StateError(
'Each payment intent must include kind, source, destination, and amount',
);
}
_lastIntents = List<PaymentIntent>.from(intents); _lastIntents = List<PaymentIntent>.from(intents);
_lastPreviewOnly = previewOnly; _lastPreviewOnly = previewOnly;
@@ -135,6 +142,13 @@ class MultiQuotationProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool _isQuotable(PaymentIntent intent) {
if (intent.kind == PaymentKind.unspecified) return false;
if (intent.source == null) return false;
if (intent.destination == null) return false;
return intent.amount != null;
}
void _setResource(Resource<PaymentQuotes> quotation) { void _setResource(Resource<PaymentQuotes> quotation) {
_quotation = quotation; _quotation = quotation;
_syncAutoRefresh(); _syncAutoRefresh();

View File

@@ -12,6 +12,7 @@ import 'package:pshared/api/requests/payment/quote.dart';
import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/controllers/payment/source.dart';
import 'package:pshared/data/mapper/payment/intent/payment.dart'; import 'package:pshared/data/mapper/payment/intent/payment.dart';
import 'package:pshared/models/payment/intent.dart'; import 'package:pshared/models/payment/intent.dart';
import 'package:pshared/models/payment/kind.dart';
import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/quote/quote.dart';
import 'package:pshared/models/auto_refresh_mode.dart'; import 'package:pshared/models/auto_refresh_mode.dart';
import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/organizations.dart';
@@ -52,13 +53,17 @@ class QuotationProvider extends ChangeNotifier {
) { ) {
_organizations = venue; _organizations = venue;
_sourceCurrencyCode = source.selectedCurrencyCode; _sourceCurrencyCode = source.selectedCurrencyCode;
final intent = _intentBuilder.build( final builtIntent = _intentBuilder.build(
payment: payment, payment: payment,
source: source, source: source,
flow: flow, flow: flow,
recipients: recipients, recipients: recipients,
); );
if (intent == null) return; if (!_isQuotable(builtIntent)) {
_clearQuotation();
return;
}
final intent = builtIntent!;
final intentKey = _buildIntentKey(intent); final intentKey = _buildIntentKey(intent);
final lastIntent = _lastIntent; final lastIntent = _lastIntent;
if (lastIntent != null && intentKey == _buildIntentKey(lastIntent)) return; if (lastIntent != null && intentKey == _buildIntentKey(lastIntent)) return;
@@ -100,12 +105,19 @@ class QuotationProvider extends ChangeNotifier {
} }
Future<PaymentQuote?> refreshQuotation() async { Future<PaymentQuote?> refreshQuotation() async {
final intent = _lastIntent; final lastIntent = _lastIntent;
if (intent == null) return null; if (!_isQuotable(lastIntent)) {
return getQuotation(intent); _clearQuotation();
return null;
}
return getQuotation(lastIntent!);
} }
Future<PaymentQuote?> getQuotation(PaymentIntent intent) async { Future<PaymentQuote?> getQuotation(PaymentIntent intent) async {
if (!_isQuotable(intent)) {
_clearQuotation();
return null;
}
if (!_organizations.isOrganizationSet) { if (!_organizations.isOrganizationSet) {
throw StateError('Organization is not set'); throw StateError('Organization is not set');
} }
@@ -138,8 +150,20 @@ class QuotationProvider extends ChangeNotifier {
void reset() { void reset() {
_isLoaded = false; _isLoaded = false;
_lastIntent = null;
_sourceCurrencyCode = null; _sourceCurrencyCode = null;
_clearQuotation();
}
bool _isQuotable(PaymentIntent? intent) {
if (intent == null) return false;
if (intent.kind == PaymentKind.unspecified) return false;
if (intent.source == null) return false;
if (intent.destination == null) return false;
return intent.amount != null;
}
void _clearQuotation() {
_lastIntent = null;
_setResource(Resource(data: null, isLoading: false, error: null)); _setResource(Resource(data: null, isLoading: false, error: null));
} }

View File

@@ -1,225 +0,0 @@
import 'dart:convert';
import 'package:test/test.dart';
import 'package:pshared/api/requests/payment/initiate.dart';
import 'package:pshared/api/requests/payment/initiate_payments.dart';
import 'package:pshared/api/requests/payment/quote.dart';
import 'package:pshared/api/responses/payment/payment.dart';
import 'package:pshared/api/responses/payment/quotation.dart';
import 'package:pshared/data/dto/money.dart';
import 'package:pshared/data/dto/payment/currency_pair.dart';
import 'package:pshared/data/dto/payment/endpoint.dart';
import 'package:pshared/data/dto/payment/intent/fx.dart';
import 'package:pshared/data/dto/payment/intent/payment.dart';
import 'package:pshared/data/mapper/payment/payment.dart';
import 'package:pshared/data/mapper/payment/payment_response.dart';
import 'package:pshared/models/payment/asset.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/payment/methods/card_token.dart';
import 'package:pshared/models/payment/methods/crypto_address.dart';
import 'package:pshared/models/payment/methods/managed_wallet.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
void main() {
group('Payment request DTO contract', () {
test('serializes endpoint types to backend canonical values', () {
final managed = ManagedWalletPaymentMethod(
managedWalletRef: 'mw-1',
).toDTO();
final external = CryptoAddressPaymentMethod(
asset: const PaymentAsset(
chain: ChainNetwork.tronMainnet,
tokenSymbol: 'USDT',
),
address: 'TXYZ',
).toDTO();
final cardToken = CardTokenPaymentMethod(
token: 'tok_1',
maskedPan: '4111',
).toDTO();
expect(managed.type, equals('managedWallet'));
expect(external.type, equals('cryptoAddress'));
expect(cardToken.type, equals('cardToken'));
});
test('quote payment request uses expected backend field names', () {
final request = QuotePaymentRequest(
idempotencyKey: '',
previewOnly: true,
intent: const PaymentIntentDTO(
kind: 'payout',
source: PaymentEndpointDTO(
type: 'ledger',
data: {'ledger_account_ref': 'ledger:src'},
),
destination: PaymentEndpointDTO(
type: 'cardToken',
data: {'token': 'tok_1', 'masked_pan': '4111'},
),
amount: MoneyDTO(amount: '10', currency: 'USD'),
settlementMode: 'fix_received',
comment: 'invoice-7',
),
);
final json =
jsonDecode(jsonEncode(request.toJson())) as Map<String, dynamic>;
expect(json['idempotencyKey'], equals(''));
expect(json['previewOnly'], isTrue);
expect(json['intent'], isA<Map<String, dynamic>>());
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>;
final destination = intent['destination'] as Map<String, dynamic>;
expect(source['type'], equals('ledger'));
expect(destination['type'], equals('cardToken'));
});
test('quote payment request serializes fx side to backend value', () {
final request = QuotePaymentRequest(
idempotencyKey: '',
previewOnly: true,
intent: const PaymentIntentDTO(
kind: 'payout',
source: PaymentEndpointDTO(
type: 'managedWallet',
data: {'managed_wallet_ref': 'mw-1'},
),
destination: PaymentEndpointDTO(
type: 'cardToken',
data: {'token': 'tok_1', 'masked_pan': '4111'},
),
amount: MoneyDTO(amount: '10', currency: 'USDT'),
settlementMode: 'fix_source',
fx: FxIntentDTO(
pair: CurrencyPairDTO(base: 'RUB', quote: 'USDT'),
side: 'buy_base_sell_quote',
),
),
);
final json =
jsonDecode(jsonEncode(request.toJson())) as Map<String, dynamic>;
final intent = json['intent'] as Map<String, dynamic>;
final fx = intent['fx'] as Map<String, dynamic>;
expect(fx['side'], equals('buy_base_sell_quote'));
});
test('quote response parses backend fx quote pricedAtUnixMs', () {
final response = PaymentQuoteResponse.fromJson({
'accessToken': {'token': 'token', 'expiration': '2026-02-25T00:00:00Z'},
'idempotencyKey': 'idem-1',
'quote': {
'quoteRef': 'q-1',
'intentRef': 'intent-1',
'amounts': {
'sourcePrincipal': {'amount': '10', 'currency': 'USDT'},
'sourceDebitTotal': {'amount': '10.75', 'currency': 'USDT'},
'destinationSettlement': {'amount': '760', 'currency': 'RUB'},
},
'fees': {
'lines': [
{
'ledgerAccountRef': 'ledger:fees',
'amount': {'amount': '0.75', 'currency': 'USDT'},
'lineType': 'posting_line_type_fee',
'side': 'entry_side_debit',
'meta': {'fee_target': 'wallet'},
},
],
},
'fxQuote': {
'quoteRef': 'fx-1',
'baseCurrency': 'USDT',
'quoteCurrency': 'RUB',
'side': 'sell_base_buy_quote',
'price': '76',
'baseAmount': {'amount': '10', 'currency': 'USDT'},
'quoteAmount': {'amount': '760', 'currency': 'RUB'},
'expiresAtUnixMs': 1771945907749,
'pricedAtUnixMs': 1771945907000,
'provider': 'binance',
'rateRef': 'rate-1',
'firm': false,
},
},
});
expect(response.quote.fxQuote?.pricedAtUnixMs, equals(1771945907000));
expect(response.quote.intentRef, equals('intent-1'));
expect(response.quote.amounts?.sourceDebitTotal?.amount, equals('10.75'));
expect(response.quote.fees?.lines?.length, equals(1));
});
test('initiate payment by quote keeps expected fields', () {
final request = InitiatePaymentRequest(
idempotencyKey: 'idem-2',
quoteRef: 'q-1',
clientPaymentRef: 'cp-1',
);
final json = request.toJson();
expect(json['idempotencyKey'], equals('idem-2'));
expect(json['quoteRef'], equals('q-1'));
expect(json['clientPaymentRef'], equals('cp-1'));
expect(json.containsKey('intent'), isTrue);
expect(json['intent'], isNull);
});
test('initiate multi payments request keeps expected fields', () {
final request = InitiatePaymentsRequest(
idempotencyKey: 'idem-3',
quoteRef: 'q-2',
clientPaymentRef: 'cp-1',
);
final json = request.toJson();
expect(json['idempotencyKey'], equals('idem-3'));
expect(json['quoteRef'], equals('q-2'));
expect(json['clientPaymentRef'], equals('cp-1'));
expect(json.containsKey('intentRef'), isFalse);
expect(json.containsKey('intentRefs'), isFalse);
});
test(
'payment response parses source and destination endpoint snapshots',
() {
final response = PaymentResponse.fromJson({
'accessToken': {
'token': 'token',
'expiration': '2026-02-25T00:00:00Z',
},
'payment': {
'paymentRef': 'pay-1',
'state': 'orchestration_state_created',
'source': {
'type': 'wallet',
'data': {'walletId': 'wallet-1'},
},
'destination': {'paymentMethodRef': 'pm-123'},
'operations': [],
'meta': {'quotationRef': 'quote-1'},
},
});
final payment = response.payment.toDomain();
expect(payment.paymentRef, equals('pay-1'));
expect(payment.source, isNotNull);
expect(payment.destination, isNotNull);
expect(payment.destination?.paymentMethodRef, equals('pm-123'));
expect(payment.source?.method, isA<WalletPaymentMethod>());
final sourceMethod = payment.source?.method as WalletPaymentMethod;
expect(sourceMethod.walletId, equals('wallet-1'));
},
);
});
}

View File

@@ -27,11 +27,15 @@ class OperationHistoryTile extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final title = resolveOperationTitle(loc, operation.code);
final operationLabel = operation.label?.trim(); final operationLabel = operation.label?.trim();
final title = (operationLabel != null && operationLabel.isNotEmpty)
? operationLabel
: resolveOperationTitle(loc, operation.code);
// final operationComment = operation.comment?.trim();
final stateView = resolveStepStateView(context, operation.state); final stateView = resolveStepStateView(context, operation.state);
final completedAt = formatCompletedAt(context, operation.completedAt); final completedAt = formatCompletedAt(context, operation.completedAt);
final amount = formatMoneyUi(context, operation.amount); final amount = formatMoneyUi(context, operation.amount);
final convertedAmount = formatMoneyUi(context, operation.convertedAmount);
final canDownload = canDownloadDocument && onDownloadDocument != null; final canDownload = canDownloadDocument && onDownloadDocument != null;
return Column( return Column(
@@ -52,17 +56,6 @@ class OperationHistoryTile extends StatelessWidget {
StepStateChip(view: stateView), StepStateChip(view: stateView),
], ],
), ),
if (operationLabel != null &&
operationLabel.isNotEmpty &&
operationLabel != title) ...[
const SizedBox(height: 4),
Text(
operationLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
'${loc.completedAtLabel}: $completedAt', '${loc.completedAtLabel}: $completedAt',
@@ -77,6 +70,24 @@ class OperationHistoryTile extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
if (operation.convertedAmount != null) ...[
const SizedBox(height: 4),
Text(
'${loc.toAmountColumn}: $convertedAmount',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
// if (operationComment != null && operationComment.isNotEmpty) ...[
// const SizedBox(height: 4),
// Text(
// '${loc.comment}: $operationComment',
// style: theme.textTheme.bodySmall?.copyWith(
// color: theme.colorScheme.onSurfaceVariant,
// ),
// ),
// ],
if (canDownload) ...[ if (canDownload) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
TextButton.icon( TextButton.icon(

View File

@@ -58,7 +58,7 @@ class OperationRow {
DataCell(Text(op.cardNumber ?? '-')), DataCell(Text(op.cardNumber ?? '-')),
DataCell(Text(op.name)), DataCell(Text(op.name)),
DataCell(Text(dateLabel)), DataCell(Text(dateLabel)),
DataCell(Text(op.comment)), // DataCell(Text(op.comment)),
], ],
); );
} }

View File

@@ -1,85 +0,0 @@
import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/models/payment/status.dart';
class OperationService {
Future<List<OperationItem>> fetchOperations() async {
await Future.delayed(const Duration(milliseconds: 500));
return [
OperationItem(
status: OperationStatus.error,
fileName: 'cards_payout_sample_june.csv',
amount: 10,
currency: 'EUR',
toAmount: 10,
toCurrency: 'EUR',
payId: '860163800',
cardNumber: null,
name: 'John Snow',
date: DateTime(2025, 7, 14, 19, 59, 2),
comment: 'EUR visa',
),
OperationItem(
status: OperationStatus.processing,
fileName: 'cards_payout_sample_june.csv',
amount: 10,
currency: 'EUR',
toAmount: 10,
toCurrency: 'EUR',
payId: '860163700',
cardNumber: null,
name: 'Baltasar Gelt',
date: DateTime(2025, 7, 14, 19, 59, 2),
comment: 'EUR master',
),
OperationItem(
status: OperationStatus.error,
fileName: 'cards_payout_sample_june.csv',
amount: 10,
currency: 'EUR',
toAmount: 10,
toCurrency: 'EUR',
payId: '40000000****0077',
cardNumber: '40000000****0077',
name: 'John Snow',
date: DateTime(2025, 7, 14, 19, 23, 22),
comment: 'EUR visa',
),
OperationItem(
status: OperationStatus.success,
fileName: null,
amount: 10,
currency: 'EUR',
toAmount: 10,
toCurrency: 'EUR',
payId: '54133300****0019',
cardNumber: '54133300****0019',
name: 'Baltasar Gelt',
date: DateTime(2025, 7, 14, 19, 23, 21),
comment: 'EUR master',
),
OperationItem(
status: OperationStatus.success,
fileName: null,
amount: 130,
currency: 'EUR',
toAmount: 130,
toCurrency: 'EUR',
payId: '54134300****0019',
cardNumber: '54153300****0019',
name: 'Ivan Brokov',
date: DateTime(2025, 7, 15, 19, 23, 21),
comment: 'EUR master 2',
),
];
}
// Add real API:
// Future<List<OperationItem>> fetchOperations() async {
// final response = await _httpClient.get('/api/operations');
// return (response.data as List)
// .map((json) => OperationItem.fromJson(json))
// .toList();
// }
}

View File

@@ -23,14 +23,15 @@ OperationItem mapPaymentToOperation(Payment payment) {
final payId = _firstNonEmpty([payment.paymentRef]) ?? '-'; final payId = _firstNonEmpty([payment.paymentRef]) ?? '-';
final name = final name =
_firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef]) ?? '-'; _firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef]) ?? '-';
final comment = // final comment =
_firstNonEmpty([ // _firstNonEmpty([
payment.comment, // ...payment.operations.map((operation) => operation.comment),
payment.failureReason, // payment.comment,
payment.failureCode, // payment.failureReason,
payment.state, // payment.failureCode,
]) ?? // payment.state,
''; // ]) ??
// '';
final operationDocument = _resolveOperationDocument(payment); final operationDocument = _resolveOperationDocument(payment);
return OperationItem( return OperationItem(
@@ -47,7 +48,7 @@ OperationItem mapPaymentToOperation(Payment payment) {
cardNumber: null, cardNumber: null,
name: name, name: name,
date: resolvePaymentDate(payment), date: resolvePaymentDate(payment),
comment: comment, // comment: comment,
); );
} }

View File

@@ -480,12 +480,9 @@ components:
label: label:
description: Human-readable operation label. description: Human-readable operation label.
type: string type: string
amount: money:
description: Primary money amount associated with the operation. description: Planned and executed monetary snapshots for the operation.
$ref: ../common/money.yaml#/components/schemas/Money $ref: ./payment.yaml#/components/schemas/PaymentOperationMoney
convertedAmount:
description: Secondary amount for conversion operations (for example FX convert output amount).
$ref: ../common/money.yaml#/components/schemas/Money
operationRef: operationRef:
description: External operation reference identifier reported by the gateway. description: External operation reference identifier reported by the gateway.
type: string type: string
@@ -507,6 +504,30 @@ components:
type: string type: string
format: date-time format: date-time
PaymentOperationMoney:
description: Planned and executed monetary snapshots associated with the operation.
type: object
additionalProperties: false
properties:
planned:
description: Monetary values planned for the operation before execution.
$ref: ./payment.yaml#/components/schemas/PaymentOperationMoneySnapshot
executed:
description: Monetary values observed after the operation was executed.
$ref: ./payment.yaml#/components/schemas/PaymentOperationMoneySnapshot
PaymentOperationMoneySnapshot:
description: Monetary snapshot containing base and converted amounts for an operation state.
type: object
additionalProperties: false
properties:
amount:
description: Primary money amount associated with the operation snapshot.
$ref: ../common/money.yaml#/components/schemas/Money
convertedAmount:
description: Secondary amount for conversion operations in the same snapshot.
$ref: ../common/money.yaml#/components/schemas/Money
Payment: Payment:
description: Persisted payment aggregate with status, quote, and operation history. description: Persisted payment aggregate with status, quote, and operation history.
type: object type: object