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 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
import '../domain/quick_import_result.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).
|
/// • [importFile] — multipart upload (PDF / image bytes, max 10 MB).
|
||||||
/// • [importUrl] — JSON body with `{ input: url }`.
|
/// • [importUrl] — JSON body with `{ input: url }`.
|
||||||
class ImportRepository {
|
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 http.Client _client;
|
||||||
final String _baseUrl;
|
final String _baseUrl;
|
||||||
|
|
||||||
@@ -51,6 +24,68 @@ class ImportRepository {
|
|||||||
defaultValue: '/api',
|
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.
|
/// Upload a file (PDF or image) for recipe extraction.
|
||||||
///
|
///
|
||||||
/// [bytes] — raw file bytes from file_picker.
|
/// [bytes] — raw file bytes from file_picker.
|
||||||
@@ -61,27 +96,51 @@ class ImportRepository {
|
|||||||
required String filename,
|
required String filename,
|
||||||
String? token,
|
String? token,
|
||||||
}) async {
|
}) async {
|
||||||
final uri = Uri.parse('$_baseUrl/quick-import');
|
try {
|
||||||
final request = http.MultipartRequest('POST', uri);
|
developer.log('Starting file import for file: $filename', name: 'ImportRepository');
|
||||||
|
|
||||||
if (token != null) {
|
final uri = Uri.parse('$_baseUrl/quick-import');
|
||||||
request.headers['Authorization'] = 'Bearer $token';
|
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.
|
/// Import a recipe from a URL.
|
||||||
@@ -89,62 +148,67 @@ class ImportRepository {
|
|||||||
required String url,
|
required String url,
|
||||||
String? token,
|
String? token,
|
||||||
}) async {
|
}) async {
|
||||||
final uri = Uri.parse('$_baseUrl/quick-import');
|
try {
|
||||||
final response = await _client
|
developer.log('Starting URL import for: $url', name: 'ImportRepository');
|
||||||
.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.',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return QuickImportResult.fromJson(_parseResponse(response));
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
/// Helper method to map HTTP status codes to [ApiErrorType].
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
ApiErrorType _mapStatusCodeToErrorType(int statusCode) {
|
||||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
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 {
|
try {
|
||||||
body = jsonDecode(response.body) as Map<String, dynamic>;
|
return jsonDecode(response.body);
|
||||||
} catch (_) {}
|
} catch (e) {
|
||||||
|
developer.log('Failed to parse response: ${response.body}', name: 'ImportRepository', error: e);
|
||||||
final message = body?['message'] as String? ??
|
|
||||||
body?['error'] as String? ??
|
|
||||||
'Import misslyckades (${response.statusCode}).';
|
|
||||||
|
|
||||||
if (response.statusCode == 401) {
|
|
||||||
throw ApiException(
|
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,
|
type: ApiErrorType.unknown,
|
||||||
statusCode: response.statusCode,
|
message: 'Felaktigt svar från servern: ${response.body}',
|
||||||
message: message);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user