Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
/// Datasource distant pour le workflow financier (API)
|
||||
library finance_workflow_remote_datasource;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/config/environment.dart';
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
import '../models/transaction_approval_model.dart';
|
||||
import '../models/budget_model.dart';
|
||||
import '../../domain/entities/transaction_approval.dart';
|
||||
import '../../domain/entities/budget.dart';
|
||||
|
||||
@lazySingleton
|
||||
class FinanceWorkflowRemoteDatasource {
|
||||
final http.Client client;
|
||||
final FlutterSecureStorage secureStorage;
|
||||
|
||||
FinanceWorkflowRemoteDatasource({
|
||||
required this.client,
|
||||
required this.secureStorage,
|
||||
});
|
||||
|
||||
/// Headers HTTP avec authentification
|
||||
Future<Map<String, String>> _getHeaders() async {
|
||||
final token = await secureStorage.read(key: 'access_token');
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
if (token != null) 'Authorization': 'Bearer $token',
|
||||
};
|
||||
}
|
||||
|
||||
// === APPROBATIONS ===
|
||||
|
||||
Future<List<TransactionApprovalModel>> getPendingApprovals({
|
||||
String? organizationId,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/approvals/pending')
|
||||
.replace(queryParameters: {
|
||||
if (organizationId != null) 'organizationId': organizationId,
|
||||
});
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> 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 des approbations');
|
||||
}
|
||||
}
|
||||
|
||||
Future<TransactionApprovalModel> getApprovalById(String approvalId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/finance/approvals/$approvalId');
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return TransactionApprovalModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 404) {
|
||||
throw NotFoundException('Approbation non trouvée');
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération de l\'approbation');
|
||||
}
|
||||
}
|
||||
|
||||
Future<TransactionApprovalModel> approveTransaction({
|
||||
required String approvalId,
|
||||
String? comment,
|
||||
}) async {
|
||||
final uri =
|
||||
Uri.parse('${AppConfig.apiBaseUrl}/api/finance/approvals/$approvalId/approve');
|
||||
|
||||
final body = json.encode({
|
||||
if (comment != null) 'comment': comment,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return TransactionApprovalModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 403) {
|
||||
throw ForbiddenException('Permission insuffisante pour approuver');
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'approbation');
|
||||
}
|
||||
}
|
||||
|
||||
Future<TransactionApprovalModel> rejectTransaction({
|
||||
required String approvalId,
|
||||
required String reason,
|
||||
}) async {
|
||||
final uri =
|
||||
Uri.parse('${AppConfig.apiBaseUrl}/api/finance/approvals/$approvalId/reject');
|
||||
|
||||
final body = json.encode({
|
||||
'reason': reason,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return TransactionApprovalModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 403) {
|
||||
throw ForbiddenException('Permission insuffisante pour rejeter');
|
||||
} else {
|
||||
throw ServerException('Erreur lors du rejet');
|
||||
}
|
||||
}
|
||||
|
||||
// === BUDGETS ===
|
||||
|
||||
Future<List<BudgetModel>> getBudgets({
|
||||
String? organizationId,
|
||||
String? status,
|
||||
int? year,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/budgets')
|
||||
.replace(queryParameters: {
|
||||
if (organizationId != null) 'organizationId': organizationId,
|
||||
if (status != null) 'status': status,
|
||||
if (year != null) 'year': year.toString(),
|
||||
});
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> jsonList = json.decode(response.body);
|
||||
return jsonList.map((json) => BudgetModel.fromJson(json)).toList();
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération des budgets');
|
||||
}
|
||||
}
|
||||
|
||||
Future<BudgetModel> getBudgetById(String budgetId) async {
|
||||
final uri =
|
||||
Uri.parse('${AppConfig.apiBaseUrl}/api/finance/budgets/$budgetId');
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
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 {
|
||||
throw ServerException('Erreur lors de la récupération du budget');
|
||||
}
|
||||
}
|
||||
|
||||
Future<BudgetModel> createBudget({
|
||||
required String name,
|
||||
String? description,
|
||||
required String organizationId,
|
||||
required String period,
|
||||
required int year,
|
||||
int? month,
|
||||
required List<Map<String, dynamic>> lines,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/finance/budgets');
|
||||
|
||||
final body = json.encode({
|
||||
'name': name,
|
||||
if (description != null) 'description': description,
|
||||
'organizationId': organizationId,
|
||||
'period': period,
|
||||
'year': year,
|
||||
if (month != null) 'month': month,
|
||||
'lines': lines,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return BudgetModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 403) {
|
||||
throw ForbiddenException('Permission insuffisante pour créer un budget');
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la création du budget');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getBudgetTracking({
|
||||
required String budgetId,
|
||||
}) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/finance/budgets/$budgetId/tracking');
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body) as Map<String, dynamic>;
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération du suivi budgétaire');
|
||||
}
|
||||
}
|
||||
}
|
||||
100
lib/features/finance_workflow/data/models/budget_model.dart
Normal file
100
lib/features/finance_workflow/data/models/budget_model.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
/// Model de données Budget avec sérialisation JSON
|
||||
library budget_model;
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../domain/entities/budget.dart';
|
||||
|
||||
part 'budget_model.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class BudgetModel extends Budget {
|
||||
@JsonKey(
|
||||
fromJson: _linesFromJson,
|
||||
toJson: _linesToJson,
|
||||
)
|
||||
@override
|
||||
final List<BudgetLine> lines;
|
||||
|
||||
const BudgetModel({
|
||||
required super.id,
|
||||
required super.name,
|
||||
super.description,
|
||||
required super.organizationId,
|
||||
required super.period,
|
||||
required super.year,
|
||||
super.month,
|
||||
required super.status,
|
||||
this.lines = const [],
|
||||
required super.totalPlanned,
|
||||
super.totalRealized,
|
||||
super.currency,
|
||||
required super.createdBy,
|
||||
required super.createdAt,
|
||||
super.approvedAt,
|
||||
super.approvedBy,
|
||||
required super.startDate,
|
||||
required super.endDate,
|
||||
super.metadata,
|
||||
}) : super(lines: lines);
|
||||
|
||||
static List<BudgetLine> _linesFromJson(List<dynamic>? json) =>
|
||||
json?.map((e) => BudgetLineModel.fromJson(e as Map<String, dynamic>)).toList() ?? [];
|
||||
|
||||
static List<Map<String, dynamic>> _linesToJson(List<BudgetLine>? lines) =>
|
||||
lines?.map((l) => BudgetLineModel(
|
||||
id: l.id,
|
||||
category: l.category,
|
||||
name: l.name,
|
||||
description: l.description,
|
||||
amountPlanned: l.amountPlanned,
|
||||
amountRealized: l.amountRealized,
|
||||
notes: l.notes,
|
||||
).toJson()).toList() ?? [];
|
||||
|
||||
factory BudgetModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$BudgetModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BudgetModelToJson(this);
|
||||
|
||||
factory BudgetModel.fromEntity(Budget entity) {
|
||||
return BudgetModel(
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
description: entity.description,
|
||||
organizationId: entity.organizationId,
|
||||
period: entity.period,
|
||||
year: entity.year,
|
||||
month: entity.month,
|
||||
status: entity.status,
|
||||
lines: entity.lines,
|
||||
totalPlanned: entity.totalPlanned,
|
||||
totalRealized: entity.totalRealized,
|
||||
currency: entity.currency,
|
||||
createdBy: entity.createdBy,
|
||||
createdAt: entity.createdAt,
|
||||
approvedAt: entity.approvedAt,
|
||||
approvedBy: entity.approvedBy,
|
||||
startDate: entity.startDate,
|
||||
endDate: entity.endDate,
|
||||
metadata: entity.metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class BudgetLineModel extends BudgetLine {
|
||||
const BudgetLineModel({
|
||||
required super.id,
|
||||
required super.category,
|
||||
required super.name,
|
||||
super.description,
|
||||
required super.amountPlanned,
|
||||
super.amountRealized,
|
||||
super.notes,
|
||||
});
|
||||
|
||||
factory BudgetLineModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$BudgetLineModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BudgetLineModelToJson(this);
|
||||
}
|
||||
102
lib/features/finance_workflow/data/models/budget_model.g.dart
Normal file
102
lib/features/finance_workflow/data/models/budget_model.g.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'budget_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
BudgetModel _$BudgetModelFromJson(Map<String, dynamic> json) => BudgetModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
organizationId: json['organizationId'] as String,
|
||||
period: $enumDecode(_$BudgetPeriodEnumMap, json['period']),
|
||||
year: (json['year'] as num).toInt(),
|
||||
month: (json['month'] as num?)?.toInt(),
|
||||
status: $enumDecode(_$BudgetStatusEnumMap, json['status']),
|
||||
lines: json['lines'] == null
|
||||
? const []
|
||||
: BudgetModel._linesFromJson(json['lines'] as List?),
|
||||
totalPlanned: (json['totalPlanned'] as num).toDouble(),
|
||||
totalRealized: (json['totalRealized'] as num?)?.toDouble() ?? 0,
|
||||
currency: json['currency'] as String? ?? 'XOF',
|
||||
createdBy: json['createdBy'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
approvedAt: json['approvedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['approvedAt'] as String),
|
||||
approvedBy: json['approvedBy'] as String?,
|
||||
startDate: DateTime.parse(json['startDate'] as String),
|
||||
endDate: DateTime.parse(json['endDate'] as String),
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BudgetModelToJson(BudgetModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'organizationId': instance.organizationId,
|
||||
'period': _$BudgetPeriodEnumMap[instance.period]!,
|
||||
'year': instance.year,
|
||||
'month': instance.month,
|
||||
'status': _$BudgetStatusEnumMap[instance.status]!,
|
||||
'totalPlanned': instance.totalPlanned,
|
||||
'totalRealized': instance.totalRealized,
|
||||
'currency': instance.currency,
|
||||
'createdBy': instance.createdBy,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'approvedAt': instance.approvedAt?.toIso8601String(),
|
||||
'approvedBy': instance.approvedBy,
|
||||
'startDate': instance.startDate.toIso8601String(),
|
||||
'endDate': instance.endDate.toIso8601String(),
|
||||
'metadata': instance.metadata,
|
||||
'lines': BudgetModel._linesToJson(instance.lines),
|
||||
};
|
||||
|
||||
const _$BudgetPeriodEnumMap = {
|
||||
BudgetPeriod.monthly: 'monthly',
|
||||
BudgetPeriod.quarterly: 'quarterly',
|
||||
BudgetPeriod.semiannual: 'semiannual',
|
||||
BudgetPeriod.annual: 'annual',
|
||||
};
|
||||
|
||||
const _$BudgetStatusEnumMap = {
|
||||
BudgetStatus.draft: 'draft',
|
||||
BudgetStatus.active: 'active',
|
||||
BudgetStatus.closed: 'closed',
|
||||
BudgetStatus.cancelled: 'cancelled',
|
||||
};
|
||||
|
||||
BudgetLineModel _$BudgetLineModelFromJson(Map<String, dynamic> json) =>
|
||||
BudgetLineModel(
|
||||
id: json['id'] as String,
|
||||
category: $enumDecode(_$BudgetCategoryEnumMap, json['category']),
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
amountPlanned: (json['amountPlanned'] as num).toDouble(),
|
||||
amountRealized: (json['amountRealized'] as num?)?.toDouble() ?? 0,
|
||||
notes: json['notes'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BudgetLineModelToJson(BudgetLineModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'category': _$BudgetCategoryEnumMap[instance.category]!,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'amountPlanned': instance.amountPlanned,
|
||||
'amountRealized': instance.amountRealized,
|
||||
'notes': instance.notes,
|
||||
};
|
||||
|
||||
const _$BudgetCategoryEnumMap = {
|
||||
BudgetCategory.contributions: 'contributions',
|
||||
BudgetCategory.savings: 'savings',
|
||||
BudgetCategory.solidarity: 'solidarity',
|
||||
BudgetCategory.events: 'events',
|
||||
BudgetCategory.operational: 'operational',
|
||||
BudgetCategory.investments: 'investments',
|
||||
BudgetCategory.other: 'other',
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/// Model de données TransactionApproval avec sérialisation JSON
|
||||
library transaction_approval_model;
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../domain/entities/transaction_approval.dart';
|
||||
|
||||
part 'transaction_approval_model.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class TransactionApprovalModel extends TransactionApproval {
|
||||
@JsonKey(
|
||||
fromJson: _approversFromJson,
|
||||
toJson: _approversToJson,
|
||||
)
|
||||
@override
|
||||
final List<ApproverAction> approvers;
|
||||
|
||||
const TransactionApprovalModel({
|
||||
required super.id,
|
||||
required super.transactionId,
|
||||
required super.transactionType,
|
||||
required super.amount,
|
||||
super.currency,
|
||||
required super.requesterId,
|
||||
required super.requesterName,
|
||||
super.organizationId,
|
||||
required super.requiredLevel,
|
||||
required super.status,
|
||||
this.approvers = const [],
|
||||
super.rejectionReason,
|
||||
required super.createdAt,
|
||||
super.expiresAt,
|
||||
super.completedAt,
|
||||
super.metadata,
|
||||
}) : super(approvers: approvers);
|
||||
|
||||
static List<ApproverAction> _approversFromJson(List<dynamic>? json) =>
|
||||
json?.map((e) => ApproverActionModel.fromJson(e as Map<String, dynamic>)).toList() ?? [];
|
||||
|
||||
static List<Map<String, dynamic>> _approversToJson(List<ApproverAction>? approvers) =>
|
||||
approvers?.map((a) => ApproverActionModel(
|
||||
approverId: a.approverId,
|
||||
approverName: a.approverName,
|
||||
approverRole: a.approverRole,
|
||||
decision: a.decision,
|
||||
comment: a.comment,
|
||||
decidedAt: a.decidedAt,
|
||||
).toJson()).toList() ?? [];
|
||||
|
||||
factory TransactionApprovalModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$TransactionApprovalModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$TransactionApprovalModelToJson(this);
|
||||
|
||||
factory TransactionApprovalModel.fromEntity(TransactionApproval entity) {
|
||||
return TransactionApprovalModel(
|
||||
id: entity.id,
|
||||
transactionId: entity.transactionId,
|
||||
transactionType: entity.transactionType,
|
||||
amount: entity.amount,
|
||||
currency: entity.currency,
|
||||
requesterId: entity.requesterId,
|
||||
requesterName: entity.requesterName,
|
||||
organizationId: entity.organizationId,
|
||||
requiredLevel: entity.requiredLevel,
|
||||
status: entity.status,
|
||||
approvers: entity.approvers,
|
||||
rejectionReason: entity.rejectionReason,
|
||||
createdAt: entity.createdAt,
|
||||
expiresAt: entity.expiresAt,
|
||||
completedAt: entity.completedAt,
|
||||
metadata: entity.metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ApproverActionModel extends ApproverAction {
|
||||
const ApproverActionModel({
|
||||
required super.approverId,
|
||||
required super.approverName,
|
||||
required super.approverRole,
|
||||
required super.decision,
|
||||
super.comment,
|
||||
super.decidedAt,
|
||||
});
|
||||
|
||||
factory ApproverActionModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ApproverActionModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ApproverActionModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'transaction_approval_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
TransactionApprovalModel _$TransactionApprovalModelFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
TransactionApprovalModel(
|
||||
id: json['id'] as String,
|
||||
transactionId: json['transactionId'] as String,
|
||||
transactionType:
|
||||
$enumDecode(_$TransactionTypeEnumMap, json['transactionType']),
|
||||
amount: (json['amount'] as num).toDouble(),
|
||||
currency: json['currency'] as String? ?? 'XOF',
|
||||
requesterId: json['requesterId'] as String,
|
||||
requesterName: json['requesterName'] as String,
|
||||
organizationId: json['organizationId'] as String?,
|
||||
requiredLevel: $enumDecode(_$ApprovalLevelEnumMap, json['requiredLevel']),
|
||||
status: $enumDecode(_$ApprovalStatusEnumMap, json['status']),
|
||||
approvers: json['approvers'] == null
|
||||
? const []
|
||||
: TransactionApprovalModel._approversFromJson(
|
||||
json['approvers'] as List?),
|
||||
rejectionReason: json['rejectionReason'] as String?,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
expiresAt: json['expiresAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expiresAt'] as String),
|
||||
completedAt: json['completedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['completedAt'] as String),
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TransactionApprovalModelToJson(
|
||||
TransactionApprovalModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'transactionId': instance.transactionId,
|
||||
'transactionType': _$TransactionTypeEnumMap[instance.transactionType]!,
|
||||
'amount': instance.amount,
|
||||
'currency': instance.currency,
|
||||
'requesterId': instance.requesterId,
|
||||
'requesterName': instance.requesterName,
|
||||
'organizationId': instance.organizationId,
|
||||
'requiredLevel': _$ApprovalLevelEnumMap[instance.requiredLevel]!,
|
||||
'status': _$ApprovalStatusEnumMap[instance.status]!,
|
||||
'rejectionReason': instance.rejectionReason,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'expiresAt': instance.expiresAt?.toIso8601String(),
|
||||
'completedAt': instance.completedAt?.toIso8601String(),
|
||||
'metadata': instance.metadata,
|
||||
'approvers':
|
||||
TransactionApprovalModel._approversToJson(instance.approvers),
|
||||
};
|
||||
|
||||
const _$TransactionTypeEnumMap = {
|
||||
TransactionType.contribution: 'contribution',
|
||||
TransactionType.deposit: 'deposit',
|
||||
TransactionType.withdrawal: 'withdrawal',
|
||||
TransactionType.transfer: 'transfer',
|
||||
TransactionType.solidarity: 'solidarity',
|
||||
TransactionType.event: 'event',
|
||||
TransactionType.other: 'other',
|
||||
};
|
||||
|
||||
const _$ApprovalLevelEnumMap = {
|
||||
ApprovalLevel.none: 'none',
|
||||
ApprovalLevel.level1: 'level1',
|
||||
ApprovalLevel.level2: 'level2',
|
||||
ApprovalLevel.level3: 'level3',
|
||||
};
|
||||
|
||||
const _$ApprovalStatusEnumMap = {
|
||||
ApprovalStatus.pending: 'pending',
|
||||
ApprovalStatus.approved: 'approved',
|
||||
ApprovalStatus.validated: 'validated',
|
||||
ApprovalStatus.rejected: 'rejected',
|
||||
ApprovalStatus.expired: 'expired',
|
||||
ApprovalStatus.cancelled: 'cancelled',
|
||||
};
|
||||
|
||||
ApproverActionModel _$ApproverActionModelFromJson(Map<String, dynamic> json) =>
|
||||
ApproverActionModel(
|
||||
approverId: json['approverId'] as String,
|
||||
approverName: json['approverName'] as String,
|
||||
approverRole: json['approverRole'] as String,
|
||||
decision: $enumDecode(_$ApprovalDecisionEnumMap, json['decision']),
|
||||
comment: json['comment'] as String?,
|
||||
decidedAt: json['decidedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['decidedAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ApproverActionModelToJson(
|
||||
ApproverActionModel instance) =>
|
||||
<String, dynamic>{
|
||||
'approverId': instance.approverId,
|
||||
'approverName': instance.approverName,
|
||||
'approverRole': instance.approverRole,
|
||||
'decision': _$ApprovalDecisionEnumMap[instance.decision]!,
|
||||
'comment': instance.comment,
|
||||
'decidedAt': instance.decidedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
const _$ApprovalDecisionEnumMap = {
|
||||
ApprovalDecision.pending: 'pending',
|
||||
ApprovalDecision.approved: 'approved',
|
||||
ApprovalDecision.rejected: 'rejected',
|
||||
};
|
||||
@@ -0,0 +1,413 @@
|
||||
/// Implémentation du repository de workflow financier
|
||||
library finance_workflow_repository_impl;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/network/network_info.dart';
|
||||
import '../../../../core/network/retry_policy.dart';
|
||||
import '../../../../core/network/offline_manager.dart';
|
||||
import '../../domain/entities/budget.dart';
|
||||
import '../../domain/entities/financial_audit_log.dart';
|
||||
import '../../domain/entities/transaction_approval.dart';
|
||||
import '../../domain/repositories/finance_workflow_repository.dart';
|
||||
import '../datasources/finance_workflow_remote_datasource.dart';
|
||||
|
||||
@LazySingleton(as: FinanceWorkflowRepository)
|
||||
class FinanceWorkflowRepositoryImpl implements FinanceWorkflowRepository {
|
||||
final FinanceWorkflowRemoteDatasource remoteDatasource;
|
||||
final NetworkInfo networkInfo;
|
||||
final OfflineManager offlineManager;
|
||||
final RetryPolicy _retryPolicy;
|
||||
|
||||
FinanceWorkflowRepositoryImpl({
|
||||
required this.remoteDatasource,
|
||||
required this.networkInfo,
|
||||
required this.offlineManager,
|
||||
}) : _retryPolicy = RetryPolicy(config: RetryConfig.standard);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<TransactionApproval>>> getPendingApprovals({
|
||||
String? organizationId,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final approvals = await _retryPolicy.execute(
|
||||
operation: () => remoteDatasource.getPendingApprovals(
|
||||
organizationId: organizationId,
|
||||
),
|
||||
shouldRetry: (error) => _isRetryableError(error),
|
||||
);
|
||||
return Right(approvals);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on TimeoutException {
|
||||
return Left(NetworkFailure('Délai d\'attente dépassé'));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, TransactionApproval>> getApprovalById(
|
||||
String approvalId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final approval = await _retryPolicy.execute(
|
||||
operation: () => remoteDatasource.getApprovalById(approvalId),
|
||||
shouldRetry: (error) => _isRetryableError(error),
|
||||
);
|
||||
return Right(approval);
|
||||
} on NotFoundException {
|
||||
return Left(NotFoundFailure('Approbation non trouvée'));
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on TimeoutException {
|
||||
return Left(NetworkFailure('Délai d\'attente dépassé'));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, TransactionApproval>> approveTransaction({
|
||||
required String approvalId,
|
||||
String? comment,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
// Queue for retry when back online
|
||||
await offlineManager.queueOperation(
|
||||
operationType: 'approveTransaction',
|
||||
endpoint: '/api/finance/approvals/$approvalId/approve',
|
||||
data: {'approvalId': approvalId, 'comment': comment},
|
||||
);
|
||||
return Left(NetworkFailure('Pas de connexion Internet. Opération mise en attente.'));
|
||||
}
|
||||
|
||||
try {
|
||||
final approval = await _retryPolicy.execute(
|
||||
operation: () => remoteDatasource.approveTransaction(
|
||||
approvalId: approvalId,
|
||||
comment: comment,
|
||||
),
|
||||
shouldRetry: (error) => _isRetryableError(error),
|
||||
);
|
||||
return Right(approval);
|
||||
} on ForbiddenException catch (e) {
|
||||
return Left(ForbiddenFailure(e.message));
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on TimeoutException {
|
||||
return Left(NetworkFailure('Délai d\'attente dépassé'));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, TransactionApproval>> rejectTransaction({
|
||||
required String approvalId,
|
||||
required String reason,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
// Queue for retry when back online
|
||||
await offlineManager.queueOperation(
|
||||
operationType: 'rejectTransaction',
|
||||
endpoint: '/api/finance/approvals/$approvalId/reject',
|
||||
data: {'approvalId': approvalId, 'reason': reason},
|
||||
);
|
||||
return Left(NetworkFailure('Pas de connexion Internet. Opération mise en attente.'));
|
||||
}
|
||||
|
||||
try {
|
||||
final approval = await _retryPolicy.execute(
|
||||
operation: () => remoteDatasource.rejectTransaction(
|
||||
approvalId: approvalId,
|
||||
reason: reason,
|
||||
),
|
||||
shouldRetry: (error) => _isRetryableError(error),
|
||||
);
|
||||
return Right(approval);
|
||||
} on ForbiddenException catch (e) {
|
||||
return Left(ForbiddenFailure(e.message));
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on TimeoutException {
|
||||
return Left(NetworkFailure('Délai d\'attente dépassé'));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Budget>>> getBudgets({
|
||||
String? organizationId,
|
||||
BudgetStatus? status,
|
||||
int? year,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final budgets = await _retryPolicy.execute(
|
||||
operation: () => remoteDatasource.getBudgets(
|
||||
organizationId: organizationId,
|
||||
status: status?.name,
|
||||
year: year,
|
||||
),
|
||||
shouldRetry: (error) => _isRetryableError(error),
|
||||
);
|
||||
return Right(budgets);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on TimeoutException {
|
||||
return Left(NetworkFailure('Délai d\'attente dépassé'));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Budget>> getBudgetById(String budgetId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final budget = await _retryPolicy.execute(
|
||||
operation: () => remoteDatasource.getBudgetById(budgetId),
|
||||
shouldRetry: (error) => _isRetryableError(error),
|
||||
);
|
||||
return Right(budget);
|
||||
} on NotFoundException {
|
||||
return Left(NotFoundFailure('Budget non trouvé'));
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on TimeoutException {
|
||||
return Left(NetworkFailure('Délai d\'attente dépassé'));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Budget>> createBudget({
|
||||
required String name,
|
||||
String? description,
|
||||
required String organizationId,
|
||||
required BudgetPeriod period,
|
||||
required int year,
|
||||
int? month,
|
||||
required List<BudgetLine> lines,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
// Queue for retry when back online
|
||||
await offlineManager.queueOperation(
|
||||
operationType: 'createBudget',
|
||||
endpoint: '/api/finance/budgets',
|
||||
data: {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'organizationId': organizationId,
|
||||
'period': period.name,
|
||||
'year': year,
|
||||
'month': month,
|
||||
'lines': lines.map((line) => {
|
||||
'id': line.id,
|
||||
'category': line.category.name,
|
||||
'name': line.name,
|
||||
'description': line.description,
|
||||
'amountPlanned': line.amountPlanned,
|
||||
'amountRealized': line.amountRealized,
|
||||
'notes': line.notes,
|
||||
}).toList(),
|
||||
},
|
||||
);
|
||||
return Left(NetworkFailure('Pas de connexion Internet. Opération mise en attente.'));
|
||||
}
|
||||
|
||||
try {
|
||||
final budget = await _retryPolicy.execute(
|
||||
operation: () => remoteDatasource.createBudget(
|
||||
name: name,
|
||||
description: description,
|
||||
organizationId: organizationId,
|
||||
period: period.name,
|
||||
year: year,
|
||||
month: month,
|
||||
lines: lines.map((line) => {
|
||||
'id': line.id,
|
||||
'category': line.category.name,
|
||||
'name': line.name,
|
||||
'description': line.description,
|
||||
'amountPlanned': line.amountPlanned,
|
||||
'amountRealized': line.amountRealized,
|
||||
'notes': line.notes,
|
||||
}).toList(),
|
||||
),
|
||||
shouldRetry: (error) => _isRetryableError(error),
|
||||
);
|
||||
return Right(budget);
|
||||
} on ForbiddenException catch (e) {
|
||||
return Left(ForbiddenFailure(e.message));
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on TimeoutException {
|
||||
return Left(NetworkFailure('Délai d\'attente dépassé'));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> getBudgetTracking({
|
||||
required String budgetId,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final tracking = await _retryPolicy.execute(
|
||||
operation: () => remoteDatasource.getBudgetTracking(budgetId: budgetId),
|
||||
shouldRetry: (error) => _isRetryableError(error),
|
||||
);
|
||||
return Right(tracking);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on TimeoutException {
|
||||
return Left(NetworkFailure('Délai d\'attente dépassé'));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if an error is retryable
|
||||
bool _isRetryableError(dynamic error) {
|
||||
// Server errors are retryable
|
||||
if (error is ServerException) return true;
|
||||
|
||||
// Timeout errors are retryable
|
||||
if (error is TimeoutException) return true;
|
||||
|
||||
// Client errors are not retryable
|
||||
if (error is UnauthorizedException) return false;
|
||||
if (error is ForbiddenException) return false;
|
||||
if (error is NotFoundException) return false;
|
||||
if (error is ValidationException) return false;
|
||||
|
||||
// Unknown errors - default to not retryable
|
||||
return false;
|
||||
}
|
||||
|
||||
// === MÉTHODES NON IMPLÉMENTÉES (Stubs) ===
|
||||
|
||||
@override
|
||||
Future<Either<Failure, TransactionApproval>> requestApproval({
|
||||
required String transactionId,
|
||||
required TransactionType transactionType,
|
||||
required double amount,
|
||||
}) async {
|
||||
return Left(
|
||||
NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<TransactionApproval>>> getApprovalsHistory({
|
||||
String? organizationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
ApprovalStatus? status,
|
||||
}) async {
|
||||
return Left(
|
||||
NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Budget>> updateBudget({
|
||||
required String budgetId,
|
||||
String? name,
|
||||
String? description,
|
||||
List<BudgetLine>? lines,
|
||||
BudgetStatus? status,
|
||||
}) async {
|
||||
return Left(
|
||||
NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> deleteBudget(String budgetId) async {
|
||||
return Left(
|
||||
NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<FinancialAuditLog>>> getAuditLogs({
|
||||
String? organizationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
AuditOperation? operation,
|
||||
AuditEntityType? entityType,
|
||||
AuditSeverity? severity,
|
||||
int? limit,
|
||||
}) async {
|
||||
return Left(
|
||||
NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<FinancialAuditLog>>> getAnomalies({
|
||||
String? organizationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) async {
|
||||
return Left(
|
||||
NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, String>> exportAuditLogs({
|
||||
required String organizationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
String format = 'csv',
|
||||
}) async {
|
||||
return Left(
|
||||
NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> getWorkflowStats({
|
||||
required String organizationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) async {
|
||||
return Left(
|
||||
NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user