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:
@@ -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,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<String, dynamic> _parseResponse(http.Response response) {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
}
|
||||
/// 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<String, dynamic>? body;
|
||||
/// Parse the HTTP response and handle potential errors.
|
||||
dynamic _parseResponse(http.Response response) {
|
||||
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) {
|
||||
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}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user