Files
velocity-pay-flutter/lib/screens/groups/controllers/batch_controller.dart
2026-06-11 23:48:26 +02:00

503 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:qpay/http/http.dart';
import 'package:qpay/models/api_response.dart';
import 'package:qpay/screens/groups/models/group_batch_model.dart';
import 'package:qpay/models/pageable_model.dart';
import 'package:qpay/screens/home/home_controller.dart';
import 'package:qpay/screens/pay/pay_controller.dart';
import 'package:qpay/screens/transactions/transaction_model.dart';
class BatchScreenModel {
List<GroupBatch> batches = [];
List<GroupBatchItem> batchItems = [];
GroupBatch? currentBatch;
List<BillProvider> providers = [];
List<PaymentProcessor> processors = [];
BillProvider? selectedProvider;
PaymentProcessor? selectedProcessor;
bool isLoading = false;
bool isLoadingMore = false;
bool isActing = false;
String? errorMessage;
int currentPage = 0;
int totalPages = 0;
int totalElements = 0;
String searchQuery = '';
String? statusFilter;
String? currencyFilter;
bool selectMode = false;
Set<String> selectedBatchIds = {};
}
class BatchController extends ChangeNotifier {
final BatchScreenModel model = BatchScreenModel();
final Http http = Http();
BuildContext context;
bool _disposed = false;
BatchController(this.context);
@override
void dispose() {
_disposed = true;
super.dispose();
}
dynamic _unwrap(dynamic response) {
if (response is Map && response.containsKey('body')) {
return response['body'];
}
return response;
}
String buildQueryParameters(Map<String, String> params) {
if (params.isEmpty) {
return '';
}
String query = '';
params.forEach((key, value) {
query += '$key=$value&';
});
return query.substring(0, query.length - 1);
}
Future<ApiResponse<List<BillProvider>>> loadProviders() async {
try {
final raw = await http.get(
'/public/providers?currency=USD&sort=priority,asc',
);
final response = _unwrap(raw);
final List<dynamic> content;
if (response is Map && response.containsKey('content')) {
content = response['content'] as List<dynamic>;
} else if (response is List) {
content = response;
} else {
content = [];
}
model.providers = content
.map((e) => BillProvider.fromJson(e as Map<String, dynamic>))
.toList();
if (model.providers.isNotEmpty) {
model.selectedProvider = model.providers.first;
}
if (!_disposed) notifyListeners();
return ApiResponse.success(model.providers);
} catch (e) {
logger.e(e);
return ApiResponse.failure('Failed to load providers');
}
}
Future<ApiResponse<List<PaymentProcessor>>> loadProcessors() async {
try {
final raw = await http.get('/public/payment-processors');
final response = _unwrap(raw);
final List<dynamic> content = response is List
? response
: (response['content'] ?? []);
model.processors = content
.map((e) => PaymentProcessor.fromJson(e as Map<String, dynamic>))
.toList();
if (model.processors.isNotEmpty) {
model.selectedProcessor = model.processors.first;
}
if (!_disposed) notifyListeners();
return ApiResponse.success(model.processors);
} catch (e) {
logger.e(e);
return ApiResponse.failure('Failed to load processors');
}
}
Future<ApiResponse<List<GroupBatch>>> listBatches(
String groupId, {
int page = 0,
int size = 20,
String? search,
String? status,
String? currency,
}) async {
if (page == 0) {
model.isLoading = true;
model.batches = [];
model.currentPage = 0;
} else {
model.isLoadingMore = true;
}
if (!_disposed) notifyListeners();
try {
final params = <String, String>{
'recipientGroupId': groupId,
'page': page.toString(),
'size': size.toString(),
'sort': 'createdAt,desc',
};
if (search != null && search.isNotEmpty) {
params['description'] = search;
}
if (status != null && status.isNotEmpty) {
params['status'] = status;
}
if (currency != null && currency.isNotEmpty) {
params['currency'] = currency;
}
final raw = await http.get(
'/public/group-batches?${buildQueryParameters(params)}',
);
final response = _unwrap(raw);
final pageableModel = PageableModel.fromJson(
response as Map<String, dynamic>,
);
final newBatches = pageableModel.content
.map((e) => GroupBatch.fromJson(e as Map<String, dynamic>))
.toList();
if (page == 0) {
model.batches = newBatches;
} else {
model.batches.addAll(newBatches);
}
model.currentPage = pageableModel.number;
model.totalPages = pageableModel.totalPages;
model.totalElements = pageableModel.totalElements;
model.isLoading = false;
model.isLoadingMore = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(model.batches);
} catch (e) {
logger.e(e);
model.errorMessage = e.toString();
if (page == 0) {
model.batches = [];
}
model.isLoading = false;
model.isLoadingMore = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to load batches');
}
}
Future<void> searchBatches(
String groupId, {
String? search,
String? status,
String? currency,
}) async {
model.searchQuery = search ?? '';
model.statusFilter = status;
model.currencyFilter = currency;
await listBatches(
groupId,
page: 0,
search: search,
status: status,
currency: currency,
);
}
Future<void> loadNextPage(String groupId) async {
if (model.isLoadingMore || model.currentPage + 1 >= model.totalPages) {
return;
}
await listBatches(
groupId,
page: model.currentPage + 1,
search: model.searchQuery.isNotEmpty ? model.searchQuery : null,
status: model.statusFilter,
currency: model.currencyFilter,
);
}
Future<ApiResponse<List<GroupBatchItem>>> listBatchItems(
String batchId,
) async {
model.isLoading = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.get(
'/public/group-batches/items?groupBatchId=$batchId&sort=createdAt,desc',
);
final response = _unwrap(raw);
final List<dynamic> content;
if (response is Map && response.containsKey('content')) {
content = PageableModel.fromJson(
response as Map<String, dynamic>,
).content;
} else if (response is List) {
content = response;
} else {
content = [];
}
model.batchItems = content
.map((e) => GroupBatchItem.fromJson(e as Map<String, dynamic>))
.toList();
model.isLoading = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(model.batchItems);
} catch (e) {
logger.e(e);
model.errorMessage = e.toString();
model.batchItems = [];
model.isLoading = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to load batch items');
}
}
Future<ApiResponse<GroupBatch>> getBatch(
String batchId,
String userId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.get(
'/public/group-batches/$batchId?userId=$userId',
);
final response = _unwrap(raw);
final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
model.currentBatch = batch;
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(batch);
} catch (e) {
logger.e(e);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to load batch');
}
}
Future<ApiResponse<GroupBatch>> createBatch(CreateBatchRequest req) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.post('/public/group-batches', req.toJson());
final response = _unwrap(raw);
final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
model.currentBatch = batch;
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(batch);
} catch (e, s) {
logger.e(e);
logger.e(s);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to create batch');
}
}
Future<ApiResponse<GroupBatch>> updateBatch(
GroupBatchUpdateRequest request,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.put(
'/public/group-batches/${request.id}',
request.toJson(),
);
final response = _unwrap(raw);
final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
// Update the batch in the local list if present
final index = model.batches.indexWhere((b) => b.id == batch.id);
if (index != -1) {
model.batches[index] = batch;
}
model.currentBatch = batch;
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(batch);
} catch (e, s) {
logger.e(e);
logger.e(s);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to update batch');
}
}
Future<ApiResponse<GroupBatch>> confirmBatch(
String batchId,
String userId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final key =
'batch-$batchId-confirm-${DateTime.now().millisecondsSinceEpoch}';
final body = BatchActionRequest(userId: userId, idempotencyKey: key);
final raw = await http.post(
'/public/group-batches/$batchId/confirm',
body.toJson(),
);
final response = _unwrap(raw);
final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
model.currentBatch = batch;
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(batch);
} catch (e) {
logger.e(e);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to confirm batch');
}
}
Future<ApiResponse<TransactionModel>> requestBatch(
String batchId,
String userId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final key =
'batch-$batchId-request-${DateTime.now().millisecondsSinceEpoch}';
final body = BatchActionRequest(userId: userId, idempotencyKey: key);
final raw = await http.post(
'/public/group-batches/$batchId/request',
body.toJson(),
);
final response = _unwrap(raw);
final txn = TransactionModel.fromJson(response as Map<String, dynamic>);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(txn);
} catch (e, s) {
logger.e(e);
logger.e(s);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to request batch payment');
}
}
Future<ApiResponse<Map<String, dynamic>>> downloadBatchReport(
String batchId,
String userId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.get(
'/public/group-batches/$batchId/report?userId=$userId',
);
final response = _unwrap(raw);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(response as Map<String, dynamic>);
} catch (e, s) {
logger.e(e);
logger.e(s);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to download batch report');
}
}
Future<ApiResponse<TransactionModel>> pollBatch(
String batchId,
String userId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final key =
'batch-$batchId-poll-${DateTime.now().millisecondsSinceEpoch}';
final body = BatchActionRequest(userId: userId, idempotencyKey: key);
final raw = await http.post(
'/public/group-batches/$batchId/poll',
body.toJson(),
);
final response = _unwrap(raw);
final batchTransaction = TransactionModel.fromJson(
response as Map<String, dynamic>,
);
model.isActing = false;
if (!_disposed) notifyListeners();
if (response['status'] != 'FAILED') {
return ApiResponse.success(batchTransaction);
} else {
return ApiResponse.failure(
response['errorMessage'] ?? "Transaction failed",
);
}
} catch (e, s) {
logger.e(e);
logger.e(s);
model.isActing = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to poll batch');
}
}
Future<void> deleteBatch({
required String batchId,
required String userId,
}) async {
try {
await http.delete('/public/group-batches/$batchId?userId=$userId');
model.batches.removeWhere((b) => b.id == batchId);
model.selectedBatchIds.remove(batchId);
model.currentBatch = null;
if (!_disposed) notifyListeners();
} catch (e) {
logger.e(e);
model.errorMessage = e.toString();
if (!_disposed) notifyListeners();
}
}
Future<void> deleteSelectedBatches({
required String groupId,
required String userId,
}) async {
final idsToDelete = List<String>.from(model.selectedBatchIds);
model.selectMode = false;
model.selectedBatchIds.clear();
if (!_disposed) notifyListeners();
for (final id in idsToDelete) {
await deleteBatch(batchId: id, userId: userId);
}
// Refresh after deletion
await listBatches(groupId, page: 0);
}
void selectProvider(BillProvider provider) {
model.selectedProvider = provider;
if (!_disposed) notifyListeners();
}
void selectProcessor(PaymentProcessor processor) {
model.selectedProcessor = processor;
if (!_disposed) notifyListeners();
}
void toggleSelectMode() {
model.selectMode = !model.selectMode;
if (!model.selectMode) {
model.selectedBatchIds.clear();
}
if (!_disposed) notifyListeners();
}
void toggleBatchSelection(String batchId) {
if (model.selectedBatchIds.contains(batchId)) {
model.selectedBatchIds.remove(batchId);
} else {
model.selectedBatchIds.add(batchId);
}
if (!_disposed) notifyListeners();
}
}