diff --git a/lib/features/finance_workflow/data/datasources/finance_workflow_remote_datasource.dart b/lib/features/finance_workflow/data/datasources/finance_workflow_remote_datasource.dart index fbbaf14..f8c9e02 100644 --- a/lib/features/finance_workflow/data/datasources/finance_workflow_remote_datasource.dart +++ b/lib/features/finance_workflow/data/datasources/finance_workflow_remote_datasource.dart @@ -129,6 +129,66 @@ class FinanceWorkflowRemoteDatasource { } } + Future requestApproval({ + required String transactionId, + required String transactionType, + required double amount, + String? organizationId, + }) async { + final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/approvals'); + + final body = json.encode({ + 'transactionId': transactionId, + 'transactionType': transactionType, + 'amount': amount, + if (organizationId != null) 'organizationId': organizationId, + }); + + final response = await client.post( + uri, + headers: await _getHeaders(), + body: body, + ); + + if (response.statusCode == 201 || response.statusCode == 200) { + return TransactionApprovalModel.fromJson(json.decode(response.body)); + } else if (response.statusCode == 401) { + throw UnauthorizedException(); + } else if (response.statusCode == 400) { + throw ValidationException('Données invalides'); + } else { + throw ServerException('Erreur lors de la demande d\'approbation'); + } + } + + Future> getApprovalsHistory({ + required String organizationId, + DateTime? startDate, + DateTime? endDate, + String? status, + }) async { + final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/approvals/history') + .replace(queryParameters: { + 'organizationId': organizationId, + if (startDate != null) 'startDate': startDate.toIso8601String(), + if (endDate != null) 'endDate': endDate.toIso8601String(), + if (status != null) 'status': status, + }); + + final response = await client.get(uri, headers: await _getHeaders()); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList + .map((json) => TransactionApprovalModel.fromJson(json)) + .toList(); + } else if (response.statusCode == 401) { + throw UnauthorizedException(); + } else { + throw ServerException('Erreur lors de la récupération de l\'historique'); + } + } + // === BUDGETS === Future> getBudgets({ @@ -226,4 +286,160 @@ class FinanceWorkflowRemoteDatasource { throw ServerException('Erreur lors de la récupération du suivi budgétaire'); } } + + Future updateBudget({ + required String budgetId, + required Map updates, + }) async { + final uri = + Uri.parse('${AppConfig.apiBaseUrl}/api/finance/budgets/$budgetId'); + + final body = json.encode(updates); + + final response = await client.put( + uri, + headers: await _getHeaders(), + body: body, + ); + + if (response.statusCode == 200) { + return BudgetModel.fromJson(json.decode(response.body)); + } else if (response.statusCode == 404) { + throw NotFoundException('Budget non trouvé'); + } else if (response.statusCode == 401) { + throw UnauthorizedException(); + } else if (response.statusCode == 400) { + throw ValidationException('Données invalides'); + } else { + throw ServerException('Erreur lors de la mise à jour du budget'); + } + } + + Future deleteBudget({required String budgetId}) async { + final uri = + Uri.parse('${AppConfig.apiBaseUrl}/api/finance/budgets/$budgetId'); + + final response = await client.delete(uri, headers: await _getHeaders()); + + if (response.statusCode != 204 && response.statusCode != 200) { + if (response.statusCode == 404) { + throw NotFoundException('Budget non trouvé'); + } else if (response.statusCode == 401) { + throw UnauthorizedException(); + } else { + throw ServerException('Erreur lors de la suppression du budget'); + } + } + } + + // === WORKFLOW STATS & AUDIT === + + Future> getWorkflowStats({ + String? organizationId, + DateTime? startDate, + DateTime? endDate, + }) async { + final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/stats') + .replace(queryParameters: { + if (organizationId != null) 'organizationId': organizationId, + if (startDate != null) 'startDate': startDate.toIso8601String(), + if (endDate != null) 'endDate': endDate.toIso8601String(), + }); + + final response = await client.get(uri, headers: await _getHeaders()); + + if (response.statusCode == 200) { + return json.decode(response.body) as Map; + } else if (response.statusCode == 401) { + throw UnauthorizedException(); + } else { + throw ServerException('Erreur lors de la récupération des statistiques'); + } + } + + Future>> getAuditLogs({ + String? organizationId, + DateTime? startDate, + DateTime? endDate, + String? operation, + String? entityType, + String? severity, + int limit = 100, + }) async { + final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/audit-logs') + .replace(queryParameters: { + if (organizationId != null) 'organizationId': organizationId, + if (startDate != null) 'startDate': startDate.toIso8601String(), + if (endDate != null) 'endDate': endDate.toIso8601String(), + if (operation != null) 'operation': operation, + if (entityType != null) 'entityType': entityType, + if (severity != null) 'severity': severity, + 'limit': limit.toString(), + }); + + final response = await client.get(uri, headers: await _getHeaders()); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((e) => e as Map).toList(); + } else if (response.statusCode == 401) { + throw UnauthorizedException(); + } else { + throw ServerException('Erreur lors de la récupération des logs d\'audit'); + } + } + + Future>> getAnomalies({ + String? organizationId, + DateTime? startDate, + DateTime? endDate, + }) async { + final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/audit-logs/anomalies') + .replace(queryParameters: { + if (organizationId != null) 'organizationId': organizationId, + if (startDate != null) 'startDate': startDate.toIso8601String(), + if (endDate != null) 'endDate': endDate.toIso8601String(), + }); + + final response = await client.get(uri, headers: await _getHeaders()); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((e) => e as Map).toList(); + } else if (response.statusCode == 401) { + throw UnauthorizedException(); + } else { + throw ServerException('Erreur lors de la récupération des anomalies'); + } + } + + Future> exportAuditLogs({ + String? organizationId, + String format = 'csv', + DateTime? startDate, + DateTime? endDate, + }) async { + final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/audit-logs/export'); + + final body = json.encode({ + if (organizationId != null) 'organizationId': organizationId, + 'format': format, + if (startDate != null) 'startDate': startDate.toIso8601String(), + if (endDate != null) 'endDate': endDate.toIso8601String(), + }); + + final response = await client.post( + uri, + headers: await _getHeaders(), + body: body, + ); + + if (response.statusCode == 200) { + return json.decode(response.body) as Map; + } else if (response.statusCode == 401) { + throw UnauthorizedException(); + } else { + throw ServerException('Erreur lors de l\'export des logs d\'audit'); + } + } } diff --git a/lib/features/finance_workflow/data/repositories/finance_workflow_repository_impl.dart b/lib/features/finance_workflow/data/repositories/finance_workflow_repository_impl.dart index 010d942..39f7ac2 100644 --- a/lib/features/finance_workflow/data/repositories/finance_workflow_repository_impl.dart +++ b/lib/features/finance_workflow/data/repositories/finance_workflow_repository_impl.dart @@ -325,7 +325,7 @@ class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository { return false; } - // === MÉTHODES NON IMPLÉMENTÉES (Stubs) === + // === WORKFLOW APPROVALS (EXTENDED) === @override Future> requestApproval({ @@ -333,8 +333,29 @@ class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository { required TransactionType transactionType, required double amount, }) async { - return Left( - NotImplementedFailure('Fonctionnalité en cours de développement')); + if (!await networkInfo.isConnected) { + return Left(NetworkFailure('Pas de connexion Internet')); + } + + try { + final approval = await _retryPolicy.execute( + operation: () => remoteDatasource.requestApproval( + transactionId: transactionId, + transactionType: transactionType.name, + amount: amount, + ), + shouldRetry: (error) => _isRetryableError(error), + ); + return Right(approval); + } on UnauthorizedException { + return Left(UnauthorizedFailure('Session expirée')); + } on ValidationException catch (e) { + return Left(ValidationFailure(e.message)); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnexpectedFailure('Erreur inattendue: $e')); + } } @override @@ -344,10 +365,36 @@ class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository { DateTime? endDate, ApprovalStatus? status, }) async { - return Left( - NotImplementedFailure('Fonctionnalité en cours de développement')); + if (!await networkInfo.isConnected) { + return Left(NetworkFailure('Pas de connexion Internet')); + } + + try { + if (organizationId == null) { + return Left(ValidationFailure('organizationId requis')); + } + + final approvals = await _retryPolicy.execute( + operation: () => remoteDatasource.getApprovalsHistory( + organizationId: organizationId, + startDate: startDate, + endDate: endDate, + status: status?.name, + ), + shouldRetry: (error) => _isRetryableError(error), + ); + return Right(approvals); + } on UnauthorizedException { + return Left(UnauthorizedFailure('Session expirée')); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnexpectedFailure('Erreur inattendue: $e')); + } } + // === BUDGETS (EXTENDED) === + @override Future> updateBudget({ required String budgetId, @@ -356,16 +403,63 @@ class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository { List? lines, BudgetStatus? status, }) async { - return Left( - NotImplementedFailure('Fonctionnalité en cours de développement')); + if (!await networkInfo.isConnected) { + return Left(NetworkFailure('Pas de connexion Internet')); + } + + try { + final updates = {}; + if (name != null) updates['name'] = name; + if (description != null) updates['description'] = description; + if (status != null) updates['status'] = status.name; + // Note: lines update not implemented in backend yet + + final budget = await _retryPolicy.execute( + operation: () => remoteDatasource.updateBudget( + budgetId: budgetId, + updates: updates, + ), + shouldRetry: (error) => _isRetryableError(error), + ); + return Right(budget); + } on UnauthorizedException { + return Left(UnauthorizedFailure('Session expirée')); + } on NotFoundException catch (e) { + return Left(NotFoundFailure(e.message)); + } on ValidationException catch (e) { + return Left(ValidationFailure(e.message)); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnexpectedFailure('Erreur inattendue: $e')); + } } @override Future> deleteBudget(String budgetId) async { - return Left( - NotImplementedFailure('Fonctionnalité en cours de développement')); + if (!await networkInfo.isConnected) { + return Left(NetworkFailure('Pas de connexion Internet')); + } + + try { + await _retryPolicy.execute( + operation: () => remoteDatasource.deleteBudget(budgetId: budgetId), + shouldRetry: (error) => _isRetryableError(error), + ); + return const Right(null); + } on UnauthorizedException { + return Left(UnauthorizedFailure('Session expirée')); + } on NotFoundException catch (e) { + return Left(NotFoundFailure(e.message)); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnexpectedFailure('Erreur inattendue: $e')); + } } + // === AUDIT & STATS === + @override Future>> getAuditLogs({ String? organizationId, @@ -376,8 +470,36 @@ class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository { AuditSeverity? severity, int? limit, }) async { - return Left( - NotImplementedFailure('Fonctionnalité en cours de développement')); + if (!await networkInfo.isConnected) { + return Left(NetworkFailure('Pas de connexion Internet')); + } + + try { + final logs = await _retryPolicy.execute( + operation: () => remoteDatasource.getAuditLogs( + organizationId: organizationId, + startDate: startDate, + endDate: endDate, + operation: operation?.name, + entityType: entityType?.name, + severity: severity?.name, + limit: limit ?? 100, + ), + shouldRetry: (error) => _isRetryableError(error), + ); + + // Convert raw maps to FinancialAuditLog entities + final auditLogs = logs + .map((log) => FinancialAuditLog.fromJson(log)) + .toList(); + return Right(auditLogs); + } on UnauthorizedException { + return Left(UnauthorizedFailure('Session expirée')); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnexpectedFailure('Erreur inattendue: $e')); + } } @override @@ -386,8 +508,32 @@ class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository { DateTime? startDate, DateTime? endDate, }) async { - return Left( - NotImplementedFailure('Fonctionnalité en cours de développement')); + if (!await networkInfo.isConnected) { + return Left(NetworkFailure('Pas de connexion Internet')); + } + + try { + final anomalies = await _retryPolicy.execute( + operation: () => remoteDatasource.getAnomalies( + organizationId: organizationId, + startDate: startDate, + endDate: endDate, + ), + shouldRetry: (error) => _isRetryableError(error), + ); + + // Convert raw maps to FinancialAuditLog entities + final auditLogs = anomalies + .map((log) => FinancialAuditLog.fromJson(log)) + .toList(); + return Right(auditLogs); + } on UnauthorizedException { + return Left(UnauthorizedFailure('Session expirée')); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnexpectedFailure('Erreur inattendue: $e')); + } } @override @@ -397,8 +543,34 @@ class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository { DateTime? endDate, String format = 'csv', }) async { - return Left( - NotImplementedFailure('Fonctionnalité en cours de développement')); + if (!await networkInfo.isConnected) { + return Left(NetworkFailure('Pas de connexion Internet')); + } + + try { + final result = await _retryPolicy.execute( + operation: () => remoteDatasource.exportAuditLogs( + organizationId: organizationId, + startDate: startDate, + endDate: endDate, + format: format, + ), + shouldRetry: (error) => _isRetryableError(error), + ); + + // Extract export URL from response + final exportUrl = result['exportUrl'] as String?; + if (exportUrl == null) { + return Left(ServerFailure('URL d\'export non disponible')); + } + return Right(exportUrl); + } on UnauthorizedException { + return Left(UnauthorizedFailure('Session expirée')); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnexpectedFailure('Erreur inattendue: $e')); + } } @override @@ -407,7 +579,26 @@ class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository { DateTime? startDate, DateTime? endDate, }) async { - return Left( - NotImplementedFailure('Fonctionnalité en cours de développement')); + if (!await networkInfo.isConnected) { + return Left(NetworkFailure('Pas de connexion Internet')); + } + + try { + final stats = await _retryPolicy.execute( + operation: () => remoteDatasource.getWorkflowStats( + organizationId: organizationId, + startDate: startDate, + endDate: endDate, + ), + shouldRetry: (error) => _isRetryableError(error), + ); + return Right(stats); + } on UnauthorizedException { + return Left(UnauthorizedFailure('Session expirée')); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnexpectedFailure('Erreur inattendue: $e')); + } } }