feat: enhance import functionality with detailed logging and error handling for receipt and URL imports

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Nils-Johan Gynther
2026-04-30 11:32:28 +02:00
parent 1ac644eb3e
commit 4188cea7d9
@@ -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<List<ParsedReceiptItem>> 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<String, dynamic>)).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<List<ParsedReceiptItem>> 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<String, dynamic>)).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,6 +96,9 @@ class ImportRepository {
required String filename,
String? token,
}) async {
try {
developer.log('Starting file import for file: $filename', name: 'ImportRepository');
final uri = Uri.parse('$_baseUrl/quick-import');
final request = http.MultipartRequest('POST', uri);
@@ -72,16 +110,37 @@ class ImportRepository {
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: () => throw ApiException(
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;
}
}
/// Import a recipe from a URL.
@@ -89,6 +148,9 @@ class ImportRepository {
required String url,
String? token,
}) async {
try {
developer.log('Starting URL import for: $url', name: 'ImportRepository');
final uri = Uri.parse('$_baseUrl/quick-import');
final response = await _client
.post(
@@ -101,50 +163,52 @@ class ImportRepository {
)
.timeout(
const Duration(seconds: 120),
onTimeout: () => throw ApiException(
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.',
),
);
},
);
return QuickImportResult.fromJson(_parseResponse(response));
}
developer.log('Received response with status: ${response.statusCode}', name: 'ImportRepository');
Map<String, dynamic> _parseResponse(http.Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonDecode(response.body) as Map<String, dynamic>;
}
Map<String, dynamic>? body;
try {
body = jsonDecode(response.body) as Map<String, dynamic>;
} catch (_) {}
final message = body?['message'] as String? ??
body?['error'] as String? ??
'Import misslyckades (${response.statusCode}).';
if (response.statusCode == 401) {
if (response.statusCode != 200) {
developer.log('Error response: ${response.body}', name: 'ImportRepository', error: 'HTTP Error');
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,
type: _mapStatusCodeToErrorType(response.statusCode),
message: 'Fel vid import: ${response.body}',
statusCode: response.statusCode,
message: message);
);
}
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;
}
}
/// 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;
}
/// Parse the HTTP response and handle potential errors.
dynamic _parseResponse(http.Response response) {
try {
return jsonDecode(response.body);
} catch (e) {
developer.log('Failed to parse response: ${response.body}', name: 'ImportRepository', error: e);
throw ApiException(
type: ApiErrorType.unknown,
statusCode: response.statusCode,
message: message);
message: 'Felaktigt svar från servern: ${response.body}',
);
}
}
}