import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../models/closed_tab.dart'; import '../models/closed_tab_item.dart'; import '../utils/app_config.dart'; import 'bar_tab_service.dart'; abstract class ExportService { Future uploadToServer(); } /// Uploads the closed-tab history to the backup server. Best effort: a /// failed backup is logged but never interrupts the bar. class DefaultExportService implements ExportService { final BarTabService barTabService; final Future Function()? authToken; final http.Client _client; DefaultExportService({ required this.barTabService, this.authToken, http.Client? client, }) : _client = client ?? http.Client(); static const _timeout = Duration(seconds: 30); @override Future uploadToServer() async { try { final closedTabs = await barTabService.getClosedTabs(); final appVersion = (await PackageInfo.fromPlatform()).version; final token = await authToken?.call(); final response = await _client .post( Uri.parse(kExportServerUrl), headers: { 'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }, body: jsonEncode(_buildPayload(closedTabs, appVersion: appVersion)), ) .timeout(_timeout); if (response.statusCode != 200 && response.statusCode != 201) { unawaited( Sentry.captureMessage( 'Backup upload failed with HTTP ${response.statusCode}', level: SentryLevel.warning, ), ); } } on Exception catch (e) { // Mostly offline tablets; not worth an error report. debugPrint('ExportService: backup upload failed: $e'); } } Map _buildPayload( List tabs, { required String appVersion, }) { return { 'exportedAt': DateTime.now().toIso8601String(), 'appVersion': appVersion, 'closedTabs': tabs.map(_tabToJson).toList(), }; } Map _tabToJson(ClosedTab tab) { return { 'id': tab.id, 'originalTabId': tab.originalTabId, 'customerName': tab.customerName, 'closedAt': tab.closedAt.toIso8601String(), 'paymentMethod': tab.paymentMethod.value, 'items': tab.items.map(_itemToJson).toList(), 'itemCount': tab.itemCount, 'totalInCents': tab.totalInCents, }; } Map _itemToJson(ClosedTabItem item) { return { 'productId': item.productId, 'productName': item.productName, 'quantity': item.quantity, 'unitPriceInCents': item.unitPriceInCents, 'purchasedAt': item.purchasedAt?.toIso8601String(), 'removedAt': item.removedAt?.toIso8601String(), 'isRemoved': item.isRemoved, 'lineTotalInCents': item.lineTotalInCents, }; } }