added comment for payment, changed intent and added amount ui in operations #719
@@ -1,8 +1,10 @@
|
|||||||
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/money.dart';
|
||||||
|
|
||||||
part 'operation.g.dart';
|
part 'operation.g.dart';
|
||||||
|
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class PaymentOperationDTO {
|
class PaymentOperationDTO {
|
||||||
final String? stepRef;
|
final String? stepRef;
|
||||||
@@ -11,7 +13,8 @@ class PaymentOperationDTO {
|
|||||||
final String? code;
|
final String? code;
|
||||||
final String? state;
|
final String? state;
|
||||||
final String? label;
|
final String? label;
|
||||||
final PaymentOperationMoneyDTO? money;
|
final MoneyDTO? amount;
|
||||||
|
final MoneyDTO? convertedAmount;
|
||||||
final String? failureCode;
|
final String? failureCode;
|
||||||
final String? failureReason;
|
final String? failureReason;
|
||||||
final String? startedAt;
|
final String? startedAt;
|
||||||
@@ -24,7 +27,8 @@ class PaymentOperationDTO {
|
|||||||
this.code,
|
this.code,
|
||||||
this.state,
|
this.state,
|
||||||
this.label,
|
this.label,
|
||||||
this.money,
|
this.amount,
|
||||||
|
this.convertedAmount,
|
||||||
this.failureCode,
|
this.failureCode,
|
||||||
this.failureReason,
|
this.failureReason,
|
||||||
this.startedAt,
|
this.startedAt,
|
||||||
@@ -35,29 +39,3 @@ class PaymentOperationDTO {
|
|||||||
_$PaymentOperationDTOFromJson(json);
|
_$PaymentOperationDTOFromJson(json);
|
||||||
Map<String, dynamic> toJson() => _$PaymentOperationDTOToJson(this);
|
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 {
|
class PaymentDTO {
|
||||||
final String? paymentRef;
|
final String? paymentRef;
|
||||||
final String? state;
|
final String? state;
|
||||||
|
final String? comment;
|
||||||
final PaymentResponseEndpointDTO? source;
|
final PaymentResponseEndpointDTO? source;
|
||||||
final PaymentResponseEndpointDTO? destination;
|
final PaymentResponseEndpointDTO? destination;
|
||||||
final String? failureCode;
|
final String? failureCode;
|
||||||
@@ -23,6 +24,7 @@ class PaymentDTO {
|
|||||||
const PaymentDTO({
|
const PaymentDTO({
|
||||||
this.paymentRef,
|
this.paymentRef,
|
||||||
this.state,
|
this.state,
|
||||||
|
this.comment,
|
||||||
this.source,
|
this.source,
|
||||||
this.destination,
|
this.destination,
|
||||||
this.failureCode,
|
this.failureCode,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:pshared/data/dto/payment/operation.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';
|
||||||
|
|
||||||
|
|
||||||
extension PaymentOperationDTOMapper on PaymentOperationDTO {
|
extension PaymentOperationDTOMapper on PaymentOperationDTO {
|
||||||
PaymentExecutionOperation toDomain() => PaymentExecutionOperation(
|
PaymentExecutionOperation toDomain() => PaymentExecutionOperation(
|
||||||
stepRef: stepRef,
|
stepRef: stepRef,
|
||||||
@@ -10,7 +11,8 @@ extension PaymentOperationDTOMapper on PaymentOperationDTO {
|
|||||||
code: code,
|
code: code,
|
||||||
state: state,
|
state: state,
|
||||||
label: label,
|
label: label,
|
||||||
money: money?.toDomain(),
|
amount: amount?.toDomain(),
|
||||||
|
convertedAmount: convertedAmount?.toDomain(),
|
||||||
failureCode: failureCode,
|
failureCode: failureCode,
|
||||||
failureReason: failureReason,
|
failureReason: failureReason,
|
||||||
startedAt: _parseDateTime(startedAt),
|
startedAt: _parseDateTime(startedAt),
|
||||||
@@ -26,7 +28,8 @@ extension PaymentExecutionOperationMapper on PaymentExecutionOperation {
|
|||||||
code: code,
|
code: code,
|
||||||
state: state,
|
state: state,
|
||||||
label: label,
|
label: label,
|
||||||
money: money?.toDTO(),
|
amount: amount?.toDTO(),
|
||||||
|
convertedAmount: convertedAmount?.toDTO(),
|
||||||
failureCode: failureCode,
|
failureCode: failureCode,
|
||||||
failureReason: failureReason,
|
failureReason: failureReason,
|
||||||
startedAt: startedAt?.toUtc().toIso8601String(),
|
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) {
|
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;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ extension PaymentDTOMapper on PaymentDTO {
|
|||||||
Payment toDomain() => Payment(
|
Payment toDomain() => Payment(
|
||||||
paymentRef: paymentRef,
|
paymentRef: paymentRef,
|
||||||
state: state,
|
state: state,
|
||||||
|
comment: comment,
|
||||||
source: source?.toDomain(),
|
source: source?.toDomain(),
|
||||||
destination: destination?.toDomain(),
|
destination: destination?.toDomain(),
|
||||||
orchestrationState: paymentOrchestrationStateFromValue(state),
|
orchestrationState: paymentOrchestrationStateFromValue(state),
|
||||||
@@ -26,6 +27,7 @@ extension PaymentMapper on Payment {
|
|||||||
PaymentDTO toDTO() => PaymentDTO(
|
PaymentDTO toDTO() => PaymentDTO(
|
||||||
paymentRef: paymentRef,
|
paymentRef: paymentRef,
|
||||||
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
||||||
|
comment: comment,
|
||||||
source: source?.toDTO(),
|
source: source?.toDTO(),
|
||||||
destination: destination?.toDTO(),
|
destination: destination?.toDTO(),
|
||||||
failureCode: failureCode,
|
failureCode: failureCode,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:pshared/models/money.dart';
|
import 'package:pshared/models/money.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentExecutionOperation {
|
class PaymentExecutionOperation {
|
||||||
final String? stepRef;
|
final String? stepRef;
|
||||||
final String? operationRef;
|
final String? operationRef;
|
||||||
@@ -7,7 +8,8 @@ class PaymentExecutionOperation {
|
|||||||
final String? code;
|
final String? code;
|
||||||
final String? state;
|
final String? state;
|
||||||
final String? label;
|
final String? label;
|
||||||
final PaymentExecutionOperationMoney? money;
|
final Money? amount;
|
||||||
|
final Money? convertedAmount;
|
||||||
final String? failureCode;
|
final String? failureCode;
|
||||||
final String? failureReason;
|
final String? failureReason;
|
||||||
final DateTime? startedAt;
|
final DateTime? startedAt;
|
||||||
@@ -20,30 +22,11 @@ class PaymentExecutionOperation {
|
|||||||
required this.code,
|
required this.code,
|
||||||
required this.state,
|
required this.state,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.money,
|
required this.amount,
|
||||||
|
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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/methods/type.dart';
|
||||||
import 'package:pshared/models/payment/status.dart';
|
import 'package:pshared/models/payment/status.dart';
|
||||||
|
|
||||||
|
|
||||||
class OperationItem {
|
class OperationItem {
|
||||||
final OperationStatus status;
|
final OperationStatus status;
|
||||||
final String? fileName;
|
final String? fileName;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:pshared/models/payment/state.dart';
|
|||||||
class Payment {
|
class Payment {
|
||||||
final String? paymentRef;
|
final String? paymentRef;
|
||||||
final String? state;
|
final String? state;
|
||||||
|
final String? comment;
|
||||||
final PaymentEndpoint? source;
|
final PaymentEndpoint? source;
|
||||||
final PaymentEndpoint? destination;
|
final PaymentEndpoint? destination;
|
||||||
final PaymentOrchestrationState orchestrationState;
|
final PaymentOrchestrationState orchestrationState;
|
||||||
@@ -20,6 +21,7 @@ class Payment {
|
|||||||
const Payment({
|
const Payment({
|
||||||
required this.paymentRef,
|
required this.paymentRef,
|
||||||
required this.state,
|
required this.state,
|
||||||
|
required this.comment,
|
||||||
required this.source,
|
required this.source,
|
||||||
required this.destination,
|
required this.destination,
|
||||||
required this.orchestrationState,
|
required this.orchestrationState,
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentAmountProvider with ChangeNotifier {
|
class PaymentAmountProvider with ChangeNotifier {
|
||||||
double _amount = 10.0;
|
double? _amount;
|
||||||
bool _payerCoversFee = true;
|
bool _payerCoversFee = true;
|
||||||
SettlementMode _settlementMode = SettlementMode.fixSource;
|
SettlementMode _settlementMode = SettlementMode.fixSource;
|
||||||
|
String _comment = '';
|
||||||
|
|
||||||
double get amount => _amount;
|
double? get amount => _amount;
|
||||||
bool get payerCoversFee => _payerCoversFee;
|
bool get payerCoversFee => _payerCoversFee;
|
||||||
SettlementMode get settlementMode => _settlementMode;
|
SettlementMode get settlementMode => _settlementMode;
|
||||||
|
String get comment => _comment;
|
||||||
|
|
||||||
void setAmount(double value) {
|
void setAmount(double? value) {
|
||||||
|
if (_amount == value) return;
|
||||||
_amount = value;
|
_amount = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -26,4 +30,10 @@ class PaymentAmountProvider with ChangeNotifier {
|
|||||||
_settlementMode = value;
|
_settlementMode = value;
|
||||||
notifyListeners();
|
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/service/payment/service.dart';
|
||||||
import 'package:pshared/utils/exception.dart';
|
import 'package:pshared/utils/exception.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentsProvider with ChangeNotifier {
|
class PaymentsProvider with ChangeNotifier {
|
||||||
OrganizationsProvider? _organizations;
|
OrganizationsProvider? _organizations;
|
||||||
String? _loadedOrganizationRef;
|
String? _loadedOrganizationRef;
|
||||||
@@ -253,9 +254,7 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _paymentKey(Payment payment) {
|
String? _paymentKey(Payment payment) => _normalize(payment.paymentRef);
|
||||||
return _normalize(payment.paymentRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String>? _normalizeStates(List<String>? states) {
|
List<String>? _normalizeStates(List<String>? states) {
|
||||||
if (states == null || states.isEmpty) return null;
|
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/card.dart';
|
||||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||||
import 'package:pshared/models/payment/methods/data.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/ledger.dart';
|
||||||
import 'package:pshared/models/payment/methods/managed_wallet.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/payment/methods/type.dart';
|
||||||
import 'package:pshared/models/money.dart';
|
import 'package:pshared/models/money.dart';
|
||||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||||
@@ -27,6 +25,7 @@ import 'package:pshared/utils/payment/fx_helpers.dart';
|
|||||||
|
|
||||||
class QuotationIntentBuilder {
|
class QuotationIntentBuilder {
|
||||||
static const String _settlementCurrency = 'RUB';
|
static const String _settlementCurrency = 'RUB';
|
||||||
|
static const String _addressBookCustomerFallbackId = 'address_book_customer';
|
||||||
|
|
||||||
PaymentIntent? build({
|
PaymentIntent? build({
|
||||||
required PaymentAmountProvider payment,
|
required PaymentAmountProvider payment,
|
||||||
@@ -38,9 +37,11 @@ class QuotationIntentBuilder {
|
|||||||
final sourceCurrency = source.selectedCurrencyCode;
|
final sourceCurrency = source.selectedCurrencyCode;
|
||||||
final paymentData = flow.selectedPaymentData;
|
final paymentData = flow.selectedPaymentData;
|
||||||
final selectedMethod = flow.selectedMethod;
|
final selectedMethod = flow.selectedMethod;
|
||||||
|
final amountValue = payment.amount;
|
||||||
if (sourceMethod == null || sourceCurrency == null || paymentData == null) {
|
if (sourceMethod == null || sourceCurrency == null || paymentData == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (amountValue == null) return null;
|
||||||
|
|
||||||
final customer = _buildCustomer(
|
final customer = _buildCustomer(
|
||||||
recipient: recipients.currentObject,
|
recipient: recipients.currentObject,
|
||||||
@@ -51,7 +52,7 @@ class QuotationIntentBuilder {
|
|||||||
? _settlementCurrency
|
? _settlementCurrency
|
||||||
: sourceCurrency;
|
: sourceCurrency;
|
||||||
final amount = Money(
|
final amount = Money(
|
||||||
amount: payment.amount.toString(),
|
amount: amountValue.toString(),
|
||||||
currency: amountCurrency,
|
currency: amountCurrency,
|
||||||
);
|
);
|
||||||
final isLedgerSource = source.selectedLedgerAccount != null;
|
final isLedgerSource = source.selectedLedgerAccount != null;
|
||||||
@@ -65,6 +66,7 @@ class QuotationIntentBuilder {
|
|||||||
isLedgerSource: isLedgerSource,
|
isLedgerSource: isLedgerSource,
|
||||||
enabled: !isCryptoToCrypto,
|
enabled: !isCryptoToCrypto,
|
||||||
);
|
);
|
||||||
|
final comment = _resolveComment(payment.comment);
|
||||||
return PaymentIntent(
|
return PaymentIntent(
|
||||||
kind: PaymentKind.payout,
|
kind: PaymentKind.payout,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
@@ -75,6 +77,7 @@ class QuotationIntentBuilder {
|
|||||||
? FeeTreatment.addToSource
|
? FeeTreatment.addToSource
|
||||||
: FeeTreatment.deductFromDestination,
|
: FeeTreatment.deductFromDestination,
|
||||||
settlementMode: payment.settlementMode,
|
settlementMode: payment.settlementMode,
|
||||||
|
comment: comment,
|
||||||
customer: customer,
|
customer: customer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -134,17 +137,14 @@ class QuotationIntentBuilder {
|
|||||||
required PaymentMethod? method,
|
required PaymentMethod? method,
|
||||||
required PaymentMethodData? data,
|
required PaymentMethodData? data,
|
||||||
}) {
|
}) {
|
||||||
final id = recipient?.id ?? method?.recipientRef;
|
final name = recipient?.name.trim();
|
||||||
if (id == null || id.isEmpty) return null;
|
if (name == null || name.isEmpty) return null;
|
||||||
|
final id = recipient?.id.trim();
|
||||||
|
final customerId = id == null || id.isEmpty
|
||||||
|
? _addressBookCustomerFallbackId
|
||||||
|
: id;
|
||||||
|
|
||||||
final name = _resolveCustomerName(
|
final parts = name.split(RegExp(r'\s+'));
|
||||||
method: method,
|
|
||||||
data: data,
|
|
||||||
recipient: recipient,
|
|
||||||
);
|
|
||||||
final parts = name == null || name.trim().isEmpty
|
|
||||||
? const <String>[]
|
|
||||||
: name.trim().split(RegExp(r'\s+'));
|
|
||||||
final firstName = parts.isNotEmpty ? parts.first : null;
|
final firstName = parts.isNotEmpty ? parts.first : null;
|
||||||
final lastName = parts.length >= 2 ? parts.last : null;
|
final lastName = parts.length >= 2 ? parts.last : null;
|
||||||
final middleName = parts.length > 2
|
final middleName = parts.length > 2
|
||||||
@@ -152,7 +152,7 @@ class QuotationIntentBuilder {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Customer(
|
return Customer(
|
||||||
id: id,
|
id: customerId,
|
||||||
firstName: firstName,
|
firstName: firstName,
|
||||||
middleName: middleName,
|
middleName: middleName,
|
||||||
lastName: lastName,
|
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({
|
String? _resolveCustomerCountry({
|
||||||
required PaymentMethod? method,
|
required PaymentMethod? method,
|
||||||
required PaymentMethodData? data,
|
required PaymentMethodData? data,
|
||||||
@@ -194,4 +167,9 @@ class QuotationIntentBuilder {
|
|||||||
final card = method?.cardData ?? (data is CardPaymentMethod ? data : null);
|
final card = method?.cardData ?? (data is CardPaymentMethod ? data : null);
|
||||||
return card?.country;
|
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -145,16 +145,27 @@ RouteBase payoutShellRoute() => ShellRoute(
|
|||||||
update: (context, verification, controller) =>
|
update: (context, verification, controller) =>
|
||||||
controller!..update(verification),
|
controller!..update(verification),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider4<
|
ChangeNotifierProxyProvider5<
|
||||||
PaymentProvider,
|
PaymentProvider,
|
||||||
QuotationProvider,
|
QuotationProvider,
|
||||||
PaymentFlowProvider,
|
PaymentFlowProvider,
|
||||||
|
PaymentAmountProvider,
|
||||||
RecipientsProvider,
|
RecipientsProvider,
|
||||||
PaymentPageController
|
PaymentPageController
|
||||||
>(
|
>(
|
||||||
create: (_) => PaymentPageController(),
|
create: (_) => PaymentPageController(),
|
||||||
update: (context, payment, quotation, flow, recipients, controller) =>
|
update:
|
||||||
controller!..update(payment, quotation, flow, recipients),
|
(
|
||||||
|
context,
|
||||||
|
payment,
|
||||||
|
quotation,
|
||||||
|
flow,
|
||||||
|
amount,
|
||||||
|
recipients,
|
||||||
|
controller,
|
||||||
|
) =>
|
||||||
|
controller!
|
||||||
|
..update(payment, quotation, flow, amount, recipients),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider<
|
ChangeNotifierProxyProvider<
|
||||||
OrganizationsProvider,
|
OrganizationsProvider,
|
||||||
@@ -301,6 +312,12 @@ RouteBase payoutShellRoute() => ShellRoute(
|
|||||||
name: PayoutRoutes.payment,
|
name: PayoutRoutes.payment,
|
||||||
path: PayoutRoutes.paymentPath,
|
path: PayoutRoutes.paymentPath,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
|
final amountProvider = context.read<PaymentAmountProvider>();
|
||||||
|
if (amountProvider.comment.isNotEmpty) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
amountProvider.setComment('');
|
||||||
|
});
|
||||||
|
}
|
||||||
final fallbackDestination = PayoutDestination.dashboard;
|
final fallbackDestination = PayoutDestination.dashboard;
|
||||||
|
|
||||||
return NoTransitionPage(
|
return NoTransitionPage(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:pshared/utils/money.dart';
|
|||||||
|
|
||||||
import 'package:pweb/models/payment/amount/mode.dart';
|
import 'package:pweb/models/payment/amount/mode.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentAmountFieldController extends ChangeNotifier {
|
class PaymentAmountFieldController extends ChangeNotifier {
|
||||||
static const String _settlementCurrencyCode = 'RUB';
|
static const String _settlementCurrencyCode = 'RUB';
|
||||||
|
|
||||||
@@ -19,9 +20,9 @@ class PaymentAmountFieldController extends ChangeNotifier {
|
|||||||
bool _isSyncingText = false;
|
bool _isSyncingText = false;
|
||||||
PaymentAmountMode _mode = PaymentAmountMode.debit;
|
PaymentAmountMode _mode = PaymentAmountMode.debit;
|
||||||
|
|
||||||
PaymentAmountFieldController({required double initialAmount})
|
PaymentAmountFieldController({required double? initialAmount})
|
||||||
: textController = TextEditingController(
|
: textController = TextEditingController(
|
||||||
text: amountToString(initialAmount),
|
text: initialAmount == null ? '' : amountToString(initialAmount),
|
||||||
);
|
);
|
||||||
|
|
||||||
PaymentAmountMode get mode => _mode;
|
PaymentAmountMode get mode => _mode;
|
||||||
@@ -57,9 +58,7 @@ class PaymentAmountFieldController extends ChangeNotifier {
|
|||||||
void handleChanged(String value) {
|
void handleChanged(String value) {
|
||||||
if (_isSyncingText) return;
|
if (_isSyncingText) return;
|
||||||
final parsed = _parseAmount(value);
|
final parsed = _parseAmount(value);
|
||||||
if (parsed != null) {
|
_provider?.setAmount(parsed);
|
||||||
_provider?.setAmount(parsed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleModeChanged(PaymentAmountMode value) {
|
void handleModeChanged(PaymentAmountMode value) {
|
||||||
@@ -130,11 +129,11 @@ class PaymentAmountFieldController extends ChangeNotifier {
|
|||||||
return parsed.isNaN ? null : parsed;
|
return parsed.isNaN ? null : parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncTextWithAmount(double amount) {
|
void _syncTextWithAmount(double? amount) {
|
||||||
final parsedText = _parseAmount(textController.text);
|
final parsedText = _parseAmount(textController.text);
|
||||||
if (parsedText != null && parsedText == amount) return;
|
if (parsedText == amount) return;
|
||||||
|
|
||||||
final nextText = amountToString(amount);
|
final nextText = amount == null ? '' : amountToString(amount);
|
||||||
_isSyncingText = true;
|
_isSyncingText = true;
|
||||||
textController.value = TextEditingValue(
|
textController.value = TextEditingValue(
|
||||||
text: nextText,
|
text: nextText,
|
||||||
|
|||||||
53
frontend/pweb/lib/controllers/payments/comment_field.dart
Normal file
53
frontend/pweb/lib/controllers/payments/comment_field.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/provider/payment/amount.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentCommentFieldController extends ChangeNotifier {
|
||||||
|
final TextEditingController textController;
|
||||||
|
|
||||||
|
PaymentAmountProvider? _provider;
|
||||||
|
bool _isSyncingText = false;
|
||||||
|
|
||||||
|
PaymentCommentFieldController({required String initialComment})
|
||||||
|
: textController = TextEditingController(text: initialComment);
|
||||||
|
|
||||||
|
void update(PaymentAmountProvider provider) {
|
||||||
|
if (identical(_provider, provider)) {
|
||||||
|
_syncTextWithComment(provider.comment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_provider?.removeListener(_handleProviderChanged);
|
||||||
|
_provider = provider;
|
||||||
|
_provider?.addListener(_handleProviderChanged);
|
||||||
|
_syncTextWithComment(provider.comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleChanged(String value) {
|
||||||
|
if (_isSyncingText) return;
|
||||||
|
_provider?.setComment(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleProviderChanged() {
|
||||||
|
final provider = _provider;
|
||||||
|
if (provider == null) return;
|
||||||
|
_syncTextWithComment(provider.comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncTextWithComment(String comment) {
|
||||||
|
if (textController.text == comment) return;
|
||||||
|
_isSyncingText = true;
|
||||||
|
textController.value = TextEditingValue(
|
||||||
|
text: comment,
|
||||||
|
selection: TextSelection.collapsed(offset: comment.length),
|
||||||
|
);
|
||||||
|
_isSyncingText = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_provider?.removeListener(_handleProviderChanged);
|
||||||
|
textController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:pshared/provider/payment/flow.dart';
|
import 'package:pshared/provider/payment/flow.dart';
|
||||||
|
import 'package:pshared/provider/payment/amount.dart';
|
||||||
import 'package:pshared/provider/payment/provider.dart';
|
import 'package:pshared/provider/payment/provider.dart';
|
||||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
@@ -14,6 +15,7 @@ class PaymentPageController extends ChangeNotifier {
|
|||||||
PaymentProvider? _payment;
|
PaymentProvider? _payment;
|
||||||
QuotationProvider? _quotation;
|
QuotationProvider? _quotation;
|
||||||
PaymentFlowProvider? _flow;
|
PaymentFlowProvider? _flow;
|
||||||
|
PaymentAmountProvider? _amount;
|
||||||
RecipientsProvider? _recipients;
|
RecipientsProvider? _recipients;
|
||||||
|
|
||||||
bool _isSending = false;
|
bool _isSending = false;
|
||||||
@@ -26,11 +28,13 @@ class PaymentPageController extends ChangeNotifier {
|
|||||||
PaymentProvider payment,
|
PaymentProvider payment,
|
||||||
QuotationProvider quotation,
|
QuotationProvider quotation,
|
||||||
PaymentFlowProvider flow,
|
PaymentFlowProvider flow,
|
||||||
|
PaymentAmountProvider amount,
|
||||||
RecipientsProvider recipients,
|
RecipientsProvider recipients,
|
||||||
) {
|
) {
|
||||||
_payment = payment;
|
_payment = payment;
|
||||||
_quotation = quotation;
|
_quotation = quotation;
|
||||||
_flow = flow;
|
_flow = flow;
|
||||||
|
_amount = amount;
|
||||||
_recipients = recipients;
|
_recipients = recipients;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +63,7 @@ class PaymentPageController extends ChangeNotifier {
|
|||||||
_quotation?.reset();
|
_quotation?.reset();
|
||||||
_payment?.reset();
|
_payment?.reset();
|
||||||
_flow?.setManualPaymentData(null);
|
_flow?.setManualPaymentData(null);
|
||||||
|
_amount?.setComment('');
|
||||||
_recipients?.setCurrentObject(null);
|
_recipients?.setCurrentObject(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class CsvPayoutRow {
|
|||||||
final int expMonth;
|
final int expMonth;
|
||||||
final int expYear;
|
final int expYear;
|
||||||
final String amount;
|
final String amount;
|
||||||
|
final String? comment;
|
||||||
|
|
||||||
const CsvPayoutRow({
|
const CsvPayoutRow({
|
||||||
required this.pan,
|
required this.pan,
|
||||||
@@ -13,5 +14,6 @@ class CsvPayoutRow {
|
|||||||
required this.expMonth,
|
required this.expMonth,
|
||||||
required this.expYear,
|
required this.expYear,
|
||||||
required this.amount,
|
required this.amount,
|
||||||
|
this.comment,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
27
frontend/pweb/lib/pages/dashboard/payouts/comment/field.dart
Normal file
27
frontend/pweb/lib/pages/dashboard/payouts/comment/field.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/controllers/payments/comment_field.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentCommentField extends StatelessWidget {
|
||||||
|
const PaymentCommentField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
final controller = context.watch<PaymentCommentFieldController>();
|
||||||
|
|
||||||
|
return TextField(
|
||||||
|
controller: controller.textController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '${loc.comment} (${loc.optional})',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: controller.handleChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/provider/payment/amount.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/controllers/payments/comment_field.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/payouts/comment/field.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentCommentWidget extends StatelessWidget {
|
||||||
|
const PaymentCommentWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProxyProvider<
|
||||||
|
PaymentAmountProvider,
|
||||||
|
PaymentCommentFieldController
|
||||||
|
>(
|
||||||
|
create: (ctx) {
|
||||||
|
final initialComment = ctx.read<PaymentAmountProvider>().comment;
|
||||||
|
return PaymentCommentFieldController(initialComment: initialComment);
|
||||||
|
},
|
||||||
|
update: (ctx, amountProvider, controller) {
|
||||||
|
controller!.update(amountProvider);
|
||||||
|
return controller;
|
||||||
|
},
|
||||||
|
child: const PaymentCommentField(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:pweb/controllers/payouts/quotation.dart';
|
import 'package:pweb/controllers/payouts/quotation.dart';
|
||||||
import 'package:pweb/models/dashboard/quote_status_data.dart';
|
import 'package:pweb/models/dashboard/quote_status_data.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/amount/widget.dart';
|
import 'package:pweb/pages/dashboard/payouts/amount/widget.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/payouts/comment/widget.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/fee_payer.dart';
|
import 'package:pweb/pages/dashboard/payouts/fee_payer.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/card.dart';
|
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/card.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/refresh_section.dart';
|
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/refresh_section.dart';
|
||||||
@@ -23,7 +24,6 @@ class PaymentFormWidget extends StatelessWidget {
|
|||||||
static const double _columnSpacing = 24;
|
static const double _columnSpacing = 24;
|
||||||
static const double _narrowWidth = 560;
|
static const double _narrowWidth = 560;
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -66,6 +66,8 @@ class PaymentFormWidget extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const PaymentAmountWidget(),
|
const PaymentAmountWidget(),
|
||||||
|
const SizedBox(height: _mediumSpacing),
|
||||||
|
const PaymentCommentWidget(),
|
||||||
const SizedBox(height: _smallSpacing),
|
const SizedBox(height: _smallSpacing),
|
||||||
FeePayerSwitch(
|
FeePayerSwitch(
|
||||||
spacing: _smallSpacing,
|
spacing: _smallSpacing,
|
||||||
@@ -104,12 +106,9 @@ class PaymentFormWidget extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(flex: 3, child: const PaymentAmountWidget()),
|
||||||
flex: 3,
|
|
||||||
child: const PaymentAmountWidget(),
|
|
||||||
),
|
|
||||||
const SizedBox(width: _columnSpacing),
|
const SizedBox(width: _columnSpacing),
|
||||||
Expanded(flex: 2, child: quoteCard),
|
Expanded(flex: 2, child: PaymentCommentWidget()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: _smallSpacing),
|
const SizedBox(height: _smallSpacing),
|
||||||
@@ -136,8 +135,9 @@ class PaymentFormWidget extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
autoRefreshSection,
|
quoteCard,
|
||||||
],
|
const SizedBox(height: _mediumSpacing),
|
||||||
|
autoRefreshSection],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||||
|
|
||||||
|
|
||||||
const String sampleFileName = 'sample.csv';
|
const String sampleFileName = 'sample.csv';
|
||||||
|
|
||||||
final List<CsvPayoutRow> sampleRows = [
|
final List<CsvPayoutRow> sampleRows = [
|
||||||
@@ -11,6 +10,7 @@ final List<CsvPayoutRow> sampleRows = [
|
|||||||
expMonth: 12,
|
expMonth: 12,
|
||||||
expYear: 27,
|
expYear: 27,
|
||||||
amount: "500",
|
amount: "500",
|
||||||
|
comment: "Salary payout",
|
||||||
),
|
),
|
||||||
CsvPayoutRow(
|
CsvPayoutRow(
|
||||||
pan: "9022****12",
|
pan: "9022****12",
|
||||||
@@ -27,16 +27,26 @@ final List<CsvPayoutRow> sampleRows = [
|
|||||||
expMonth: 3,
|
expMonth: 3,
|
||||||
expYear: 28,
|
expYear: 28,
|
||||||
amount: "120",
|
amount: "120",
|
||||||
|
comment: "Refund",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
String buildSampleCsvContent() {
|
String buildSampleCsvContent() {
|
||||||
final rows = <String>[
|
final rows = <String>[
|
||||||
'pan,first_name,last_name,exp_month,exp_year,amount',
|
'pan,first_name,last_name,exp_month,exp_year,amount,comment',
|
||||||
...sampleRows.map(
|
...sampleRows.map(
|
||||||
(row) =>
|
(row) =>
|
||||||
'${row.pan},${row.firstName},${row.lastName},${row.expMonth},${row.expYear},${row.amount}',
|
'${row.pan},${row.firstName},${row.lastName},${row.expMonth},${row.expYear},${row.amount},${_escapeCsvCell(row.comment)}',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
return rows.join('\n');
|
return rows.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _escapeCsvCell(String? value) {
|
||||||
|
if (value == null || value.isEmpty) return '';
|
||||||
|
if (!value.contains(',') && !value.contains('"') && !value.contains('\n')) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
final escaped = value.replaceAll('"', '""');
|
||||||
|
return '"$escaped"';
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
|
|
||||||
class FileFormatSampleTable extends StatelessWidget {
|
class FileFormatSampleTable extends StatelessWidget {
|
||||||
const FileFormatSampleTable({
|
const FileFormatSampleTable({super.key, required this.rows});
|
||||||
super.key,
|
|
||||||
required this.rows,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<CsvPayoutRow> rows;
|
final List<CsvPayoutRow> rows;
|
||||||
|
|
||||||
@@ -24,6 +21,7 @@ class FileFormatSampleTable extends StatelessWidget {
|
|||||||
DataColumn(label: Text(l10n.lastName)),
|
DataColumn(label: Text(l10n.lastName)),
|
||||||
DataColumn(label: Text(l10n.expiryDate)),
|
DataColumn(label: Text(l10n.expiryDate)),
|
||||||
DataColumn(label: Text(l10n.amountColumn)),
|
DataColumn(label: Text(l10n.amountColumn)),
|
||||||
|
DataColumn(label: Text(l10n.commentColumn)),
|
||||||
],
|
],
|
||||||
rows: rows.map((row) {
|
rows: rows.map((row) {
|
||||||
return DataRow(
|
return DataRow(
|
||||||
@@ -35,6 +33,7 @@ class FileFormatSampleTable extends StatelessWidget {
|
|||||||
Text('${row.expMonth.toString().padLeft(2, '0')}/${row.expYear}'),
|
Text('${row.expMonth.toString().padLeft(2, '0')}/${row.expYear}'),
|
||||||
),
|
),
|
||||||
DataCell(Text(row.amount)),
|
DataCell(Text(row.amount)),
|
||||||
|
DataCell(Text(row.comment ?? '')),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:pshared/models/payment/type.dart';
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/payment/amount.dart';
|
||||||
|
|
||||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
import 'package:pweb/controllers/payments/page_ui.dart';
|
import 'package:pweb/controllers/payments/page_ui.dart';
|
||||||
@@ -46,6 +49,7 @@ class _PaymentPageState extends State<PaymentPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
context.read<PaymentAmountProvider>().setComment('');
|
||||||
_uiController.dispose();
|
_uiController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:pshared/models/payment/execution_operation.dart';
|
|||||||
|
|
||||||
import 'package:pweb/utils/report/operations/state_mapper.dart';
|
import 'package:pweb/utils/report/operations/state_mapper.dart';
|
||||||
import 'package:pweb/pages/report/details/sections/operations/state_chip.dart';
|
import 'package:pweb/pages/report/details/sections/operations/state_chip.dart';
|
||||||
|
import 'package:pweb/utils/money_display.dart';
|
||||||
import 'package:pweb/utils/report/operations/time_format.dart';
|
import 'package:pweb/utils/report/operations/time_format.dart';
|
||||||
import 'package:pweb/utils/report/operations/title_mapper.dart';
|
import 'package:pweb/utils/report/operations/title_mapper.dart';
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ class OperationHistoryTile extends StatelessWidget {
|
|||||||
final operationLabel = operation.label?.trim();
|
final operationLabel = operation.label?.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 canDownload = canDownloadDocument && onDownloadDocument != null;
|
final canDownload = canDownloadDocument && onDownloadDocument != null;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -67,7 +69,14 @@ class OperationHistoryTile extends StatelessWidget {
|
|||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${loc.amountColumn}: $amount',
|
||||||
|
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(
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ class MultipleCsvParser {
|
|||||||
'expiry_year',
|
'expiry_year',
|
||||||
]);
|
]);
|
||||||
final amountIndex = _resolveHeaderIndex(header, const ['amount', 'sum']);
|
final amountIndex = _resolveHeaderIndex(header, const ['amount', 'sum']);
|
||||||
|
final commentIndex = _resolveHeaderIndex(header, const ['comment']);
|
||||||
|
|
||||||
if (panIndex < 0 ||
|
if (panIndex < 0 ||
|
||||||
firstNameIndex < 0 ||
|
firstNameIndex < 0 ||
|
||||||
lastNameIndex < 0 ||
|
lastNameIndex < 0 ||
|
||||||
(expDateIndex < 0 &&
|
(expDateIndex < 0 && (expMonthIndex < 0 || expYearIndex < 0)) ||
|
||||||
(expMonthIndex < 0 || expYearIndex < 0)) ||
|
|
||||||
amountIndex < 0) {
|
amountIndex < 0) {
|
||||||
throw FormatException(
|
throw FormatException(
|
||||||
'CSV header must contain pan, first_name, last_name, amount columns and either exp_date/expiry or exp_month and exp_year',
|
'CSV header must contain pan, first_name, last_name, amount columns and either exp_date/expiry or exp_month and exp_year',
|
||||||
@@ -67,6 +67,7 @@ class MultipleCsvParser {
|
|||||||
final expMonthRaw = _cell(raw, expMonthIndex);
|
final expMonthRaw = _cell(raw, expMonthIndex);
|
||||||
final expYearRaw = _cell(raw, expYearIndex);
|
final expYearRaw = _cell(raw, expYearIndex);
|
||||||
final amount = _normalizeAmount(_cell(raw, amountIndex));
|
final amount = _normalizeAmount(_cell(raw, amountIndex));
|
||||||
|
final comment = commentIndex >= 0 ? _cell(raw, commentIndex) : '';
|
||||||
|
|
||||||
if (pan.isEmpty) {
|
if (pan.isEmpty) {
|
||||||
throw FormatException('CSV row ${i + 1}: pan is required');
|
throw FormatException('CSV row ${i + 1}: pan is required');
|
||||||
@@ -117,6 +118,7 @@ class MultipleCsvParser {
|
|||||||
expMonth: expMonth,
|
expMonth: expMonth,
|
||||||
expYear: expYear,
|
expYear: expYear,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
|
comment: _normalizeComment(comment),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -206,6 +208,11 @@ class MultipleCsvParser {
|
|||||||
return value.trim().replaceAll(' ', '').replaceAll(',', '.');
|
return value.trim().replaceAll(' ', '').replaceAll(',', '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _normalizeComment(String value) {
|
||||||
|
final normalized = value.trim();
|
||||||
|
return normalized.isEmpty ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
_ExpiryDate _parseExpiryDate(String value, int rowNumber) {
|
_ExpiryDate _parseExpiryDate(String value, int rowNumber) {
|
||||||
final match = RegExp(r'^\s*(\d{1,2})\s*/\s*(\d{2})\s*$').firstMatch(value);
|
final match = RegExp(r'^\s*(\d{1,2})\s*/\s*(\d{2})\s*$').firstMatch(value);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:pshared/models/money.dart';
|
import 'package:pshared/models/money.dart';
|
||||||
|
import 'package:pshared/models/payment/customer.dart';
|
||||||
import 'package:pshared/models/payment/fees/treatment.dart';
|
import 'package:pshared/models/payment/fees/treatment.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/kind.dart';
|
||||||
@@ -9,6 +10,7 @@ import 'package:pshared/utils/payment/fx_helpers.dart';
|
|||||||
|
|
||||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||||
|
|
||||||
|
|
||||||
class MultipleIntentBuilder {
|
class MultipleIntentBuilder {
|
||||||
static const String _currency = 'RUB';
|
static const String _currency = 'RUB';
|
||||||
|
|
||||||
@@ -23,22 +25,33 @@ class MultipleIntentBuilder {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
.map((row) {
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
final rowIndex = entry.key;
|
||||||
|
final row = entry.value;
|
||||||
final amount = Money(amount: row.amount, currency: _currency);
|
final amount = Money(amount: row.amount, currency: _currency);
|
||||||
|
final destination = CardPaymentMethod(
|
||||||
|
pan: row.pan,
|
||||||
|
firstName: row.firstName,
|
||||||
|
lastName: row.lastName,
|
||||||
|
expMonth: row.expMonth,
|
||||||
|
expYear: row.expYear,
|
||||||
|
);
|
||||||
return PaymentIntent(
|
return PaymentIntent(
|
||||||
kind: PaymentKind.payout,
|
kind: PaymentKind.payout,
|
||||||
source: sourceMethod,
|
source: sourceMethod,
|
||||||
destination: CardPaymentMethod(
|
destination: destination,
|
||||||
pan: row.pan,
|
|
||||||
firstName: row.firstName,
|
|
||||||
lastName: row.lastName,
|
|
||||||
expMonth: row.expMonth,
|
|
||||||
expYear: row.expYear,
|
|
||||||
),
|
|
||||||
amount: amount,
|
amount: amount,
|
||||||
feeTreatment: FeeTreatment.addToSource,
|
feeTreatment: FeeTreatment.addToSource,
|
||||||
settlementMode: SettlementMode.fixReceived,
|
settlementMode: SettlementMode.fixReceived,
|
||||||
fx: fxIntent,
|
fx: fxIntent,
|
||||||
|
comment: row.comment,
|
||||||
|
customer: Customer(
|
||||||
|
id: 'csv_row_${rowIndex + 1}',
|
||||||
|
firstName: destination.firstName,
|
||||||
|
lastName: destination.lastName,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ OperationItem mapPaymentToOperation(Payment payment) {
|
|||||||
_firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef]) ?? '-';
|
_firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef]) ?? '-';
|
||||||
final comment =
|
final comment =
|
||||||
_firstNonEmpty([
|
_firstNonEmpty([
|
||||||
|
payment.comment,
|
||||||
payment.failureReason,
|
payment.failureReason,
|
||||||
payment.failureCode,
|
payment.failureCode,
|
||||||
payment.state,
|
payment.state,
|
||||||
|
|||||||
Reference in New Issue
Block a user