chore(import): improve error handling and add flyer integration
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Failing after 3m41s
Test Suite / flutter-quality (push) Successful in 2m3s

- Replace BadRequestException with UnauthorizedException for authentication failures in flyer-import and flyer-selection controllers
- Add bulk selection endpoint in FlyerSelectionController for creating multiple selections in one request
- Update FlyerSelectionModule to include new FlyerSelectionMatcherService and FlyerSelectionSyncController
- Extend FlyerSelectionService with createMany method for bulk operations
- Add new DTOs for bulk selection and receipt matching functionality
- Update ReceiptImportService to accept FlyerSelectionService dependency and track successful rows
- Extend SaveReceiptResponse with flyerAutoSync field for receipt-to-flyer matching results
- Add new API paths for flyer import and selection endpoints
- Update Flutter UI to include Flyer import tab and adjust tab controller length
- Add new domain models and repository methods for flyer import functionality
- Update test files to include new FlyerSelectionService dependency
- Modify .kilo plan documentation to reflect current system architecture
This commit is contained in:
Nils-Johan Gynther
2026-05-18 22:51:27 +02:00
parent 24a96c3da1
commit d5f903db98
26 changed files with 1359 additions and 247 deletions
@@ -1,7 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'import_repository.dart';
import 'import_repository.dart';
final importRepositoryProvider = Provider<ImportRepository>(
(_) => ImportRepository(),
);
final importRepositoryProvider = Provider<ImportRepository>(
(_) => ImportRepository(),
);
@@ -5,10 +5,11 @@ import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'dart:developer' as developer;
import '../../../core/api/api_paths.dart';
import '../../../core/api/api_exception.dart';
import '../domain/help_text_content.dart';
import '../domain/quick_import_result.dart';
import '../../../core/api/api_paths.dart';
import '../../../core/api/api_exception.dart';
import '../domain/flyer_import_result.dart';
import '../domain/help_text_content.dart';
import '../domain/quick_import_result.dart';
/// Handles communication with the quick-import API endpoint.
///
@@ -60,7 +61,7 @@ class ImportRepository {
/// Upload a receipt file for parsing (Fas 6b).
/// Returns a list of parsed receipt items.
Future<List<ParsedReceiptItem>> importReceiptFile({
Future<List<ParsedReceiptItem>> importReceiptFile({
required Uint8List bytes,
required String filename,
String? token,
@@ -142,7 +143,83 @@ class ImportRepository {
developer.log('Exception during receipt import: $e', name: 'ImportRepository', error: e);
rethrow;
}
}
}
Future<FlyerImportResult> importFlyerFile({
required Uint8List bytes,
required String filename,
String? token,
}) async {
final uri = Uri.parse('$_baseUrl${FlyerImportApiPaths.parse}');
final request = http.MultipartRequest('POST', uri);
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
request.files.add(
http.MultipartFile.fromBytes('file', bytes, filename: filename),
);
final streamed = await _client.send(request).timeout(
const Duration(seconds: 120),
onTimeout: () {
throw ApiException(
type: ApiErrorType.network,
message: 'Flyerimporten tog för lång tid. Försök igen.',
);
},
);
final response = await http.Response.fromStream(streamed);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw ApiException(
type: _mapStatusCodeToErrorType(response.statusCode),
message: 'Fel vid flyerimport: ${response.body}',
statusCode: response.statusCode,
);
}
final parsed = _parseResponse(response);
if (parsed is! Map<String, dynamic>) {
throw ApiException(
type: ApiErrorType.unknown,
message: 'Felaktigt svar från flyerimport.',
);
}
return FlyerImportResult.fromJson(parsed);
}
Future<List<Map<String, dynamic>>> createFlyerSelectionsBulk({
required int sessionId,
required List<Map<String, dynamic>> items,
String? token,
}) async {
final uri = Uri.parse('$_baseUrl${FlyerSelectionApiPaths.bulkBySession(sessionId)}');
final response = await _client.post(
uri,
headers: {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
},
body: jsonEncode({
'items': items,
}),
);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw ApiException(
type: _mapStatusCodeToErrorType(response.statusCode),
message: 'Kunde inte skapa flyer-selections: ${response.body}',
statusCode: response.statusCode,
);
}
final parsed = _parseResponse(response);
if (parsed is! List) return const [];
return parsed.cast<Map<String, dynamic>>();
}
/// Upload a file (PDF or image) for recipe extraction.
///
@@ -335,7 +412,7 @@ class ImportRepository {
);
}
final result = _parseResponse(response) as Map<String, dynamic>;
final result = _parseResponse(response) as Map<String, dynamic>;
developer.log('saveReceipt succeeded: ${result['created']} created, ${result['merged']} merged', name: 'ImportRepository');
return result;
} catch (e) {