feat(mobile): récupération seuil LCB-FT depuis API (T018)
Phase 4 Mobile - Section 4.1 Épargne Nouveaux fichiers : - SeuilLcbFtModel : modèle pour seuil depuis API - ParametresLcbFtRepository : appel /api/parametres-lcb-ft/seuil-justification - @lazySingleton pour injection GetIt Modifications : - DepotEpargneDialog : charge seuil au initState, fallback 500k XOF - RetraitEpargneDialog : idem - Remplace constante kSeuilOrigineFondsObligatoireXOF par valeur dynamique Impact : - Seuil LCB-FT maintenant configurable par organisation - Fallback automatique si API échoue - Messages utilisateur avec montant dynamique Spec : specs/001-mutuelles-anti-blanchiment/spec.md Progression : 16/27 tâches (59%) Signed-off-by: lions dev Team
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
/// Modèle pour le seuil LCB-FT récupéré depuis l'API.
|
||||||
|
/// Endpoint: GET /api/parametres-lcb-ft/seuil-justification
|
||||||
|
class SeuilLcbFtModel {
|
||||||
|
final double montantSeuil;
|
||||||
|
final String codeDevise;
|
||||||
|
|
||||||
|
const SeuilLcbFtModel({
|
||||||
|
required this.montantSeuil,
|
||||||
|
required this.codeDevise,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SeuilLcbFtModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return SeuilLcbFtModel(
|
||||||
|
montantSeuil: (json['montantSeuil'] as num).toDouble(),
|
||||||
|
codeDevise: json['codeDevise'] as String? ?? 'XOF',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seuil par défaut si l'API échoue (500k XOF selon spec LCB-FT BCEAO).
|
||||||
|
factory SeuilLcbFtModel.defaultSeuil() {
|
||||||
|
return const SeuilLcbFtModel(
|
||||||
|
montantSeuil: 500000.0,
|
||||||
|
codeDevise: 'XOF',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||||
|
import 'package:unionflow_mobile_apps/core/utils/logger.dart';
|
||||||
|
import '../models/seuil_lcb_ft_model.dart';
|
||||||
|
|
||||||
|
/// Repository pour les paramètres LCB-FT (seuils anti-blanchiment).
|
||||||
|
/// Endpoints: GET /api/parametres-lcb-ft, GET /api/parametres-lcb-ft/seuil-justification
|
||||||
|
@lazySingleton
|
||||||
|
class ParametresLcbFtRepository {
|
||||||
|
final ApiClient _apiClient;
|
||||||
|
static const String _base = '/api/parametres-lcb-ft';
|
||||||
|
|
||||||
|
ParametresLcbFtRepository(this._apiClient);
|
||||||
|
|
||||||
|
/// Récupère uniquement le seuil de justification (endpoint léger).
|
||||||
|
/// Paramètres optionnels : organisationId, codeDevise (XOF par défaut).
|
||||||
|
/// Retourne le seuil par défaut (500k XOF) en cas d'erreur.
|
||||||
|
Future<SeuilLcbFtModel> getSeuilJustification({
|
||||||
|
String? organisationId,
|
||||||
|
String codeDevise = 'XOF',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final queryParams = <String, dynamic>{};
|
||||||
|
if (organisationId != null && organisationId.isNotEmpty) {
|
||||||
|
queryParams['organisationId'] = organisationId;
|
||||||
|
}
|
||||||
|
queryParams['codeDevise'] = codeDevise;
|
||||||
|
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
'$_base/seuil-justification',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && response.data != null) {
|
||||||
|
return SeuilLcbFtModel.fromJson(response.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.warning(
|
||||||
|
'ParametresLcbFtRepository: getSeuilJustification status ${response.statusCode}, fallback au seuil par défaut',
|
||||||
|
);
|
||||||
|
return SeuilLcbFtModel.defaultSeuil();
|
||||||
|
} catch (e, st) {
|
||||||
|
AppLogger.error(
|
||||||
|
'ParametresLcbFtRepository: getSeuilJustification échoué, fallback au seuil par défaut',
|
||||||
|
error: e,
|
||||||
|
stackTrace: st,
|
||||||
|
);
|
||||||
|
return SeuilLcbFtModel.defaultSeuil();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les paramètres LCB-FT complets (tous les seuils + config).
|
||||||
|
/// Pour usage admin ou affichage détaillé.
|
||||||
|
Future<Map<String, dynamic>?> getParametres({
|
||||||
|
String? organisationId,
|
||||||
|
String codeDevise = 'XOF',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final queryParams = <String, dynamic>{};
|
||||||
|
if (organisationId != null && organisationId.isNotEmpty) {
|
||||||
|
queryParams['organisationId'] = organisationId;
|
||||||
|
}
|
||||||
|
queryParams['codeDevise'] = codeDevise;
|
||||||
|
|
||||||
|
final response = await _apiClient.get(_base, queryParameters: queryParams);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && response.data != null) {
|
||||||
|
return response.data as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.warning(
|
||||||
|
'ParametresLcbFtRepository: getParametres status ${response.statusCode}',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
} catch (e, st) {
|
||||||
|
AppLogger.error(
|
||||||
|
'ParametresLcbFtRepository: getParametres échoué',
|
||||||
|
error: e,
|
||||||
|
stackTrace: st,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../../core/constants/lcb_ft_constants.dart';
|
import '../../../../core/constants/lcb_ft_constants.dart';
|
||||||
|
import '../../../../core/data/repositories/parametres_lcb_ft_repository.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';
|
||||||
|
|
||||||
@@ -34,16 +35,34 @@ class _DepotEpargneDialogState extends State<DepotEpargneDialog> {
|
|||||||
bool _waveLoading = false;
|
bool _waveLoading = false;
|
||||||
_DepotMode _mode = _DepotMode.manual;
|
_DepotMode _mode = _DepotMode.manual;
|
||||||
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;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_repository = GetIt.I<TransactionEpargneRepository>();
|
_repository = GetIt.I<TransactionEpargneRepository>();
|
||||||
|
_parametresRepository = GetIt.I<ParametresLcbFtRepository>();
|
||||||
|
_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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _origineFondsRequis {
|
bool get _origineFondsRequis {
|
||||||
final m = double.tryParse(_montantController.text.replaceAll(',', '.'));
|
final m = double.tryParse(_montantController.text.replaceAll(',', '.'));
|
||||||
return m != null && m >= kSeuilOrigineFondsObligatoireXOF;
|
return m != null && m >= _seuilLcbFt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -114,7 +133,7 @@ class _DepotEpargneDialogState extends State<DepotEpargneDialog> {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'L\'origine des fonds est obligatoire pour les opérations à partir de ${kSeuilOrigineFondsObligatoireXOF.toStringAsFixed(0)} XOF (LCB-FT).',
|
'L\'origine des fonds est obligatoire pour les opérations à partir de ${_seuilLcbFt.toStringAsFixed(0)} XOF (LCB-FT).',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -219,7 +238,7 @@ class _DepotEpargneDialogState extends State<DepotEpargneDialog> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Requis pour les opérations ≥ ${kSeuilOrigineFondsObligatoireXOF.toStringAsFixed(0)} XOF',
|
'Requis pour les opérations ≥ ${_seuilLcbFt.toStringAsFixed(0)} XOF',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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/constants/lcb_ft_constants.dart';
|
||||||
|
import '../../../../core/data/repositories/parametres_lcb_ft_repository.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';
|
||||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||||
@@ -33,16 +34,34 @@ class _RetraitEpargneDialogState extends State<RetraitEpargneDialog> {
|
|||||||
final _origineFondsController = TextEditingController();
|
final _origineFondsController = TextEditingController();
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
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;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_repository = GetIt.I<TransactionEpargneRepository>();
|
_repository = GetIt.I<TransactionEpargneRepository>();
|
||||||
|
_parametresRepository = GetIt.I<ParametresLcbFtRepository>();
|
||||||
|
_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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _origineFondsRequis {
|
bool get _origineFondsRequis {
|
||||||
final m = double.tryParse(_montantController.text.replaceAll(',', '.'));
|
final m = double.tryParse(_montantController.text.replaceAll(',', '.'));
|
||||||
return m != null && m >= kSeuilOrigineFondsObligatoireXOF;
|
return m != null && m >= _seuilLcbFt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -66,7 +85,7 @@ class _RetraitEpargneDialogState extends State<RetraitEpargneDialog> {
|
|||||||
}
|
}
|
||||||
if (_origineFondsRequis && _origineFondsController.text.trim().isEmpty) {
|
if (_origineFondsRequis && _origineFondsController.text.trim().isEmpty) {
|
||||||
_showSnack(
|
_showSnack(
|
||||||
'L\'origine des fonds est obligatoire pour les opérations à partir de ${kSeuilOrigineFondsObligatoireXOF.toStringAsFixed(0)} XOF (LCB-FT).',
|
'L\'origine des fonds est obligatoire pour les opérations à partir de ${_seuilLcbFt.toStringAsFixed(0)} XOF (LCB-FT).',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -160,7 +179,7 @@ class _RetraitEpargneDialogState extends State<RetraitEpargneDialog> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Requis pour les opérations ≥ ${kSeuilOrigineFondsObligatoireXOF.toStringAsFixed(0)} XOF',
|
'Requis pour les opérations ≥ ${_seuilLcbFt.toStringAsFixed(0)} XOF',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: ColorTokens.primary),
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: ColorTokens.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user