Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
52
lib/features/epargne/data/models/compte_epargne_model.dart
Normal file
52
lib/features/epargne/data/models/compte_epargne_model.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
/// Modèle d'un compte épargne (aligné API CompteEpargneResponse).
|
||||
class CompteEpargneModel {
|
||||
final String? id;
|
||||
final String? membreId;
|
||||
final String? organisationId;
|
||||
final String? numeroCompte;
|
||||
final String? typeCompte;
|
||||
final double soldeActuel;
|
||||
final double soldeBloque;
|
||||
final String? statut;
|
||||
final DateTime? dateOuverture;
|
||||
final DateTime? dateDerniereTransaction;
|
||||
final String? description;
|
||||
|
||||
const CompteEpargneModel({
|
||||
this.id,
|
||||
this.membreId,
|
||||
this.organisationId,
|
||||
this.numeroCompte,
|
||||
this.typeCompte,
|
||||
this.soldeActuel = 0,
|
||||
this.soldeBloque = 0,
|
||||
this.statut,
|
||||
this.dateOuverture,
|
||||
this.dateDerniereTransaction,
|
||||
this.description,
|
||||
});
|
||||
|
||||
factory CompteEpargneModel.fromJson(Map<String, dynamic> json) {
|
||||
return CompteEpargneModel(
|
||||
id: json['id']?.toString(),
|
||||
membreId: json['membreId']?.toString(),
|
||||
organisationId: json['organisationId']?.toString(),
|
||||
numeroCompte: json['numeroCompte'] as String?,
|
||||
typeCompte: json['typeCompte'] as String?,
|
||||
soldeActuel: _toDouble(json['soldeActuel']),
|
||||
soldeBloque: _toDouble(json['soldeBloque']),
|
||||
statut: json['statut'] as String?,
|
||||
dateOuverture: json['dateOuverture'] != null ? DateTime.tryParse(json['dateOuverture'].toString()) : null,
|
||||
dateDerniereTransaction: json['dateDerniereTransaction'] != null ? DateTime.tryParse(json['dateDerniereTransaction'].toString()) : null,
|
||||
description: json['description'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0;
|
||||
}
|
||||
|
||||
double get soldeDisponible => soldeActuel - soldeBloque;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/// Modèle d'une transaction épargne (aligné API TransactionEpargneResponse).
|
||||
class TransactionEpargneModel {
|
||||
final String? id;
|
||||
final String? compteId;
|
||||
final String? type; // DEPOT, RETRAIT, TRANSFERT_ENTRANT, TRANSFERT_SORTANT
|
||||
final double montant;
|
||||
final double soldeAvant;
|
||||
final double soldeApres;
|
||||
final String? motif;
|
||||
final DateTime? dateTransaction;
|
||||
final String? statutExecution; // REUSSIE, etc.
|
||||
final String? origineFonds;
|
||||
|
||||
const TransactionEpargneModel({
|
||||
this.id,
|
||||
this.compteId,
|
||||
this.type,
|
||||
this.montant = 0,
|
||||
this.soldeAvant = 0,
|
||||
this.soldeApres = 0,
|
||||
this.motif,
|
||||
this.dateTransaction,
|
||||
this.statutExecution,
|
||||
this.origineFonds,
|
||||
});
|
||||
|
||||
factory TransactionEpargneModel.fromJson(Map<String, dynamic> json) {
|
||||
return TransactionEpargneModel(
|
||||
id: json['id']?.toString(),
|
||||
compteId: json['compteId']?.toString(),
|
||||
type: json['type']?.toString(),
|
||||
montant: _toDouble(json['montant']),
|
||||
soldeAvant: _toDouble(json['soldeAvant']),
|
||||
soldeApres: _toDouble(json['soldeApres']),
|
||||
motif: json['motif'] as String?,
|
||||
dateTransaction: json['dateTransaction'] != null
|
||||
? DateTime.tryParse(json['dateTransaction'].toString())
|
||||
: null,
|
||||
statutExecution: json['statutExecution']?.toString(),
|
||||
origineFonds: json['origineFonds'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0;
|
||||
}
|
||||
|
||||
bool get isCredit =>
|
||||
type == 'DEPOT' || type == 'TRANSFERT_ENTRANT';
|
||||
bool get isDebit =>
|
||||
type == 'RETRAIT' || type == 'TRANSFERT_SORTANT';
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/// Modèle de requête pour une transaction épargne (aligné API backend).
|
||||
/// LCB-FT : origineFonds et pieceJustificativeId obligatoires au-dessus du seuil.
|
||||
class TransactionEpargneRequest {
|
||||
final String compteId;
|
||||
final String typeTransaction; // DEPOT, RETRAIT, TRANSFERT_ENTRANT, etc.
|
||||
final double montant;
|
||||
final String? compteDestinationId;
|
||||
final String? motif;
|
||||
final String? origineFonds;
|
||||
final String? pieceJustificativeId;
|
||||
|
||||
const TransactionEpargneRequest({
|
||||
required this.compteId,
|
||||
required this.typeTransaction,
|
||||
required this.montant,
|
||||
this.compteDestinationId,
|
||||
this.motif,
|
||||
this.origineFonds,
|
||||
this.pieceJustificativeId,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'compteId': compteId,
|
||||
'typeTransaction': typeTransaction,
|
||||
'montant': montant,
|
||||
if (compteDestinationId != null) 'compteDestinationId': compteDestinationId,
|
||||
if (motif != null && motif!.isNotEmpty) 'motif': motif,
|
||||
if (origineFonds != null && origineFonds!.isNotEmpty) 'origineFonds': origineFonds,
|
||||
if (pieceJustificativeId != null && pieceJustificativeId!.isNotEmpty)
|
||||
'pieceJustificativeId': pieceJustificativeId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import 'package:dio/dio.dart';
|
||||
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/compte_epargne_model.dart';
|
||||
import '../models/transaction_epargne_request.dart';
|
||||
|
||||
/// Repository des comptes épargne — API /api/v1/epargne/comptes.
|
||||
@lazySingleton
|
||||
class CompteEpargneRepository {
|
||||
final ApiClient _apiClient;
|
||||
static const String _baseComptes = '/api/v1/epargne/comptes';
|
||||
|
||||
CompteEpargneRepository(this._apiClient);
|
||||
|
||||
List<dynamic> _parseListResponse(dynamic data) {
|
||||
if (data is List) return data;
|
||||
if (data is Map && data.containsKey('content')) {
|
||||
final content = data['content'];
|
||||
return content is List ? content : [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Comptes épargne du membre connecté (GET /api/v1/epargne/comptes/mes-comptes).
|
||||
Future<List<CompteEpargneModel>> getMesComptes() async {
|
||||
try {
|
||||
final response = await _apiClient.get('$_baseComptes/mes-comptes');
|
||||
if (response.statusCode == 200) {
|
||||
final data = _parseListResponse(response.data);
|
||||
return data.map((e) => CompteEpargneModel.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
AppLogger.error('CompteEpargneRepository: getMesComptes status ${response.statusCode}');
|
||||
throw Exception('Impossible de charger les comptes: ${response.statusCode}');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('CompteEpargneRepository: getMesComptes échoué', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<CompteEpargneModel>> getByMembre(String membreId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('$_baseComptes/membre/$membreId');
|
||||
if (response.statusCode == 200) {
|
||||
final data = _parseListResponse(response.data);
|
||||
return data.map((e) => CompteEpargneModel.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
AppLogger.error('CompteEpargneRepository: getByMembre status ${response.statusCode}');
|
||||
throw Exception('Impossible de charger les comptes du membre: ${response.statusCode}');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('CompteEpargneRepository: getByMembre échoué', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<CompteEpargneModel?> getById(String id) async {
|
||||
final response = await _apiClient.get('$_baseComptes/$id');
|
||||
if (response.statusCode == 200) {
|
||||
return CompteEpargneModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Crée un compte épargne pour un membre (réservé admin / admin organisation).
|
||||
/// POST /api/v1/epargne/comptes
|
||||
Future<CompteEpargneModel> creerCompte({
|
||||
required String membreId,
|
||||
required String organisationId,
|
||||
required String typeCompte,
|
||||
String? notesOuverture,
|
||||
}) async {
|
||||
final body = <String, dynamic>{
|
||||
'membreId': membreId,
|
||||
'organisationId': organisationId,
|
||||
'typeCompte': typeCompte,
|
||||
};
|
||||
if (notesOuverture != null && notesOuverture.isNotEmpty) {
|
||||
body['notesOuverture'] = notesOuverture;
|
||||
}
|
||||
final response = await _apiClient.post(_baseComptes, data: body);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return CompteEpargneModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Erreur création compte épargne: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Repository des transactions épargne — API /api/v1/epargne/transactions.
|
||||
/// LCB-FT : le backend exige origineFonds au-dessus du seuil configuré.
|
||||
@lazySingleton
|
||||
class TransactionEpargneRepository {
|
||||
final ApiClient _apiClient;
|
||||
static const String _base = '/api/v1/epargne/transactions';
|
||||
|
||||
TransactionEpargneRepository(this._apiClient);
|
||||
|
||||
/// Exécute une transaction (dépôt, retrait, etc.).
|
||||
Future<Map<String, dynamic>> executer(TransactionEpargneRequest request) async {
|
||||
final response = await _apiClient.post(_base, data: request.toJson());
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
throw Exception('Erreur transaction épargne: ${response.statusCode}');
|
||||
}
|
||||
|
||||
/// Transfert entre deux comptes.
|
||||
Future<Map<String, dynamic>> transferer(TransactionEpargneRequest request) async {
|
||||
final response = await _apiClient.post('$_base/transfert', data: request.toJson());
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
throw Exception('Erreur transfert: ${response.statusCode}');
|
||||
}
|
||||
|
||||
/// Historique des transactions d'un compte.
|
||||
Future<List<Map<String, dynamic>>> getByCompte(String compteId) async {
|
||||
final response = await _apiClient.get('$_base/compte/$compteId');
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
if (data is List) return List<Map<String, dynamic>>.from(data.map((e) => e as Map<String, dynamic>));
|
||||
return [];
|
||||
}
|
||||
throw Exception('Erreur chargement historique: ${response.statusCode}');
|
||||
}
|
||||
|
||||
/// Initie un dépôt sur compte épargne via Wave (même API que cotisations).
|
||||
/// Retourne l'URL à ouvrir (wave_launch_url) pour confirmer dans l'app Wave.
|
||||
Future<DepotWaveResult> initierDepotEpargneEnLigne({
|
||||
required String compteId,
|
||||
required double montant,
|
||||
required String numeroTelephone,
|
||||
}) async {
|
||||
final response = await _apiClient.post(
|
||||
'/api/paiements/initier-depot-epargne-en-ligne',
|
||||
data: {
|
||||
'compteId': compteId,
|
||||
'montant': montant,
|
||||
'numeroTelephone': numeroTelephone.replaceAll(RegExp(r'\D'), ''),
|
||||
},
|
||||
);
|
||||
if (response.statusCode != 201 && response.statusCode != 200) {
|
||||
final msg = response.data is Map
|
||||
? (response.data['message'] ?? response.data['error'] ?? response.statusCode)
|
||||
: response.statusCode;
|
||||
throw Exception('Impossible d\'initier le dépôt: $msg');
|
||||
}
|
||||
final data = response.data is Map<String, dynamic>
|
||||
? response.data as Map<String, dynamic>
|
||||
: Map<String, dynamic>.from(response.data as Map);
|
||||
return DepotWaveResult(
|
||||
waveLaunchUrl: data['waveLaunchUrl'] as String? ?? data['redirectUrl'] as String? ?? '',
|
||||
redirectUrl: data['redirectUrl'] as String? ?? data['waveLaunchUrl'] as String? ?? '',
|
||||
message: data['message'] as String? ?? 'Ouvrez Wave pour confirmer le dépôt.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Résultat de l'initiation d'un dépôt Wave (épargne).
|
||||
class DepotWaveResult {
|
||||
final String waveLaunchUrl;
|
||||
final String redirectUrl;
|
||||
final String message;
|
||||
|
||||
const DepotWaveResult({
|
||||
required this.waveLaunchUrl,
|
||||
required this.redirectUrl,
|
||||
required this.message,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user