From fdd40ba72c51e70091a0e55742370a71b38699d3 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 17:16:29 -0500 Subject: [PATCH 1/3] 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)!, ); } From fc0bb5828c024a86641ebe992063af92409d07c9 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 17:21:03 -0500 Subject: [PATCH 2/3] feat(auth-repository): Refactor AuthRepositoryImpl to remove FirebaseAuth dependency and utilize DataConnectService --- .../lib/client_authentication.dart | 8 +- .../auth_repository_impl.dart | 155 ++++++++++-------- 2 files changed, 91 insertions(+), 72 deletions(-) diff --git a/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart b/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart index 12310fde..1ee73543 100644 --- a/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart +++ b/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart @@ -1,6 +1,5 @@ library; -import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; @@ -28,12 +27,7 @@ class ClientAuthenticationModule extends Module { @override void binds(Injector i) { // Repositories - i.addLazySingleton( - () => AuthRepositoryImpl( - firebaseAuth: firebase.FirebaseAuth.instance, - dataConnect: ExampleConnector.instance, - ), - ); + i.addLazySingleton(AuthRepositoryImpl.new); // UseCases i.addLazySingleton( diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index f9ea9264..467a7c07 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -12,7 +12,6 @@ import 'package:krow_domain/krow_domain.dart' AccountExistsException, UserNotFoundException, UnauthorizedAppException, - UnauthorizedAppException, PasswordMismatchException, NetworkException; import 'package:krow_domain/krow_domain.dart' as domain; @@ -23,18 +22,13 @@ import '../../domain/repositories/auth_repository_interface.dart'; /// /// This implementation integrates with Firebase Authentication for user /// identity management and Krow's Data Connect SDK for storing user profile data. -class AuthRepositoryImpl - with dc.DataErrorHandler - implements AuthRepositoryInterface { - +class AuthRepositoryImpl implements AuthRepositoryInterface { /// Creates an [AuthRepositoryImpl] with the real dependencies. AuthRepositoryImpl({ - required firebase.FirebaseAuth firebaseAuth, - required dc.ExampleConnector dataConnect, - }) : _firebaseAuth = firebaseAuth, - _dataConnect = dataConnect; - final firebase.FirebaseAuth _firebaseAuth; - final dc.ExampleConnector _dataConnect; + dc.DataConnectService? service, + }) : _service = service ?? dc.DataConnectService.instance; + + final dc.DataConnectService _service; @override Future signInWithEmail({ @@ -42,7 +36,8 @@ class AuthRepositoryImpl required String password, }) async { try { - final firebase.UserCredential credential = await _firebaseAuth.signInWithEmailAndPassword( + final firebase.UserCredential credential = + await _service.auth.signInWithEmailAndPassword( email: email, password: password, ); @@ -59,7 +54,6 @@ class AuthRepositoryImpl fallbackEmail: firebaseUser.email ?? email, requireBusinessRole: true, ); - } on firebase.FirebaseAuthException catch (e) { if (e.code == 'invalid-credential' || e.code == 'wrong-password') { throw InvalidCredentialsException( @@ -94,7 +88,8 @@ class AuthRepositoryImpl try { // Step 1: Try to create Firebase Auth user - final firebase.UserCredential credential = await _firebaseAuth.createUserWithEmailAndPassword( + final firebase.UserCredential credential = + await _service.auth.createUserWithEmailAndPassword( email: email, password: password, ); @@ -111,9 +106,9 @@ class AuthRepositoryImpl firebaseUser: firebaseUser, companyName: companyName, email: email, - onBusinessCreated: (String businessId) => createdBusinessId = businessId, + onBusinessCreated: (String businessId) => + createdBusinessId = businessId, ); - } on firebase.FirebaseAuthException catch (e) { if (e.code == 'weak-password') { throw WeakPasswordException( @@ -137,11 +132,13 @@ class AuthRepositoryImpl } } on domain.AppException { // Rollback for our known exceptions - await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId); + await _rollbackSignUp( + firebaseUser: firebaseUser, businessId: createdBusinessId); rethrow; } catch (e) { // Rollback: Clean up any partially created resources - await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId); + await _rollbackSignUp( + firebaseUser: firebaseUser, businessId: createdBusinessId); throw SignUpFailedException( technicalMessage: 'Unexpected error: $e', ); @@ -164,11 +161,13 @@ class AuthRepositoryImpl required String password, required String companyName, }) async { - developer.log('Email exists in Firebase, attempting sign-in: $email', name: 'AuthRepository'); + developer.log('Email exists in Firebase, attempting sign-in: $email', + name: 'AuthRepository'); try { // Try to sign in with the provided password - final firebase.UserCredential credential = await _firebaseAuth.signInWithEmailAndPassword( + final firebase.UserCredential credential = + await _service.auth.signInWithEmailAndPassword( email: email, password: password, ); @@ -181,28 +180,32 @@ class AuthRepositoryImpl } // Sign-in succeeded! Check if user already has a BUSINESS account in PostgreSQL - final bool hasBusinessAccount = await _checkBusinessUserExists(firebaseUser.uid); + final bool hasBusinessAccount = + await _checkBusinessUserExists(firebaseUser.uid); if (hasBusinessAccount) { // User already has a KROW Client account - developer.log('User already has BUSINESS account: ${firebaseUser.uid}', name: 'AuthRepository'); + developer.log('User already has BUSINESS account: ${firebaseUser.uid}', + name: 'AuthRepository'); throw AccountExistsException( technicalMessage: 'User ${firebaseUser.uid} already has BUSINESS role', ); } // User exists in Firebase but not in KROW PostgreSQL - create the entities - developer.log('Creating BUSINESS account for existing Firebase user: ${firebaseUser.uid}', name: 'AuthRepository'); + developer.log( + 'Creating BUSINESS account for existing Firebase user: ${firebaseUser.uid}', + name: 'AuthRepository'); return await _createBusinessAndUser( firebaseUser: firebaseUser, companyName: companyName, email: email, onBusinessCreated: (_) {}, // No rollback needed for existing Firebase user ); - } on firebase.FirebaseAuthException catch (e) { // Sign-in failed - check why - developer.log('Sign-in failed with code: ${e.code}', name: 'AuthRepository'); + developer.log('Sign-in failed with code: ${e.code}', + name: 'AuthRepository'); if (e.code == 'wrong-password' || e.code == 'invalid-credential') { // Password doesn't match - check what providers are available @@ -226,9 +229,11 @@ class AuthRepositoryImpl // We can't distinguish between "wrong password" and "no password provider" // due to Firebase deprecating fetchSignInMethodsForEmail. // The PasswordMismatchException message covers both scenarios. - developer.log('Password mismatch or different provider for: $email', name: 'AuthRepository'); + developer.log('Password mismatch or different provider for: $email', + name: 'AuthRepository'); throw PasswordMismatchException( - technicalMessage: 'Email $email: password mismatch or different auth provider', + technicalMessage: + 'Email $email: password mismatch or different auth provider', ); } @@ -236,9 +241,11 @@ class AuthRepositoryImpl Future _checkBusinessUserExists(String firebaseUserId) async { final QueryResult response = - await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute()); + await _service.run( + () => _service.connector.getUserById(id: firebaseUserId).execute()); final dc.GetUserByIdUser? user = response.data.user; - return user != null && (user.userRole == 'BUSINESS' || user.userRole == 'BOTH'); + return user != null && + (user.userRole == 'BUSINESS' || user.userRole == 'BOTH'); } /// Creates Business and User entities in PostgreSQL for a Firebase user. @@ -250,38 +257,44 @@ class AuthRepositoryImpl }) async { // Create Business entity in PostgreSQL - final OperationResult createBusinessResponse = - await executeProtected(() => _dataConnect.createBusiness( - businessName: companyName, - userId: firebaseUser.uid, - rateGroup: dc.BusinessRateGroup.STANDARD, - status: dc.BusinessStatus.PENDING, - ).execute()); + final OperationResult + createBusinessResponse = await _service.run(() => _service.connector + .createBusiness( + businessName: companyName, + userId: firebaseUser.uid, + rateGroup: dc.BusinessRateGroup.STANDARD, + status: dc.BusinessStatus.PENDING, + ) + .execute()); - final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert; + final dc.CreateBusinessBusinessInsert businessData = + createBusinessResponse.data.business_insert; onBusinessCreated(businessData.id); // Check if User entity already exists in PostgreSQL final QueryResult userResult = - await executeProtected(() => _dataConnect.getUserById(id: firebaseUser.uid).execute()); + await _service.run(() => + _service.connector.getUserById(id: firebaseUser.uid).execute()); final dc.GetUserByIdUser? existingUser = userResult.data.user; if (existingUser != null) { // User exists (likely in another app like STAFF). Update role to BOTH. - await executeProtected(() => _dataConnect.updateUser( - id: firebaseUser.uid, - ) - .userRole('BOTH') - .execute()); + await _service.run(() => _service.connector + .updateUser( + id: firebaseUser.uid, + ) + .userRole('BOTH') + .execute()); } else { // Create new User entity in PostgreSQL - await executeProtected(() => _dataConnect.createUser( - id: firebaseUser.uid, - role: dc.UserBaseRole.USER, - ) - .email(email) - .userRole('BUSINESS') - .execute()); + await _service.run(() => _service.connector + .createUser( + id: firebaseUser.uid, + role: dc.UserBaseRole.USER, + ) + .email(email) + .userRole('BUSINESS') + .execute()); } return _getUserProfile( @@ -298,7 +311,7 @@ class AuthRepositoryImpl // Delete business first (if created) if (businessId != null) { try { - await _dataConnect.deleteBusiness(id: businessId).execute(); + await _service.connector.deleteBusiness(id: businessId).execute(); } catch (_) { // Log but don't throw - we're already in error recovery } @@ -316,8 +329,9 @@ class AuthRepositoryImpl @override Future signOut() async { try { - await _firebaseAuth.signOut(); + await _service.auth.signOut(); dc.ClientSessionStore.instance.clear(); + _service.clearCache(); } catch (e) { throw Exception('Error signing out: ${e.toString()}'); } @@ -325,7 +339,8 @@ class AuthRepositoryImpl @override Future signInWithSocial({required String provider}) { - throw UnimplementedError('Social authentication with $provider is not yet implemented.'); + throw UnimplementedError( + 'Social authentication with $provider is not yet implemented.'); } Future _getUserProfile({ @@ -334,18 +349,24 @@ class AuthRepositoryImpl bool requireBusinessRole = false, }) async { final QueryResult response = - await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute()); + await _service.run(() => + _service.connector.getUserById(id: firebaseUserId).execute()); final dc.GetUserByIdUser? user = response.data.user; if (user == null) { throw UserNotFoundException( - technicalMessage: 'Firebase UID $firebaseUserId not found in users table', + technicalMessage: + 'Firebase UID $firebaseUserId not found in users table', ); } - if (requireBusinessRole && user.userRole != 'BUSINESS' && user.userRole != 'BOTH') { - await _firebaseAuth.signOut(); + if (requireBusinessRole && + user.userRole != 'BUSINESS' && + user.userRole != 'BOTH') { + await _service.auth.signOut(); dc.ClientSessionStore.instance.clear(); + _service.clearCache(); throw UnauthorizedAppException( - technicalMessage: 'User role is ${user.userRole}, expected BUSINESS or BOTH', + technicalMessage: + 'User role is ${user.userRole}, expected BUSINESS or BOTH', ); } @@ -362,13 +383,17 @@ class AuthRepositoryImpl role: user.role.stringValue, ); - final QueryResult businessResponse = - await executeProtected(() => _dataConnect.getBusinessesByUserId( - userId: firebaseUserId, - ).execute()); - final dc.GetBusinessesByUserIdBusinesses? business = businessResponse.data.businesses.isNotEmpty - ? businessResponse.data.businesses.first - : null; + final QueryResult businessResponse = + await _service.run(() => _service.connector + .getBusinessesByUserId( + userId: firebaseUserId, + ) + .execute()); + final dc.GetBusinessesByUserIdBusinesses? business = + businessResponse.data.businesses.isNotEmpty + ? businessResponse.data.businesses.first + : null; dc.ClientSessionStore.instance.setSession( dc.ClientSession( From 19eda09620946fce0fc9feb5a2d1d79b20e5a2c3 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 17:35:15 -0500 Subject: [PATCH 3/3] feat: Refactor ClientCreateOrderRepositoryImpl and OneTimeOrderBloc to utilize DataConnectService, removing FirebaseAuth dependency --- .../lib/src/create_order_module.dart | 15 +- .../client_create_order_repository_impl.dart | 200 ++++++++---------- .../blocs/one_time_order_bloc.dart | 16 +- 3 files changed, 99 insertions(+), 132 deletions(-) diff --git a/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart b/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart index db759e08..0e2624e2 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart @@ -2,7 +2,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; -import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'data/repositories_impl/client_create_order_repository_impl.dart'; import 'domain/repositories/client_create_order_repository_interface.dart'; import 'domain/usecases/create_one_time_order_usecase.dart'; @@ -29,12 +28,7 @@ class ClientCreateOrderModule extends Module { @override void binds(Injector i) { // Repositories - i.addLazySingleton( - () => ClientCreateOrderRepositoryImpl( - firebaseAuth: firebase.FirebaseAuth.instance, - dataConnect: ExampleConnector.instance, - ), - ); + i.addLazySingleton(ClientCreateOrderRepositoryImpl.new); // UseCases i.addLazySingleton(GetOrderTypesUseCase.new); @@ -44,12 +38,7 @@ class ClientCreateOrderModule extends Module { // BLoCs i.add(ClientCreateOrderBloc.new); i.add(RapidOrderBloc.new); - i.add( - () => OneTimeOrderBloc( - i.get(), - ExampleConnector.instance, - ), - ); + i.add(OneTimeOrderBloc.new); } @override diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart index eb905a2a..0ae65a1a 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -1,4 +1,3 @@ -import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:intl/intl.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; @@ -13,21 +12,16 @@ import '../../domain/repositories/client_create_order_repository_interface.dart' /// /// It follows the KROW Clean Architecture by keeping the data layer focused /// on delegation and data mapping, without business logic. -class ClientCreateOrderRepositoryImpl - with dc.DataErrorHandler - implements ClientCreateOrderRepositoryInterface { +class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInterface { ClientCreateOrderRepositoryImpl({ - required firebase.FirebaseAuth firebaseAuth, - required dc.ExampleConnector dataConnect, - }) : _firebaseAuth = firebaseAuth, - _dataConnect = dataConnect; + required dc.DataConnectService service, + }) : _service = service; - final firebase.FirebaseAuth _firebaseAuth; - final dc.ExampleConnector _dataConnect; + final dc.DataConnectService _service; @override Future> getOrderTypes() { - return Future.value(const [ + return Future>.value(const [ domain.OrderType( id: 'one-time', titleKey: 'client_create_order.types.one_time', @@ -55,100 +49,95 @@ class ClientCreateOrderRepositoryImpl @override Future createOneTimeOrder(domain.OneTimeOrder order) async { - final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - await _firebaseAuth.signOut(); - throw Exception('Business is missing. Please sign in again.'); - } - final String? vendorId = order.vendorId; - if (vendorId == null || vendorId.isEmpty) { - throw Exception('Vendor is missing.'); - } - final domain.OneTimeOrderHubDetails? hub = order.hub; - if (hub == null || hub.id.isEmpty) { - throw Exception('Hub is missing.'); - } + return _service.run(() async { + final String businessId = await _service.getBusinessId(); + final String? vendorId = order.vendorId; + if (vendorId == null || vendorId.isEmpty) { + throw Exception('Vendor is missing.'); + } + final domain.OneTimeOrderHubDetails? hub = order.hub; + if (hub == null || hub.id.isEmpty) { + throw Exception('Hub is missing.'); + } - final DateTime orderDateOnly = DateTime( - order.date.year, - order.date.month, - order.date.day, - ); - final fdc.Timestamp orderTimestamp = _toTimestamp(orderDateOnly); - final fdc.OperationResult - orderResult = await executeProtected(() => _dataConnect - .createOrder( - businessId: businessId, - orderType: dc.OrderType.ONE_TIME, - teamHubId: hub.id, + final DateTime orderDateOnly = DateTime( + order.date.year, + order.date.month, + order.date.day, + ); + final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly); + final fdc.OperationResult orderResult = + await _service.connector + .createOrder( + businessId: businessId, + orderType: dc.OrderType.ONE_TIME, + teamHubId: hub.id, + ) + .vendorId(vendorId) + .eventName(order.eventName) + .status(dc.OrderStatus.POSTED) + .date(orderTimestamp) + .execute(); + + final String orderId = orderResult.data.order_insert.id; + + final int workersNeeded = order.positions.fold( + 0, + (int sum, domain.OneTimeOrderPosition position) => sum + position.count, + ); + final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}'; + final double shiftCost = _calculateShiftCost(order); + + final fdc.OperationResult shiftResult = + await _service.connector + .createShift(title: shiftTitle, orderId: orderId) + .date(orderTimestamp) + .location(hub.name) + .locationAddress(hub.address) + .latitude(hub.latitude) + .longitude(hub.longitude) + .placeId(hub.placeId) + .city(hub.city) + .state(hub.state) + .street(hub.street) + .country(hub.country) + .status(dc.ShiftStatus.PENDING) + .workersNeeded(workersNeeded) + .filled(0) + .durationDays(1) + .cost(shiftCost) + .execute(); + + final String shiftId = shiftResult.data.shift_insert.id; + + for (final domain.OneTimeOrderPosition position in order.positions) { + final DateTime start = _parseTime(order.date, position.startTime); + final DateTime end = _parseTime(order.date, position.endTime); + final DateTime normalizedEnd = end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final double hours = normalizedEnd.difference(start).inMinutes / 60.0; + final double rate = order.roleRates[position.role] ?? 0; + final double totalValue = rate * hours * position.count; + + await _service.connector + .createShiftRole( + shiftId: shiftId, + roleId: position.role, + count: position.count, ) - .vendorId(vendorId) - .eventName(order.eventName) - .status(dc.OrderStatus.POSTED) - .date(orderTimestamp) - .execute()); + .startTime(_service.toTimestamp(start)) + .endTime(_service.toTimestamp(normalizedEnd)) + .hours(hours) + .breakType(_breakDurationFromValue(position.lunchBreak)) + .isBreakPaid(_isBreakPaid(position.lunchBreak)) + .totalValue(totalValue) + .execute(); + } - final String orderId = orderResult.data.order_insert.id; - - final int workersNeeded = order.positions.fold( - 0, - (int sum, domain.OneTimeOrderPosition position) => sum + position.count, - ); - final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}'; - final double shiftCost = _calculateShiftCost(order); - - final fdc.OperationResult - shiftResult = await executeProtected(() => _dataConnect - .createShift(title: shiftTitle, orderId: orderId) - .date(orderTimestamp) - .location(hub.name) - .locationAddress(hub.address) - .latitude(hub.latitude) - .longitude(hub.longitude) - .placeId(hub.placeId) - .city(hub.city) - .state(hub.state) - .street(hub.street) - .country(hub.country) - .status(dc.ShiftStatus.PENDING) - .workersNeeded(workersNeeded) - .filled(0) - .durationDays(1) - .cost(shiftCost) - .execute()); - - final String shiftId = shiftResult.data.shift_insert.id; - - for (final domain.OneTimeOrderPosition position in order.positions) { - final DateTime start = _parseTime(order.date, position.startTime); - final DateTime end = _parseTime(order.date, position.endTime); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; - final double hours = normalizedEnd.difference(start).inMinutes / 60.0; - final double rate = order.roleRates[position.role] ?? 0; - final double totalValue = rate * hours * position.count; - - - - await executeProtected(() => _dataConnect - .createShiftRole( - shiftId: shiftId, - roleId: position.role, - count: position.count, - ) - .startTime(_toTimestamp(start)) - .endTime(_toTimestamp(normalizedEnd)) - .hours(hours) - .breakType(_breakDurationFromValue(position.lunchBreak)) - .isBreakPaid(_isBreakPaid(position.lunchBreak)) - .totalValue(totalValue) - .execute()); - } - - await executeProtected(() => _dataConnect - .updateOrder(id: orderId, teamHubId: hub.id) - .shifts(fdc.AnyValue([shiftId])) - .execute()); + await _service.connector + .updateOrder(id: orderId, teamHubId: hub.id) + .shifts(fdc.AnyValue([shiftId])) + .execute(); + }); } @override @@ -213,13 +202,6 @@ class ClientCreateOrderRepositoryImpl ); } - fdc.Timestamp _toTimestamp(DateTime dateTime) { - final DateTime utc = dateTime.toUtc(); - final int seconds = utc.millisecondsSinceEpoch ~/ 1000; - final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000; - return fdc.Timestamp(nanoseconds, seconds); - } - String _formatDate(DateTime dateTime) { final String year = dateTime.year.toString().padLeft(4, '0'); final String month = dateTime.month.toString().padLeft(2, '0'); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart index 30fe20e1..7e11f0eb 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart @@ -11,7 +11,7 @@ import 'one_time_order_state.dart'; /// BLoC for managing the multi-step one-time order creation form. class OneTimeOrderBloc extends Bloc with BlocErrorHandler, SafeBloc { - OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._dataConnect) + OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._service) : super(OneTimeOrderState.initial()) { on(_onVendorsLoaded); on(_onVendorChanged); @@ -28,13 +28,13 @@ class OneTimeOrderBloc extends Bloc _loadHubs(); } final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase; - final dc.ExampleConnector _dataConnect; + final dc.DataConnectService _service; Future _loadVendors() async { final List? vendors = await handleErrorWithResult( action: () async { final QueryResult result = - await _dataConnect.listVendors().execute(); + await _service.connector.listVendors().execute(); return result.data.vendors .map( (dc.ListVendorsVendors vendor) => Vendor( @@ -57,7 +57,7 @@ class OneTimeOrderBloc extends Bloc final List? roles = await handleErrorWithResult( action: () async { final QueryResult - result = await _dataConnect.listRolesByVendorId(vendorId: vendorId).execute(); + result = await _service.connector.listRolesByVendorId(vendorId: vendorId).execute(); return result.data.roles .map( (dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption( @@ -79,13 +79,9 @@ class OneTimeOrderBloc extends Bloc Future _loadHubs() async { final List? hubs = await handleErrorWithResult( action: () async { - final String? businessId = - dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return []; - } + final String businessId = await _service.getBusinessId(); final QueryResult - result = await _dataConnect + result = await _service.connector .listTeamHubsByOwnerId(ownerId: businessId) .execute(); return result.data.teamHubs