From 4188cea7d97703885501d26087bfec23a913e9bd Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Thu, 30 Apr 2026 11:32:28 +0200 Subject: [PATCH] feat: enhance import functionality with detailed logging and error handling for receipt and URL imports Co-authored-by: Copilot --- .../import/data/import_repository.dart | 260 +++++++++++------- 1 file changed, 162 insertions(+), 98 deletions(-) diff --git a/flutter/lib/features/import/data/import_repository.dart b/flutter/lib/features/import/data/import_repository.dart index 61177542..904e12dd 100644 --- a/flutter/lib/features/import/data/import_repository.dart +++ b/flutter/lib/features/import/data/import_repository.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:http/http.dart' as http; +import 'dart:developer' as developer; import '../../../core/api/api_exception.dart'; import '../domain/quick_import_result.dart'; @@ -13,34 +14,6 @@ import '../domain/quick_import_result.dart'; /// • [importFile] — multipart upload (PDF / image bytes, max 10 MB). /// • [importUrl] — JSON body with `{ input: url }`. class ImportRepository { - /// Upload a receipt file for parsing (Fas 6b). - /// Returns a list of parsed receipt items. - Future> importReceiptFile({ - required Uint8List bytes, - required String filename, - String? token, - }) async { - final uri = Uri.parse('$_baseUrl/receipt-import'); - 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: 'Importen tog för lång tid. Försök igen.', - ), - ); - final response = await http.Response.fromStream(streamed); - final parsed = _parseResponse(response); - final items = (parsed['items'] as List?) ?? parsed as List?; - if (items == null) throw ApiException(type: ApiErrorType.unknown, message: 'Felaktigt svar från servern.'); - return items.map((e) => ParsedReceiptItem.fromJson(e as Map)).toList(); - } final http.Client _client; final String _baseUrl; @@ -51,6 +24,68 @@ class ImportRepository { defaultValue: '/api', ); + /// Upload a receipt file for parsing (Fas 6b). + /// Returns a list of parsed receipt items. + Future> importReceiptFile({ + required Uint8List bytes, + required String filename, + String? token, + }) async { + try { + developer.log('Starting receipt import for file: $filename', name: 'ImportRepository'); + + final uri = Uri.parse('$_baseUrl/receipt-import'); + final request = http.MultipartRequest('POST', uri); + + if (token != null) { + request.headers['Authorization'] = 'Bearer $token'; + } + + request.files.add( + http.MultipartFile.fromBytes('file', bytes, filename: filename), + ); + + developer.log('Sending request to: ${request.url}', name: 'ImportRepository'); + + final streamed = await _client.send(request).timeout( + const Duration(seconds: 120), + onTimeout: () { + developer.log('Request timed out', name: 'ImportRepository', error: 'Timeout'); + throw ApiException( + type: ApiErrorType.network, + message: 'Importen tog för lång tid. Försök igen.', + ); + }, + ); + + final response = await http.Response.fromStream(streamed); + developer.log('Received response with status: ${response.statusCode}', name: 'ImportRepository'); + + if (response.statusCode != 200) { + developer.log('Error response: ${response.body}', name: 'ImportRepository', error: 'HTTP Error'); + throw ApiException( + type: _mapStatusCodeToErrorType(response.statusCode), + message: 'Fel vid import: ${response.body}', + statusCode: response.statusCode, + ); + } + + final parsed = _parseResponse(response); + final items = (parsed['items'] as List?) ?? parsed as List?; + + if (items == null) { + developer.log('Invalid response format: ${response.body}', name: 'ImportRepository', error: 'Invalid Data'); + throw ApiException(type: ApiErrorType.unknown, message: 'Felaktigt svar från servern.'); + } + + developer.log('Successfully parsed ${items.length} items', name: 'ImportRepository'); + return items.map((e) => ParsedReceiptItem.fromJson(e as Map)).toList(); + } catch (e) { + developer.log('Exception during receipt import: $e', name: 'ImportRepository', error: e); + rethrow; + } + } + /// Upload a file (PDF or image) for recipe extraction. /// /// [bytes] — raw file bytes from file_picker. @@ -61,27 +96,51 @@ class ImportRepository { required String filename, String? token, }) async { - final uri = Uri.parse('$_baseUrl/quick-import'); - final request = http.MultipartRequest('POST', uri); - - if (token != null) { - request.headers['Authorization'] = 'Bearer $token'; + try { + developer.log('Starting file import for file: $filename', name: 'ImportRepository'); + + final uri = Uri.parse('$_baseUrl/quick-import'); + final request = http.MultipartRequest('POST', uri); + + if (token != null) { + request.headers['Authorization'] = 'Bearer $token'; + } + + request.files.add( + http.MultipartFile.fromBytes('file', bytes, filename: filename), + ); + + developer.log('Sending request to: ${request.url}', name: 'ImportRepository'); + + final streamed = await _client.send(request).timeout( + const Duration(seconds: 120), + onTimeout: () { + developer.log('Request timed out', name: 'ImportRepository', error: 'Timeout'); + throw ApiException( + type: ApiErrorType.network, + message: 'Importen tog för lång tid. Försök igen.', + ); + }, + ); + + final response = await http.Response.fromStream(streamed); + developer.log('Received response with status: ${response.statusCode}', name: 'ImportRepository'); + + if (response.statusCode != 200) { + developer.log('Error response: ${response.body}', name: 'ImportRepository', error: 'HTTP Error'); + throw ApiException( + type: _mapStatusCodeToErrorType(response.statusCode), + message: 'Fel vid import: ${response.body}', + statusCode: response.statusCode, + ); + } + + developer.log('Successfully imported file', name: 'ImportRepository'); + return QuickImportResult.fromJson(_parseResponse(response)); + } catch (e) { + developer.log('Exception during file import: $e', name: 'ImportRepository', error: e); + rethrow; } - - 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: 'Importen tog för lång tid. Försök igen.', - ), - ); - - final response = await http.Response.fromStream(streamed); - return QuickImportResult.fromJson(_parseResponse(response)); } /// Import a recipe from a URL. @@ -89,62 +148,67 @@ class ImportRepository { required String url, String? token, }) async { - final uri = Uri.parse('$_baseUrl/quick-import'); - final response = await _client - .post( - uri, - headers: { - 'Content-Type': 'application/json', - if (token != null) 'Authorization': 'Bearer $token', - }, - body: jsonEncode({'input': url}), - ) - .timeout( - const Duration(seconds: 120), - onTimeout: () => throw ApiException( - type: ApiErrorType.network, - message: 'Importen tog för lång tid. Försök igen.', - ), + try { + developer.log('Starting URL import for: $url', name: 'ImportRepository'); + + final uri = Uri.parse('$_baseUrl/quick-import'); + final response = await _client + .post( + uri, + headers: { + 'Content-Type': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }, + body: jsonEncode({'input': url}), + ) + .timeout( + const Duration(seconds: 120), + onTimeout: () { + developer.log('Request timed out', name: 'ImportRepository', error: 'Timeout'); + throw ApiException( + type: ApiErrorType.network, + message: 'Importen tog för lång tid. Försök igen.', + ); + }, + ); + + developer.log('Received response with status: ${response.statusCode}', name: 'ImportRepository'); + + if (response.statusCode != 200) { + developer.log('Error response: ${response.body}', name: 'ImportRepository', error: 'HTTP Error'); + throw ApiException( + type: _mapStatusCodeToErrorType(response.statusCode), + message: 'Fel vid import: ${response.body}', + statusCode: response.statusCode, ); - - return QuickImportResult.fromJson(_parseResponse(response)); + } + + developer.log('Successfully imported URL', name: 'ImportRepository'); + return QuickImportResult.fromJson(_parseResponse(response)); + } catch (e) { + developer.log('Exception during URL import: $e', name: 'ImportRepository', error: e); + rethrow; + } } - Map _parseResponse(http.Response response) { - if (response.statusCode >= 200 && response.statusCode < 300) { - return jsonDecode(response.body) as Map; - } + /// Helper method to map HTTP status codes to [ApiErrorType]. + ApiErrorType _mapStatusCodeToErrorType(int statusCode) { + if (statusCode == 401) return ApiErrorType.unauthorized; + if (statusCode == 403) return ApiErrorType.forbidden; + if (statusCode >= 500) return ApiErrorType.server; + return ApiErrorType.unknown; + } - Map? body; + /// Parse the HTTP response and handle potential errors. + dynamic _parseResponse(http.Response response) { try { - body = jsonDecode(response.body) as Map; - } catch (_) {} - - final message = body?['message'] as String? ?? - body?['error'] as String? ?? - 'Import misslyckades (${response.statusCode}).'; - - if (response.statusCode == 401) { + return jsonDecode(response.body); + } catch (e) { + developer.log('Failed to parse response: ${response.body}', name: 'ImportRepository', error: e); throw ApiException( - type: ApiErrorType.unauthorized, - statusCode: 401, - message: 'Inte inloggad.'); - } - if (response.statusCode == 403) { - throw ApiException( - type: ApiErrorType.forbidden, - statusCode: 403, - message: 'Åtkomst nekad.'); - } - if (response.statusCode >= 500) { - throw ApiException( - type: ApiErrorType.server, - statusCode: response.statusCode, - message: message); - } - throw ApiException( type: ApiErrorType.unknown, - statusCode: response.statusCode, - message: message); + message: 'Felaktigt svar från servern: ${response.body}', + ); + } } }