feat(data-connect): Implement caching for business ID and enhance error handling in DataConnectService

This commit is contained in:
Achintha Isuru
2026-02-16 17:16:29 -05:00
parent d2cb05fe2e
commit fdd40ba72c
3 changed files with 154 additions and 147 deletions

View File

@@ -27,6 +27,9 @@ class DataConnectService with DataErrorHandler {
/// Cache for the current staff ID to avoid redundant lookups. /// Cache for the current staff ID to avoid redundant lookups.
String? _cachedStaffId; 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. /// Gets the current staff ID from session store or persistent storage.
Future<String> getStaffId() async { Future<String> getStaffId() async {
// 1. Check Session Store // 1. Check Session Store
@@ -41,15 +44,14 @@ class DataConnectService with DataErrorHandler {
// 3. Fetch from Data Connect using Firebase UID // 3. Fetch from Data Connect using Firebase UID
final firebase_auth.User? user = _auth.currentUser; final firebase_auth.User? user = _auth.currentUser;
if (user == null) { if (user == null) {
throw Exception('User is not authenticated'); throw const NotAuthenticatedException(
technicalMessage: 'User is not authenticated',
);
} }
try { try {
final fdc.QueryResult< final fdc.QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables>
dc.GetStaffByUserIdData, response = await executeProtected(
dc.GetStaffByUserIdVariables
>
response = await executeProtected(
() => connector.getStaffByUserId(userId: user.uid).execute(), () => connector.getStaffByUserId(userId: user.uid).execute(),
); );
@@ -65,6 +67,30 @@ class DataConnectService with DataErrorHandler {
return user.uid; return user.uid;
} }
/// Gets the current business ID from session store or persistent storage.
Future<String> 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]. /// Converts a Data Connect timestamp/string/json to a [DateTime].
DateTime? toDateTime(dynamic t) { DateTime? toDateTime(dynamic t) {
if (t == null) return null; if (t == null) return null;
@@ -116,5 +142,6 @@ class DataConnectService with DataErrorHandler {
/// Clears the internal cache (e.g., on logout). /// Clears the internal cache (e.g., on logout).
void clearCache() { void clearCache() {
_cachedStaffId = null; _cachedStaffId = null;
_cachedBusinessId = null;
} }
} }

View File

@@ -1,6 +1,5 @@
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.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 'data/repositories_impl/billing_repository_impl.dart';
import 'domain/repositories/billing_repository.dart'; import 'domain/repositories/billing_repository.dart';
@@ -19,11 +18,7 @@ class BillingModule extends Module {
// Repositories // Repositories
i.addSingleton<BillingRepository>( i.addSingleton<BillingRepository>(BillingRepositoryImpl.new);
() => BillingRepositoryImpl(
dataConnect: ExampleConnector.instance,
),
);
// Use Cases // Use Cases
i.addSingleton(GetCurrentBillAmountUseCase.new); i.addSingleton(GetCurrentBillAmountUseCase.new);

View File

@@ -6,88 +6,78 @@ import '../../domain/repositories/billing_repository.dart';
/// Implementation of [BillingRepository] in the Data layer. /// Implementation of [BillingRepository] in the Data layer.
/// ///
/// This class is responsible for retrieving billing data from the [FinancialRepositoryMock] /// This class is responsible for retrieving billing data from the
/// (which represents the Data Connect layer) and mapping it to Domain entities. /// Data Connect layer and mapping it to Domain entities.
/// class BillingRepositoryImpl implements BillingRepository {
/// 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 {
/// Creates a [BillingRepositoryImpl]. /// Creates a [BillingRepositoryImpl].
///
/// Requires the [financialRepository] to fetch financial data.
BillingRepositoryImpl({ BillingRepositoryImpl({
required data_connect.ExampleConnector dataConnect, data_connect.DataConnectService? service,
}) : _dataConnect = dataConnect; }) : _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. /// Fetches the current bill amount by aggregating open invoices.
@override @override
@override
Future<double> getCurrentBillAmount() async { Future<double> getCurrentBillAmount() async {
final String? businessId = return _service.run(() async {
data_connect.ClientSessionStore.instance.session?.business?.id; final String businessId = await _service.getBusinessId();
if (businessId == null || businessId.isEmpty) {
return 0.0;
}
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData, data_connect.ListInvoicesByBusinessIdVariables> result = await executeProtected(() => _dataConnect final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
.listInvoicesByBusinessId(businessId: businessId) data_connect.ListInvoicesByBusinessIdVariables> result =
.execute()); await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices return result.data.invoices
.map(_mapInvoice) .map(_mapInvoice)
.where((Invoice i) => i.status == InvoiceStatus.open) .where((Invoice i) => i.status == InvoiceStatus.open)
.fold<double>( .fold<double>(
0.0, 0.0,
(double sum, Invoice item) => sum + item.totalAmount, (double sum, Invoice item) => sum + item.totalAmount,
); );
});
} }
/// Fetches the history of paid invoices. /// Fetches the history of paid invoices.
@override @override
Future<List<Invoice>> getInvoiceHistory() async { Future<List<Invoice>> getInvoiceHistory() async {
final String? businessId = return _service.run(() async {
data_connect.ClientSessionStore.instance.session?.business?.id; final String businessId = await _service.getBusinessId();
if (businessId == null || businessId.isEmpty) {
return <Invoice>[];
}
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData, data_connect.ListInvoicesByBusinessIdVariables> result = await executeProtected(() => _dataConnect final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
.listInvoicesByBusinessId( data_connect.ListInvoicesByBusinessIdVariables> result =
businessId: businessId, await _service.connector
) .listInvoicesByBusinessId(
.limit(10) businessId: businessId,
.execute()); )
.limit(10)
.execute();
return result.data.invoices.map(_mapInvoice).toList(); return result.data.invoices.map(_mapInvoice).toList();
});
} }
/// Fetches pending invoices (Open or Disputed). /// Fetches pending invoices (Open or Disputed).
@override @override
@override
Future<List<Invoice>> getPendingInvoices() async { Future<List<Invoice>> getPendingInvoices() async {
final String? businessId = return _service.run(() async {
data_connect.ClientSessionStore.instance.session?.business?.id; final String businessId = await _service.getBusinessId();
if (businessId == null || businessId.isEmpty) {
return <Invoice>[];
}
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData, data_connect.ListInvoicesByBusinessIdVariables> result = await executeProtected(() => _dataConnect final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
.listInvoicesByBusinessId(businessId: businessId) data_connect.ListInvoicesByBusinessIdVariables> result =
.execute()); await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices return result.data.invoices
.map(_mapInvoice) .map(_mapInvoice)
.where( .where(
(Invoice i) => (Invoice i) =>
i.status == InvoiceStatus.open || i.status == InvoiceStatus.open ||
i.status == InvoiceStatus.disputed, i.status == InvoiceStatus.disputed,
) )
.toList(); .toList();
});
} }
/// Fetches the estimated savings amount. /// Fetches the estimated savings amount.
@@ -101,86 +91,81 @@ class BillingRepositoryImpl
/// Fetches the breakdown of spending. /// Fetches the breakdown of spending.
@override @override
Future<List<InvoiceItem>> getSpendingBreakdown(BillingPeriod period) async { Future<List<InvoiceItem>> getSpendingBreakdown(BillingPeriod period) async {
final String? businessId = return _service.run(() async {
data_connect.ClientSessionStore.instance.session?.business?.id; final String businessId = await _service.getBusinessId();
if (businessId == null || businessId.isEmpty) {
return <InvoiceItem>[];
}
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
final DateTime start; final DateTime start;
final DateTime end; final DateTime end;
if (period == BillingPeriod.week) { if (period == BillingPeriod.week) {
final int daysFromMonday = now.weekday - DateTime.monday; final int daysFromMonday = now.weekday - DateTime.monday;
final DateTime monday = DateTime( final DateTime monday = DateTime(
now.year, now.year,
now.month, now.month,
now.day, now.day,
).subtract(Duration(days: daysFromMonday)); ).subtract(Duration(days: daysFromMonday));
start = DateTime(monday.year, monday.month, monday.day); start = DateTime(monday.year, monday.month, monday.day);
end = DateTime(monday.year, monday.month, monday.day + 6, 23, 59, 59, 999); end = DateTime(
} else { monday.year, monday.month, monday.day + 6, 23, 59, 59, 999);
start = DateTime(now.year, now.month, 1);
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59, 999);
}
final fdc.QueryResult<data_connect.ListShiftRolesByBusinessAndDatesSummaryData, data_connect.ListShiftRolesByBusinessAndDatesSummaryVariables> result = await executeProtected(() => _dataConnect
.listShiftRolesByBusinessAndDatesSummary(
businessId: businessId,
start: _toTimestamp(start),
end: _toTimestamp(end),
)
.execute());
final List<data_connect.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
shiftRoles = result.data.shiftRoles;
if (shiftRoles.isEmpty) {
return <InvoiceItem>[];
}
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
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 { } else {
summary[roleId] = existing.copyWith( start = DateTime(now.year, now.month, 1);
totalHours: existing.totalHours + hours, end = DateTime(now.year, now.month + 1, 0, 23, 59, 59, 999);
totalValue: existing.totalValue + totalValue,
);
} }
}
return summary.values final fdc.QueryResult<
.map( data_connect.ListShiftRolesByBusinessAndDatesSummaryData,
(_RoleSummary item) => InvoiceItem( data_connect.ListShiftRolesByBusinessAndDatesSummaryVariables>
id: item.roleId, result = await _service.connector
invoiceId: item.roleId, .listShiftRolesByBusinessAndDatesSummary(
staffId: item.roleName, businessId: businessId,
workHours: item.totalHours, start: _service.toTimestamp(start),
rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0, end: _service.toTimestamp(end),
amount: item.totalValue, )
), .execute();
)
.toList();
}
fdc.Timestamp _toTimestamp(DateTime dateTime) { final List<data_connect.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
final DateTime utc = dateTime.toUtc(); shiftRoles = result.data.shiftRoles;
final int seconds = utc.millisecondsSinceEpoch ~/ 1000; if (shiftRoles.isEmpty) {
final int nanoseconds = return <InvoiceItem>[];
(utc.millisecondsSinceEpoch % 1000) * 1000000; }
return fdc.Timestamp(nanoseconds, seconds);
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
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) { Invoice _mapInvoice(data_connect.ListInvoicesByBusinessIdInvoices invoice) {
@@ -193,7 +178,7 @@ class BillingRepositoryImpl
workAmount: invoice.amount, workAmount: invoice.amount,
addonsAmount: invoice.otherCharges ?? 0, addonsAmount: invoice.otherCharges ?? 0,
invoiceNumber: invoice.invoiceNumber, invoiceNumber: invoice.invoiceNumber,
issueDate: invoice.issueDate.toDateTime(), issueDate: _service.toDateTime(invoice.issueDate)!,
); );
} }