added comment for payment, changed intent and added amount ui in operations
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/money.dart';
|
||||
|
||||
part 'operation.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentOperationDTO {
|
||||
final String? stepRef;
|
||||
@@ -11,7 +13,8 @@ class PaymentOperationDTO {
|
||||
final String? code;
|
||||
final String? state;
|
||||
final String? label;
|
||||
final PaymentOperationMoneyDTO? money;
|
||||
final MoneyDTO? amount;
|
||||
final MoneyDTO? convertedAmount;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
final String? startedAt;
|
||||
@@ -24,7 +27,8 @@ class PaymentOperationDTO {
|
||||
this.code,
|
||||
this.state,
|
||||
this.label,
|
||||
this.money,
|
||||
this.amount,
|
||||
this.convertedAmount,
|
||||
this.failureCode,
|
||||
this.failureReason,
|
||||
this.startedAt,
|
||||
@@ -35,29 +39,3 @@ class PaymentOperationDTO {
|
||||
_$PaymentOperationDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PaymentOperationDTOToJson(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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ part 'payment.g.dart';
|
||||
class PaymentDTO {
|
||||
final String? paymentRef;
|
||||
final String? state;
|
||||
final String? comment;
|
||||
final PaymentResponseEndpointDTO? source;
|
||||
final PaymentResponseEndpointDTO? destination;
|
||||
final String? failureCode;
|
||||
@@ -23,6 +24,7 @@ class PaymentDTO {
|
||||
const PaymentDTO({
|
||||
this.paymentRef,
|
||||
this.state,
|
||||
this.comment,
|
||||
this.source,
|
||||
this.destination,
|
||||
this.failureCode,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:pshared/data/dto/payment/operation.dart';
|
||||
import 'package:pshared/data/mapper/money.dart';
|
||||
import 'package:pshared/models/payment/execution_operation.dart';
|
||||
|
||||
|
||||
extension PaymentOperationDTOMapper on PaymentOperationDTO {
|
||||
PaymentExecutionOperation toDomain() => PaymentExecutionOperation(
|
||||
stepRef: stepRef,
|
||||
@@ -10,7 +11,8 @@ extension PaymentOperationDTOMapper on PaymentOperationDTO {
|
||||
code: code,
|
||||
state: state,
|
||||
label: label,
|
||||
money: money?.toDomain(),
|
||||
amount: amount?.toDomain(),
|
||||
convertedAmount: convertedAmount?.toDomain(),
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
startedAt: _parseDateTime(startedAt),
|
||||
@@ -26,7 +28,8 @@ extension PaymentExecutionOperationMapper on PaymentExecutionOperation {
|
||||
code: code,
|
||||
state: state,
|
||||
label: label,
|
||||
money: money?.toDTO(),
|
||||
amount: amount?.toDTO(),
|
||||
convertedAmount: convertedAmount?.toDTO(),
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
startedAt: startedAt?.toUtc().toIso8601String(),
|
||||
@@ -34,38 +37,6 @@ extension PaymentExecutionOperationMapper on PaymentExecutionOperation {
|
||||
);
|
||||
}
|
||||
|
||||
extension PaymentOperationMoneyDTOMapper on PaymentOperationMoneyDTO {
|
||||
PaymentExecutionOperationMoney toDomain() => PaymentExecutionOperationMoney(
|
||||
planned: planned?.toDomain(),
|
||||
executed: executed?.toDomain(),
|
||||
);
|
||||
}
|
||||
|
||||
extension PaymentExecutionOperationMoneyMapper
|
||||
on PaymentExecutionOperationMoney {
|
||||
PaymentOperationMoneyDTO toDTO() => PaymentOperationMoneyDTO(
|
||||
planned: planned?.toDTO(),
|
||||
executed: executed?.toDTO(),
|
||||
);
|
||||
}
|
||||
|
||||
extension PaymentOperationMoneySnapshotDTOMapper
|
||||
on PaymentOperationMoneySnapshotDTO {
|
||||
PaymentExecutionOperationMoneySnapshot toDomain() =>
|
||||
PaymentExecutionOperationMoneySnapshot(
|
||||
amount: amount?.toDomain(),
|
||||
convertedAmount: convertedAmount?.toDomain(),
|
||||
);
|
||||
}
|
||||
|
||||
extension PaymentExecutionOperationMoneySnapshotMapper
|
||||
on PaymentExecutionOperationMoneySnapshot {
|
||||
PaymentOperationMoneySnapshotDTO toDTO() => PaymentOperationMoneySnapshotDTO(
|
||||
amount: amount?.toDTO(),
|
||||
convertedAmount: convertedAmount?.toDTO(),
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? _parseDateTime(String? value) {
|
||||
final normalized = value?.trim();
|
||||
if (normalized == null || normalized.isEmpty) return null;
|
||||
|
||||
@@ -10,6 +10,7 @@ extension PaymentDTOMapper on PaymentDTO {
|
||||
Payment toDomain() => Payment(
|
||||
paymentRef: paymentRef,
|
||||
state: state,
|
||||
comment: comment,
|
||||
source: source?.toDomain(),
|
||||
destination: destination?.toDomain(),
|
||||
orchestrationState: paymentOrchestrationStateFromValue(state),
|
||||
@@ -26,6 +27,7 @@ extension PaymentMapper on Payment {
|
||||
PaymentDTO toDTO() => PaymentDTO(
|
||||
paymentRef: paymentRef,
|
||||
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
||||
comment: comment,
|
||||
source: source?.toDTO(),
|
||||
destination: destination?.toDTO(),
|
||||
failureCode: failureCode,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:pshared/models/money.dart';
|
||||
|
||||
|
||||
class PaymentExecutionOperation {
|
||||
final String? stepRef;
|
||||
final String? operationRef;
|
||||
@@ -7,7 +8,8 @@ class PaymentExecutionOperation {
|
||||
final String? code;
|
||||
final String? state;
|
||||
final String? label;
|
||||
final PaymentExecutionOperationMoney? money;
|
||||
final Money? amount;
|
||||
final Money? convertedAmount;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
final DateTime? startedAt;
|
||||
@@ -20,30 +22,11 @@ class PaymentExecutionOperation {
|
||||
required this.code,
|
||||
required this.state,
|
||||
required this.label,
|
||||
required this.money,
|
||||
required this.amount,
|
||||
required this.convertedAmount,
|
||||
required this.failureCode,
|
||||
required this.failureReason,
|
||||
required this.startedAt,
|
||||
required this.completedAt,
|
||||
});
|
||||
}
|
||||
|
||||
class PaymentExecutionOperationMoney {
|
||||
final PaymentExecutionOperationMoneySnapshot? planned;
|
||||
final PaymentExecutionOperationMoneySnapshot? executed;
|
||||
|
||||
const PaymentExecutionOperationMoney({
|
||||
required this.planned,
|
||||
required this.executed,
|
||||
});
|
||||
}
|
||||
|
||||
class PaymentExecutionOperationMoneySnapshot {
|
||||
final Money? amount;
|
||||
final Money? convertedAmount;
|
||||
|
||||
const PaymentExecutionOperationMoneySnapshot({
|
||||
required this.amount,
|
||||
required this.convertedAmount,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/models/payment/status.dart';
|
||||
|
||||
|
||||
class OperationItem {
|
||||
final OperationStatus status;
|
||||
final String? fileName;
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:pshared/models/payment/state.dart';
|
||||
class Payment {
|
||||
final String? paymentRef;
|
||||
final String? state;
|
||||
final String? comment;
|
||||
final PaymentEndpoint? source;
|
||||
final PaymentEndpoint? destination;
|
||||
final PaymentOrchestrationState orchestrationState;
|
||||
@@ -20,6 +21,7 @@ class Payment {
|
||||
const Payment({
|
||||
required this.paymentRef,
|
||||
required this.state,
|
||||
required this.comment,
|
||||
required this.source,
|
||||
required this.destination,
|
||||
required this.orchestrationState,
|
||||
|
||||
@@ -2,16 +2,20 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||
|
||||
|
||||
class PaymentAmountProvider with ChangeNotifier {
|
||||
double _amount = 10.0;
|
||||
double? _amount;
|
||||
bool _payerCoversFee = true;
|
||||
SettlementMode _settlementMode = SettlementMode.fixSource;
|
||||
String _comment = '';
|
||||
|
||||
double get amount => _amount;
|
||||
double? get amount => _amount;
|
||||
bool get payerCoversFee => _payerCoversFee;
|
||||
SettlementMode get settlementMode => _settlementMode;
|
||||
String get comment => _comment;
|
||||
|
||||
void setAmount(double value) {
|
||||
void setAmount(double? value) {
|
||||
if (_amount == value) return;
|
||||
_amount = value;
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -26,4 +30,10 @@ class PaymentAmountProvider with ChangeNotifier {
|
||||
_settlementMode = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setComment(String value) {
|
||||
if (_comment == value) return;
|
||||
_comment = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/payment/service.dart';
|
||||
import 'package:pshared/utils/exception.dart';
|
||||
|
||||
|
||||
class PaymentsProvider with ChangeNotifier {
|
||||
OrganizationsProvider? _organizations;
|
||||
String? _loadedOrganizationRef;
|
||||
@@ -253,9 +254,7 @@ class PaymentsProvider with ChangeNotifier {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
String? _paymentKey(Payment payment) {
|
||||
return _normalize(payment.paymentRef);
|
||||
}
|
||||
String? _paymentKey(Payment payment) => _normalize(payment.paymentRef);
|
||||
|
||||
List<String>? _normalizeStates(List<String>? states) {
|
||||
if (states == null || states.isEmpty) return null;
|
||||
|
||||
@@ -10,10 +10,8 @@ import 'package:pshared/models/payment/kind.dart';
|
||||
import 'package:pshared/models/payment/methods/card.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/iban.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/models/money.dart';
|
||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||
@@ -27,6 +25,7 @@ import 'package:pshared/utils/payment/fx_helpers.dart';
|
||||
|
||||
class QuotationIntentBuilder {
|
||||
static const String _settlementCurrency = 'RUB';
|
||||
static const String _addressBookCustomerFallbackId = 'address_book_customer';
|
||||
|
||||
PaymentIntent? build({
|
||||
required PaymentAmountProvider payment,
|
||||
@@ -38,9 +37,11 @@ class QuotationIntentBuilder {
|
||||
final sourceCurrency = source.selectedCurrencyCode;
|
||||
final paymentData = flow.selectedPaymentData;
|
||||
final selectedMethod = flow.selectedMethod;
|
||||
final amountValue = payment.amount;
|
||||
if (sourceMethod == null || sourceCurrency == null || paymentData == null) {
|
||||
return null;
|
||||
}
|
||||
if (amountValue == null) return null;
|
||||
|
||||
final customer = _buildCustomer(
|
||||
recipient: recipients.currentObject,
|
||||
@@ -51,7 +52,7 @@ class QuotationIntentBuilder {
|
||||
? _settlementCurrency
|
||||
: sourceCurrency;
|
||||
final amount = Money(
|
||||
amount: payment.amount.toString(),
|
||||
amount: amountValue.toString(),
|
||||
currency: amountCurrency,
|
||||
);
|
||||
final isLedgerSource = source.selectedLedgerAccount != null;
|
||||
@@ -65,6 +66,7 @@ class QuotationIntentBuilder {
|
||||
isLedgerSource: isLedgerSource,
|
||||
enabled: !isCryptoToCrypto,
|
||||
);
|
||||
final comment = _resolveComment(payment.comment);
|
||||
return PaymentIntent(
|
||||
kind: PaymentKind.payout,
|
||||
amount: amount,
|
||||
@@ -75,6 +77,7 @@ class QuotationIntentBuilder {
|
||||
? FeeTreatment.addToSource
|
||||
: FeeTreatment.deductFromDestination,
|
||||
settlementMode: payment.settlementMode,
|
||||
comment: comment,
|
||||
customer: customer,
|
||||
);
|
||||
}
|
||||
@@ -134,17 +137,14 @@ class QuotationIntentBuilder {
|
||||
required PaymentMethod? method,
|
||||
required PaymentMethodData? data,
|
||||
}) {
|
||||
final id = recipient?.id ?? method?.recipientRef;
|
||||
if (id == null || id.isEmpty) return null;
|
||||
final name = recipient?.name.trim();
|
||||
if (name == null || name.isEmpty) return null;
|
||||
final id = recipient?.id.trim();
|
||||
final customerId = id == null || id.isEmpty
|
||||
? _addressBookCustomerFallbackId
|
||||
: id;
|
||||
|
||||
final name = _resolveCustomerName(
|
||||
method: method,
|
||||
data: data,
|
||||
recipient: recipient,
|
||||
);
|
||||
final parts = name == null || name.trim().isEmpty
|
||||
? const <String>[]
|
||||
: name.trim().split(RegExp(r'\s+'));
|
||||
final parts = name.split(RegExp(r'\s+'));
|
||||
final firstName = parts.isNotEmpty ? parts.first : null;
|
||||
final lastName = parts.length >= 2 ? parts.last : null;
|
||||
final middleName = parts.length > 2
|
||||
@@ -152,7 +152,7 @@ class QuotationIntentBuilder {
|
||||
: null;
|
||||
|
||||
return Customer(
|
||||
id: id,
|
||||
id: customerId,
|
||||
firstName: firstName,
|
||||
middleName: middleName,
|
||||
lastName: lastName,
|
||||
@@ -160,33 +160,6 @@ class QuotationIntentBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
String? _resolveCustomerName({
|
||||
required PaymentMethod? method,
|
||||
required PaymentMethodData? data,
|
||||
required Recipient? recipient,
|
||||
}) {
|
||||
final card = method?.cardData ?? (data is CardPaymentMethod ? data : null);
|
||||
if (card != null) {
|
||||
final fullName = '${card.firstName} ${card.lastName}'.trim();
|
||||
if (fullName.isNotEmpty) return fullName;
|
||||
}
|
||||
|
||||
final iban = method?.ibanData ?? (data is IbanPaymentMethod ? data : null);
|
||||
if (iban != null && iban.accountHolder.trim().isNotEmpty) {
|
||||
return iban.accountHolder.trim();
|
||||
}
|
||||
|
||||
final bank =
|
||||
method?.bankAccountData ??
|
||||
(data is RussianBankAccountPaymentMethod ? data : null);
|
||||
if (bank != null && bank.recipientName.trim().isNotEmpty) {
|
||||
return bank.recipientName.trim();
|
||||
}
|
||||
|
||||
final recipientName = recipient?.name.trim();
|
||||
return recipientName?.isNotEmpty == true ? recipientName : null;
|
||||
}
|
||||
|
||||
String? _resolveCustomerCountry({
|
||||
required PaymentMethod? method,
|
||||
required PaymentMethodData? data,
|
||||
@@ -194,4 +167,9 @@ class QuotationIntentBuilder {
|
||||
final card = method?.cardData ?? (data is CardPaymentMethod ? data : null);
|
||||
return card?.country;
|
||||
}
|
||||
|
||||
String? _resolveComment(String comment) {
|
||||
final normalized = comment.trim();
|
||||
return normalized.isEmpty ? null : normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/state.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('PaymentOrchestrationState parser', () {
|
||||
test('maps v2 orchestration states', () {
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('orchestration_state_created'),
|
||||
PaymentOrchestrationState.created,
|
||||
);
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('ORCHESTRATION_STATE_EXECUTING'),
|
||||
PaymentOrchestrationState.executing,
|
||||
);
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue(
|
||||
'orchestration_state_needs_attention',
|
||||
),
|
||||
PaymentOrchestrationState.needsAttention,
|
||||
);
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('orchestration_state_settled'),
|
||||
PaymentOrchestrationState.settled,
|
||||
);
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('orchestration_state_failed'),
|
||||
PaymentOrchestrationState.failed,
|
||||
);
|
||||
});
|
||||
|
||||
test('maps legacy payment states for compatibility', () {
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('payment_state_accepted'),
|
||||
PaymentOrchestrationState.created,
|
||||
);
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('payment_state_submitted'),
|
||||
PaymentOrchestrationState.executing,
|
||||
);
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('payment_state_settled'),
|
||||
PaymentOrchestrationState.settled,
|
||||
);
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('payment_state_cancelled'),
|
||||
PaymentOrchestrationState.failed,
|
||||
);
|
||||
});
|
||||
|
||||
test('unknown state maps to unspecified', () {
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue('something_else'),
|
||||
PaymentOrchestrationState.unspecified,
|
||||
);
|
||||
expect(
|
||||
paymentOrchestrationStateFromValue(null),
|
||||
PaymentOrchestrationState.unspecified,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Payment model state helpers', () {
|
||||
test('isPending and isTerminal are derived from typed state', () {
|
||||
const created = Payment(
|
||||
paymentRef: 'p-1',
|
||||
state: 'orchestration_state_created',
|
||||
source: null,
|
||||
destination: null,
|
||||
orchestrationState: PaymentOrchestrationState.created,
|
||||
failureCode: null,
|
||||
failureReason: null,
|
||||
operations: [],
|
||||
lastQuote: null,
|
||||
metadata: null,
|
||||
createdAt: null,
|
||||
);
|
||||
const settled = Payment(
|
||||
paymentRef: 'p-2',
|
||||
state: 'orchestration_state_settled',
|
||||
source: null,
|
||||
destination: null,
|
||||
orchestrationState: PaymentOrchestrationState.settled,
|
||||
failureCode: null,
|
||||
failureReason: null,
|
||||
operations: [],
|
||||
lastQuote: null,
|
||||
metadata: null,
|
||||
createdAt: null,
|
||||
);
|
||||
|
||||
expect(created.isPending, isTrue);
|
||||
expect(created.isTerminal, isFalse);
|
||||
expect(settled.isPending, isFalse);
|
||||
expect(settled.isTerminal, isTrue);
|
||||
});
|
||||
|
||||
test('isFailure handles both explicit code and failed state', () {
|
||||
const withFailureCode = Payment(
|
||||
paymentRef: 'p-3',
|
||||
state: 'orchestration_state_executing',
|
||||
source: null,
|
||||
destination: null,
|
||||
orchestrationState: PaymentOrchestrationState.executing,
|
||||
failureCode: 'failure_ledger',
|
||||
failureReason: 'ledger failed',
|
||||
operations: [],
|
||||
lastQuote: null,
|
||||
metadata: null,
|
||||
createdAt: null,
|
||||
);
|
||||
const failedState = Payment(
|
||||
paymentRef: 'p-4',
|
||||
state: 'orchestration_state_failed',
|
||||
source: null,
|
||||
destination: null,
|
||||
orchestrationState: PaymentOrchestrationState.failed,
|
||||
failureCode: null,
|
||||
failureReason: null,
|
||||
operations: [],
|
||||
lastQuote: null,
|
||||
metadata: null,
|
||||
createdAt: null,
|
||||
);
|
||||
|
||||
expect(withFailureCode.isFailure, isTrue);
|
||||
expect(failedState.isFailure, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user