feat(data-connect): Implement caching for business ID and enhance error handling in DataConnectService
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user