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:
dahoud
2026-03-15 02:42:46 +00:00
parent 74161dfc89
commit 5ef8ae1140

View File

@@ -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),
),
),
], ],
), ),
), ),