From fdd40ba72c51e70091a0e55742370a71b38699d3 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 17:16:29 -0500 Subject: [PATCH] feat(data-connect): Implement caching for business ID and enhance error handling in DataConnectService --- .../src/services/data_connect_service.dart | 39 ++- .../billing/lib/src/billing_module.dart | 7 +- .../billing_repository_impl.dart | 255 +++++++++--------- 3 files changed, 154 insertions(+), 147 deletions(-) diff --git a/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart b/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart index c91c34d1..bad4b174 100644 --- a/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart +++ b/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart @@ -27,6 +27,9 @@ class DataConnectService with DataErrorHandler { /// Cache for the current staff ID to avoid redundant lookups. String? _cachedStaffId; + /// Cache for the current business ID to avoid redundant lookups. + String? _cachedBusinessId; + /// Gets the current staff ID from session store or persistent storage. Future getStaffId() async { // 1. Check Session Store @@ -41,15 +44,14 @@ class DataConnectService with DataErrorHandler { // 3. Fetch from Data Connect using Firebase UID final firebase_auth.User? user = _auth.currentUser; if (user == null) { - throw Exception('User is not authenticated'); + throw const NotAuthenticatedException( + technicalMessage: 'User is not authenticated', + ); } try { - final fdc.QueryResult< - dc.GetStaffByUserIdData, - dc.GetStaffByUserIdVariables - > - response = await executeProtected( + final fdc.QueryResult + response = await executeProtected( () => connector.getStaffByUserId(userId: user.uid).execute(), ); @@ -65,6 +67,30 @@ class DataConnectService with DataErrorHandler { return user.uid; } + /// Gets the current business ID from session store or persistent storage. + Future getBusinessId() async { + // 1. Check Session Store + final dc.ClientSession? session = dc.ClientSessionStore.instance.session; + if (session?.business?.id != null) { + return session!.business!.id; + } + + // 2. Check Cache + if (_cachedBusinessId != null) return _cachedBusinessId!; + + // 3. Check Auth Status + final firebase_auth.User? user = _auth.currentUser; + if (user == null) { + throw const NotAuthenticatedException( + technicalMessage: 'User is not authenticated', + ); + } + + // 4. Fallback (should ideally not happen if DB is seeded and session is initialized) + // Ideally we'd have a getBusinessByUserId query here. + return user.uid; + } + /// Converts a Data Connect timestamp/string/json to a [DateTime]. DateTime? toDateTime(dynamic t) { if (t == null) return null; @@ -116,5 +142,6 @@ class DataConnectService with DataErrorHandler { /// Clears the internal cache (e.g., on logout). void clearCache() { _cachedStaffId = null; + _cachedBusinessId = null; } } diff --git a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart index a8591d07..8c639cb3 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart @@ -1,6 +1,5 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; -import 'package:krow_data_connect/krow_data_connect.dart'; import 'data/repositories_impl/billing_repository_impl.dart'; import 'domain/repositories/billing_repository.dart'; @@ -19,11 +18,7 @@ class BillingModule extends Module { // Repositories - i.addSingleton( - () => BillingRepositoryImpl( - dataConnect: ExampleConnector.instance, - ), - ); + i.addSingleton(BillingRepositoryImpl.new); // Use Cases i.addSingleton(GetCurrentBillAmountUseCase.new); diff --git a/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart b/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart index 689ac4bf..d0441b26 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart @@ -6,88 +6,78 @@ import '../../domain/repositories/billing_repository.dart'; /// Implementation of [BillingRepository] in the Data layer. /// -/// This class is responsible for retrieving billing data from the [FinancialRepositoryMock] -/// (which represents the Data Connect layer) and mapping it to Domain entities. -/// -/// It strictly adheres to the Clean Architecture data layer responsibilities: -/// - No business logic (except necessary data transformation/filtering). -/// - Delegates to data sources. -class BillingRepositoryImpl - with data_connect.DataErrorHandler - implements BillingRepository { +/// This class is responsible for retrieving billing data from the +/// Data Connect layer and mapping it to Domain entities. +class BillingRepositoryImpl implements BillingRepository { /// Creates a [BillingRepositoryImpl]. - /// - /// Requires the [financialRepository] to fetch financial data. BillingRepositoryImpl({ - required data_connect.ExampleConnector dataConnect, - }) : _dataConnect = dataConnect; + data_connect.DataConnectService? service, + }) : _service = service ?? data_connect.DataConnectService.instance; - final data_connect.ExampleConnector _dataConnect; + final data_connect.DataConnectService _service; /// Fetches the current bill amount by aggregating open invoices. @override - @override Future getCurrentBillAmount() async { - final String? businessId = - data_connect.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return 0.0; - } + return _service.run(() async { + final String businessId = await _service.getBusinessId(); - final fdc.QueryResult result = await executeProtected(() => _dataConnect - .listInvoicesByBusinessId(businessId: businessId) - .execute()); + final fdc.QueryResult result = + await _service.connector + .listInvoicesByBusinessId(businessId: businessId) + .execute(); - return result.data.invoices - .map(_mapInvoice) - .where((Invoice i) => i.status == InvoiceStatus.open) - .fold( - 0.0, - (double sum, Invoice item) => sum + item.totalAmount, - ); + return result.data.invoices + .map(_mapInvoice) + .where((Invoice i) => i.status == InvoiceStatus.open) + .fold( + 0.0, + (double sum, Invoice item) => sum + item.totalAmount, + ); + }); } /// Fetches the history of paid invoices. @override Future> getInvoiceHistory() async { - final String? businessId = - data_connect.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return []; - } + return _service.run(() async { + final String businessId = await _service.getBusinessId(); - final fdc.QueryResult result = await executeProtected(() => _dataConnect - .listInvoicesByBusinessId( - businessId: businessId, - ) - .limit(10) - .execute()); + final fdc.QueryResult result = + await _service.connector + .listInvoicesByBusinessId( + businessId: businessId, + ) + .limit(10) + .execute(); - return result.data.invoices.map(_mapInvoice).toList(); + return result.data.invoices.map(_mapInvoice).toList(); + }); } /// Fetches pending invoices (Open or Disputed). @override - @override Future> getPendingInvoices() async { - final String? businessId = - data_connect.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return []; - } + return _service.run(() async { + final String businessId = await _service.getBusinessId(); - final fdc.QueryResult result = await executeProtected(() => _dataConnect - .listInvoicesByBusinessId(businessId: businessId) - .execute()); + final fdc.QueryResult result = + await _service.connector + .listInvoicesByBusinessId(businessId: businessId) + .execute(); - return result.data.invoices - .map(_mapInvoice) - .where( - (Invoice i) => - i.status == InvoiceStatus.open || - i.status == InvoiceStatus.disputed, - ) - .toList(); + return result.data.invoices + .map(_mapInvoice) + .where( + (Invoice i) => + i.status == InvoiceStatus.open || + i.status == InvoiceStatus.disputed, + ) + .toList(); + }); } /// Fetches the estimated savings amount. @@ -101,86 +91,81 @@ class BillingRepositoryImpl /// Fetches the breakdown of spending. @override Future> getSpendingBreakdown(BillingPeriod period) async { - final String? businessId = - data_connect.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return []; - } + return _service.run(() async { + final String businessId = await _service.getBusinessId(); - final DateTime now = DateTime.now(); - final DateTime start; - final DateTime end; - if (period == BillingPeriod.week) { - final int daysFromMonday = now.weekday - DateTime.monday; - final DateTime monday = DateTime( - now.year, - now.month, - now.day, - ).subtract(Duration(days: daysFromMonday)); - start = DateTime(monday.year, monday.month, monday.day); - end = DateTime(monday.year, monday.month, monday.day + 6, 23, 59, 59, 999); - } else { - start = DateTime(now.year, now.month, 1); - end = DateTime(now.year, now.month + 1, 0, 23, 59, 59, 999); - } - - final fdc.QueryResult result = await executeProtected(() => _dataConnect - .listShiftRolesByBusinessAndDatesSummary( - businessId: businessId, - start: _toTimestamp(start), - end: _toTimestamp(end), - ) - .execute()); - - final List - shiftRoles = result.data.shiftRoles; - if (shiftRoles.isEmpty) { - return []; - } - - final Map summary = {}; - for (final data_connect.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role - in shiftRoles) { - final String roleId = role.roleId; - final String roleName = role.role.name; - final double hours = role.hours ?? 0.0; - final double totalValue = role.totalValue ?? 0.0; - final _RoleSummary? existing = summary[roleId]; - if (existing == null) { - summary[roleId] = _RoleSummary( - roleId: roleId, - roleName: roleName, - totalHours: hours, - totalValue: totalValue, - ); + final DateTime now = DateTime.now(); + final DateTime start; + final DateTime end; + if (period == BillingPeriod.week) { + final int daysFromMonday = now.weekday - DateTime.monday; + final DateTime monday = DateTime( + now.year, + now.month, + now.day, + ).subtract(Duration(days: daysFromMonday)); + start = DateTime(monday.year, monday.month, monday.day); + end = DateTime( + monday.year, monday.month, monday.day + 6, 23, 59, 59, 999); } else { - summary[roleId] = existing.copyWith( - totalHours: existing.totalHours + hours, - totalValue: existing.totalValue + totalValue, - ); + start = DateTime(now.year, now.month, 1); + end = DateTime(now.year, now.month + 1, 0, 23, 59, 59, 999); } - } - return summary.values - .map( - (_RoleSummary item) => InvoiceItem( - id: item.roleId, - invoiceId: item.roleId, - staffId: item.roleName, - workHours: item.totalHours, - rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0, - amount: item.totalValue, - ), - ) - .toList(); - } + final fdc.QueryResult< + data_connect.ListShiftRolesByBusinessAndDatesSummaryData, + data_connect.ListShiftRolesByBusinessAndDatesSummaryVariables> + result = await _service.connector + .listShiftRolesByBusinessAndDatesSummary( + businessId: businessId, + start: _service.toTimestamp(start), + end: _service.toTimestamp(end), + ) + .execute(); - fdc.Timestamp _toTimestamp(DateTime dateTime) { - final DateTime utc = dateTime.toUtc(); - final int seconds = utc.millisecondsSinceEpoch ~/ 1000; - final int nanoseconds = - (utc.millisecondsSinceEpoch % 1000) * 1000000; - return fdc.Timestamp(nanoseconds, seconds); + final List + shiftRoles = result.data.shiftRoles; + if (shiftRoles.isEmpty) { + return []; + } + + final Map summary = {}; + for (final data_connect + .ListShiftRolesByBusinessAndDatesSummaryShiftRoles role + in shiftRoles) { + final String roleId = role.roleId; + final String roleName = role.role.name; + final double hours = role.hours ?? 0.0; + final double totalValue = role.totalValue ?? 0.0; + final _RoleSummary? existing = summary[roleId]; + if (existing == null) { + summary[roleId] = _RoleSummary( + roleId: roleId, + roleName: roleName, + totalHours: hours, + totalValue: totalValue, + ); + } else { + summary[roleId] = existing.copyWith( + totalHours: existing.totalHours + hours, + totalValue: existing.totalValue + totalValue, + ); + } + } + + return summary.values + .map( + (_RoleSummary item) => InvoiceItem( + id: item.roleId, + invoiceId: item.roleId, + staffId: item.roleName, + workHours: item.totalHours, + rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0, + amount: item.totalValue, + ), + ) + .toList(); + }); } Invoice _mapInvoice(data_connect.ListInvoicesByBusinessIdInvoices invoice) { @@ -193,7 +178,7 @@ class BillingRepositoryImpl workAmount: invoice.amount, addonsAmount: invoice.otherCharges ?? 0, invoiceNumber: invoice.invoiceNumber, - issueDate: invoice.issueDate.toDateTime(), + issueDate: _service.toDateTime(invoice.issueDate)!, ); }