feat(mobile): ajout validation LCB-FT au transfert épargne (T019)
Phase 4 Mobile - Section 4.1 Épargne Modifications TransfertEpargneDialog : - Import ParametresLcbFtRepository + lcb_ft_constants - Chargement seuil LCB-FT au initState (comme dépôt/retrait) - Ajout champ origineFonds avec validation conditionnelle - Validation : montant >= seuil → origine fonds obligatoire - Message clair pour utilisateur avec montant seuil dynamique - onChanged sur montant pour mise à jour UI en temps réel Impact : - Les 3 types d'opérations (dépôt, retrait, transfert) ont maintenant la validation LCB-FT - Champ origineFonds transmis dans TransactionEpargneRequest - Conformité BCEAO/OHADA sur tous les flux épargne Spec : specs/001-mutuelles-anti-blanchiment/spec.md Progression : 17/27 tâches (63%) Signed-off-by: lions dev Team
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
|
import '../../../../core/constants/lcb_ft_constants.dart';
|
||||||
|
import '../../../../core/data/repositories/parametres_lcb_ft_repository.dart';
|
||||||
import '../../data/models/compte_epargne_model.dart';
|
import '../../data/models/compte_epargne_model.dart';
|
||||||
import '../../data/models/transaction_epargne_request.dart';
|
import '../../data/models/transaction_epargne_request.dart';
|
||||||
import '../../data/repositories/transaction_epargne_repository.dart';
|
import '../../data/repositories/transaction_epargne_repository.dart';
|
||||||
@@ -27,9 +29,15 @@ class _TransfertEpargneDialogState extends State<TransfertEpargneDialog> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _montantController = TextEditingController();
|
final _montantController = TextEditingController();
|
||||||
final _motifController = TextEditingController();
|
final _motifController = TextEditingController();
|
||||||
|
final _origineFondsController = TextEditingController();
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
String? _compteDestinationId;
|
String? _compteDestinationId;
|
||||||
late TransactionEpargneRepository _repository;
|
late TransactionEpargneRepository _repository;
|
||||||
|
late ParametresLcbFtRepository _parametresRepository;
|
||||||
|
|
||||||
|
/// Seuil LCB-FT récupéré depuis l'API (fallback à 500k XOF).
|
||||||
|
double _seuilLcbFt = kSeuilOrigineFondsObligatoireXOF;
|
||||||
|
bool _seuilLoaded = false;
|
||||||
|
|
||||||
List<CompteEpargneModel> get _comptesDestination {
|
List<CompteEpargneModel> get _comptesDestination {
|
||||||
if (widget.compteSource.id == null) return [];
|
if (widget.compteSource.id == null) return [];
|
||||||
@@ -38,17 +46,36 @@ class _TransfertEpargneDialogState extends State<TransfertEpargneDialog> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _origineFondsRequis {
|
||||||
|
final m = double.tryParse(_montantController.text.replaceAll(',', '.'));
|
||||||
|
return m != null && m >= _seuilLcbFt;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_repository = GetIt.I<TransactionEpargneRepository>();
|
_repository = GetIt.I<TransactionEpargneRepository>();
|
||||||
|
_parametresRepository = GetIt.I<ParametresLcbFtRepository>();
|
||||||
if (_comptesDestination.isNotEmpty) _compteDestinationId = _comptesDestination.first.id;
|
if (_comptesDestination.isNotEmpty) _compteDestinationId = _comptesDestination.first.id;
|
||||||
|
_chargerSeuil();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Charge le seuil LCB-FT depuis l'API au chargement du dialog.
|
||||||
|
Future<void> _chargerSeuil() async {
|
||||||
|
final seuil = await _parametresRepository.getSeuilJustification();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_seuilLcbFt = seuil.montantSeuil;
|
||||||
|
_seuilLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_montantController.dispose();
|
_montantController.dispose();
|
||||||
_motifController.dispose();
|
_motifController.dispose();
|
||||||
|
_origineFondsController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +99,16 @@ class _TransfertEpargneDialogState extends State<TransfertEpargneDialog> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_origineFondsRequis && _origineFondsController.text.trim().isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'L\'origine des fonds est obligatoire pour les opérations à partir de ${_seuilLcbFt.toStringAsFixed(0)} XOF (LCB-FT).',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
try {
|
try {
|
||||||
final request = TransactionEpargneRequest(
|
final request = TransactionEpargneRequest(
|
||||||
@@ -80,6 +117,7 @@ class _TransfertEpargneDialogState extends State<TransfertEpargneDialog> {
|
|||||||
montant: montant,
|
montant: montant,
|
||||||
compteDestinationId: _compteDestinationId,
|
compteDestinationId: _compteDestinationId,
|
||||||
motif: _motifController.text.trim().isEmpty ? null : _motifController.text.trim(),
|
motif: _motifController.text.trim().isEmpty ? null : _motifController.text.trim(),
|
||||||
|
origineFonds: _origineFondsController.text.trim().isEmpty ? null : _origineFondsController.text.trim(),
|
||||||
);
|
);
|
||||||
await _repository.transferer(request);
|
await _repository.transferer(request);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -171,6 +209,7 @@ class _TransfertEpargneDialogState extends State<TransfertEpargneDialog> {
|
|||||||
if (n > solde) return 'Solde insuffisant';
|
if (n > solde) return 'Solde insuffisant';
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -181,6 +220,24 @@ class _TransfertEpargneDialogState extends State<TransfertEpargneDialog> {
|
|||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _origineFondsController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Origine des fonds (LCB-FT)',
|
||||||
|
hintText: _origineFondsRequis ? 'Obligatoire au-dessus du seuil' : 'Optionnel',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
if (_origineFondsRequis)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Text(
|
||||||
|
'Requis pour les opérations ≥ ${_seuilLcbFt.toStringAsFixed(0)} XOF',
|
||||||
|
style: TypographyTokens.bodySmall?.copyWith(color: ColorTokens.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user