Merge pull request #427 from Oloodi/408-feature-implement-paidunpaid-breaks---client-app-frontend-development

DataConnectService is now partially enbaled in the client application
This commit is contained in:
Achintha Isuru
2026-02-16 17:36:15 -05:00
committed by GitHub
8 changed files with 344 additions and 351 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 @@
library; library;
import 'package:firebase_auth/firebase_auth.dart' as firebase;
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 'package:krow_data_connect/krow_data_connect.dart';
@@ -28,12 +27,7 @@ class ClientAuthenticationModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repositories // Repositories
i.addLazySingleton<AuthRepositoryInterface>( i.addLazySingleton<AuthRepositoryInterface>(AuthRepositoryImpl.new);
() => AuthRepositoryImpl(
firebaseAuth: firebase.FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
// UseCases // UseCases
i.addLazySingleton( i.addLazySingleton(

View File

@@ -12,7 +12,6 @@ import 'package:krow_domain/krow_domain.dart'
AccountExistsException, AccountExistsException,
UserNotFoundException, UserNotFoundException,
UnauthorizedAppException, UnauthorizedAppException,
UnauthorizedAppException,
PasswordMismatchException, PasswordMismatchException,
NetworkException; NetworkException;
import 'package:krow_domain/krow_domain.dart' as domain; 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 /// This implementation integrates with Firebase Authentication for user
/// identity management and Krow's Data Connect SDK for storing user profile data. /// identity management and Krow's Data Connect SDK for storing user profile data.
class AuthRepositoryImpl class AuthRepositoryImpl implements AuthRepositoryInterface {
with dc.DataErrorHandler
implements AuthRepositoryInterface {
/// Creates an [AuthRepositoryImpl] with the real dependencies. /// Creates an [AuthRepositoryImpl] with the real dependencies.
AuthRepositoryImpl({ AuthRepositoryImpl({
required firebase.FirebaseAuth firebaseAuth, dc.DataConnectService? service,
required dc.ExampleConnector dataConnect, }) : _service = service ?? dc.DataConnectService.instance;
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect; final dc.DataConnectService _service;
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
@override @override
Future<domain.User> signInWithEmail({ Future<domain.User> signInWithEmail({
@@ -42,7 +36,8 @@ class AuthRepositoryImpl
required String password, required String password,
}) async { }) async {
try { try {
final firebase.UserCredential credential = await _firebaseAuth.signInWithEmailAndPassword( final firebase.UserCredential credential =
await _service.auth.signInWithEmailAndPassword(
email: email, email: email,
password: password, password: password,
); );
@@ -59,7 +54,6 @@ class AuthRepositoryImpl
fallbackEmail: firebaseUser.email ?? email, fallbackEmail: firebaseUser.email ?? email,
requireBusinessRole: true, requireBusinessRole: true,
); );
} on firebase.FirebaseAuthException catch (e) { } on firebase.FirebaseAuthException catch (e) {
if (e.code == 'invalid-credential' || e.code == 'wrong-password') { if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
throw InvalidCredentialsException( throw InvalidCredentialsException(
@@ -94,7 +88,8 @@ class AuthRepositoryImpl
try { try {
// Step 1: Try to create Firebase Auth user // 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, email: email,
password: password, password: password,
); );
@@ -111,9 +106,9 @@ class AuthRepositoryImpl
firebaseUser: firebaseUser, firebaseUser: firebaseUser,
companyName: companyName, companyName: companyName,
email: email, email: email,
onBusinessCreated: (String businessId) => createdBusinessId = businessId, onBusinessCreated: (String businessId) =>
createdBusinessId = businessId,
); );
} on firebase.FirebaseAuthException catch (e) { } on firebase.FirebaseAuthException catch (e) {
if (e.code == 'weak-password') { if (e.code == 'weak-password') {
throw WeakPasswordException( throw WeakPasswordException(
@@ -137,11 +132,13 @@ class AuthRepositoryImpl
} }
} on domain.AppException { } on domain.AppException {
// Rollback for our known exceptions // Rollback for our known exceptions
await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId); await _rollbackSignUp(
firebaseUser: firebaseUser, businessId: createdBusinessId);
rethrow; rethrow;
} catch (e) { } catch (e) {
// Rollback: Clean up any partially created resources // Rollback: Clean up any partially created resources
await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId); await _rollbackSignUp(
firebaseUser: firebaseUser, businessId: createdBusinessId);
throw SignUpFailedException( throw SignUpFailedException(
technicalMessage: 'Unexpected error: $e', technicalMessage: 'Unexpected error: $e',
); );
@@ -164,11 +161,13 @@ class AuthRepositoryImpl
required String password, required String password,
required String companyName, required String companyName,
}) async { }) 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 {
// Try to sign in with the provided password // 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, email: email,
password: password, password: password,
); );
@@ -181,28 +180,32 @@ class AuthRepositoryImpl
} }
// Sign-in succeeded! Check if user already has a BUSINESS account in PostgreSQL // 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) { if (hasBusinessAccount) {
// User already has a KROW Client account // 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( throw AccountExistsException(
technicalMessage: 'User ${firebaseUser.uid} already has BUSINESS role', technicalMessage: 'User ${firebaseUser.uid} already has BUSINESS role',
); );
} }
// User exists in Firebase but not in KROW PostgreSQL - create the entities // 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( return await _createBusinessAndUser(
firebaseUser: firebaseUser, firebaseUser: firebaseUser,
companyName: companyName, companyName: companyName,
email: email, email: email,
onBusinessCreated: (_) {}, // No rollback needed for existing Firebase user onBusinessCreated: (_) {}, // No rollback needed for existing Firebase user
); );
} on firebase.FirebaseAuthException catch (e) { } on firebase.FirebaseAuthException catch (e) {
// Sign-in failed - check why // 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') { if (e.code == 'wrong-password' || e.code == 'invalid-credential') {
// Password doesn't match - check what providers are available // 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" // We can't distinguish between "wrong password" and "no password provider"
// due to Firebase deprecating fetchSignInMethodsForEmail. // due to Firebase deprecating fetchSignInMethodsForEmail.
// The PasswordMismatchException message covers both scenarios. // 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( 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<bool> _checkBusinessUserExists(String firebaseUserId) async { Future<bool> _checkBusinessUserExists(String firebaseUserId) async {
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response = final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute()); await _service.run(
() => _service.connector.getUserById(id: firebaseUserId).execute());
final dc.GetUserByIdUser? user = response.data.user; 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. /// Creates Business and User entities in PostgreSQL for a Firebase user.
@@ -250,38 +257,44 @@ class AuthRepositoryImpl
}) async { }) async {
// Create Business entity in PostgreSQL // Create Business entity in PostgreSQL
final OperationResult<dc.CreateBusinessData, dc.CreateBusinessVariables> createBusinessResponse = final OperationResult<dc.CreateBusinessData, dc.CreateBusinessVariables>
await executeProtected(() => _dataConnect.createBusiness( createBusinessResponse = await _service.run(() => _service.connector
businessName: companyName, .createBusiness(
userId: firebaseUser.uid, businessName: companyName,
rateGroup: dc.BusinessRateGroup.STANDARD, userId: firebaseUser.uid,
status: dc.BusinessStatus.PENDING, rateGroup: dc.BusinessRateGroup.STANDARD,
).execute()); status: dc.BusinessStatus.PENDING,
)
.execute());
final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert; final dc.CreateBusinessBusinessInsert businessData =
createBusinessResponse.data.business_insert;
onBusinessCreated(businessData.id); onBusinessCreated(businessData.id);
// Check if User entity already exists in PostgreSQL // Check if User entity already exists in PostgreSQL
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> userResult = final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> 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; final dc.GetUserByIdUser? existingUser = userResult.data.user;
if (existingUser != null) { if (existingUser != null) {
// User exists (likely in another app like STAFF). Update role to BOTH. // User exists (likely in another app like STAFF). Update role to BOTH.
await executeProtected(() => _dataConnect.updateUser( await _service.run(() => _service.connector
id: firebaseUser.uid, .updateUser(
) id: firebaseUser.uid,
.userRole('BOTH') )
.execute()); .userRole('BOTH')
.execute());
} else { } else {
// Create new User entity in PostgreSQL // Create new User entity in PostgreSQL
await executeProtected(() => _dataConnect.createUser( await _service.run(() => _service.connector
id: firebaseUser.uid, .createUser(
role: dc.UserBaseRole.USER, id: firebaseUser.uid,
) role: dc.UserBaseRole.USER,
.email(email) )
.userRole('BUSINESS') .email(email)
.execute()); .userRole('BUSINESS')
.execute());
} }
return _getUserProfile( return _getUserProfile(
@@ -298,7 +311,7 @@ class AuthRepositoryImpl
// Delete business first (if created) // Delete business first (if created)
if (businessId != null) { if (businessId != null) {
try { try {
await _dataConnect.deleteBusiness(id: businessId).execute(); await _service.connector.deleteBusiness(id: businessId).execute();
} catch (_) { } catch (_) {
// Log but don't throw - we're already in error recovery // Log but don't throw - we're already in error recovery
} }
@@ -316,8 +329,9 @@ class AuthRepositoryImpl
@override @override
Future<void> signOut() async { Future<void> signOut() async {
try { try {
await _firebaseAuth.signOut(); await _service.auth.signOut();
dc.ClientSessionStore.instance.clear(); dc.ClientSessionStore.instance.clear();
_service.clearCache();
} catch (e) { } catch (e) {
throw Exception('Error signing out: ${e.toString()}'); throw Exception('Error signing out: ${e.toString()}');
} }
@@ -325,7 +339,8 @@ class AuthRepositoryImpl
@override @override
Future<domain.User> signInWithSocial({required String provider}) { Future<domain.User> 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<domain.User> _getUserProfile({ Future<domain.User> _getUserProfile({
@@ -334,18 +349,24 @@ class AuthRepositoryImpl
bool requireBusinessRole = false, bool requireBusinessRole = false,
}) async { }) async {
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response = final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute()); await _service.run(() =>
_service.connector.getUserById(id: firebaseUserId).execute());
final dc.GetUserByIdUser? user = response.data.user; final dc.GetUserByIdUser? user = response.data.user;
if (user == null) { if (user == null) {
throw UserNotFoundException( 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') { if (requireBusinessRole &&
await _firebaseAuth.signOut(); user.userRole != 'BUSINESS' &&
user.userRole != 'BOTH') {
await _service.auth.signOut();
dc.ClientSessionStore.instance.clear(); dc.ClientSessionStore.instance.clear();
_service.clearCache();
throw UnauthorizedAppException( 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, role: user.role.stringValue,
); );
final QueryResult<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> businessResponse = final QueryResult<dc.GetBusinessesByUserIdData,
await executeProtected(() => _dataConnect.getBusinessesByUserId( dc.GetBusinessesByUserIdVariables> businessResponse =
userId: firebaseUserId, await _service.run(() => _service.connector
).execute()); .getBusinessesByUserId(
final dc.GetBusinessesByUserIdBusinesses? business = businessResponse.data.businesses.isNotEmpty userId: firebaseUserId,
? businessResponse.data.businesses.first )
: null; .execute());
final dc.GetBusinessesByUserIdBusinesses? business =
businessResponse.data.businesses.isNotEmpty
? businessResponse.data.businesses.first
: null;
dc.ClientSessionStore.instance.setSession( dc.ClientSessionStore.instance.setSession(
dc.ClientSession( dc.ClientSession(

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)!,
); );
} }

View File

@@ -2,7 +2,6 @@ import 'package:flutter/widgets.dart';
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 '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 'data/repositories_impl/client_create_order_repository_impl.dart';
import 'domain/repositories/client_create_order_repository_interface.dart'; import 'domain/repositories/client_create_order_repository_interface.dart';
import 'domain/usecases/create_one_time_order_usecase.dart'; import 'domain/usecases/create_one_time_order_usecase.dart';
@@ -29,12 +28,7 @@ class ClientCreateOrderModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repositories // Repositories
i.addLazySingleton<ClientCreateOrderRepositoryInterface>( i.addLazySingleton<ClientCreateOrderRepositoryInterface>(ClientCreateOrderRepositoryImpl.new);
() => ClientCreateOrderRepositoryImpl(
firebaseAuth: firebase.FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
// UseCases // UseCases
i.addLazySingleton(GetOrderTypesUseCase.new); i.addLazySingleton(GetOrderTypesUseCase.new);
@@ -44,12 +38,7 @@ class ClientCreateOrderModule extends Module {
// BLoCs // BLoCs
i.add<ClientCreateOrderBloc>(ClientCreateOrderBloc.new); i.add<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
i.add<RapidOrderBloc>(RapidOrderBloc.new); i.add<RapidOrderBloc>(RapidOrderBloc.new);
i.add<OneTimeOrderBloc>( i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
() => OneTimeOrderBloc(
i.get<CreateOneTimeOrderUseCase>(),
ExampleConnector.instance,
),
);
} }
@override @override

View File

@@ -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:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc; 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 /// It follows the KROW Clean Architecture by keeping the data layer focused
/// on delegation and data mapping, without business logic. /// on delegation and data mapping, without business logic.
class ClientCreateOrderRepositoryImpl class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInterface {
with dc.DataErrorHandler
implements ClientCreateOrderRepositoryInterface {
ClientCreateOrderRepositoryImpl({ ClientCreateOrderRepositoryImpl({
required firebase.FirebaseAuth firebaseAuth, required dc.DataConnectService service,
required dc.ExampleConnector dataConnect, }) : _service = service;
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
final firebase.FirebaseAuth _firebaseAuth; final dc.DataConnectService _service;
final dc.ExampleConnector _dataConnect;
@override @override
Future<List<domain.OrderType>> getOrderTypes() { Future<List<domain.OrderType>> getOrderTypes() {
return Future.value(const <domain.OrderType>[ return Future<List<domain.OrderType>>.value(const <domain.OrderType>[
domain.OrderType( domain.OrderType(
id: 'one-time', id: 'one-time',
titleKey: 'client_create_order.types.one_time', titleKey: 'client_create_order.types.one_time',
@@ -55,100 +49,95 @@ class ClientCreateOrderRepositoryImpl
@override @override
Future<void> createOneTimeOrder(domain.OneTimeOrder order) async { Future<void> createOneTimeOrder(domain.OneTimeOrder order) async {
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; return _service.run(() async {
if (businessId == null || businessId.isEmpty) { final String businessId = await _service.getBusinessId();
await _firebaseAuth.signOut(); final String? vendorId = order.vendorId;
throw Exception('Business is missing. Please sign in again.'); if (vendorId == null || vendorId.isEmpty) {
} throw Exception('Vendor is missing.');
final String? vendorId = order.vendorId; }
if (vendorId == null || vendorId.isEmpty) { final domain.OneTimeOrderHubDetails? hub = order.hub;
throw Exception('Vendor is missing.'); if (hub == null || hub.id.isEmpty) {
} throw Exception('Hub is missing.');
final domain.OneTimeOrderHubDetails? hub = order.hub; }
if (hub == null || hub.id.isEmpty) {
throw Exception('Hub is missing.');
}
final DateTime orderDateOnly = DateTime( final DateTime orderDateOnly = DateTime(
order.date.year, order.date.year,
order.date.month, order.date.month,
order.date.day, order.date.day,
); );
final fdc.Timestamp orderTimestamp = _toTimestamp(orderDateOnly); final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
orderResult = await executeProtected(() => _dataConnect await _service.connector
.createOrder( .createOrder(
businessId: businessId, businessId: businessId,
orderType: dc.OrderType.ONE_TIME, orderType: dc.OrderType.ONE_TIME,
teamHubId: hub.id, 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<int>(
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<dc.CreateShiftData, dc.CreateShiftVariables> 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) .startTime(_service.toTimestamp(start))
.eventName(order.eventName) .endTime(_service.toTimestamp(normalizedEnd))
.status(dc.OrderStatus.POSTED) .hours(hours)
.date(orderTimestamp) .breakType(_breakDurationFromValue(position.lunchBreak))
.execute()); .isBreakPaid(_isBreakPaid(position.lunchBreak))
.totalValue(totalValue)
.execute();
}
final String orderId = orderResult.data.order_insert.id; await _service.connector
.updateOrder(id: orderId, teamHubId: hub.id)
final int workersNeeded = order.positions.fold<int>( .shifts(fdc.AnyValue(<String>[shiftId]))
0, .execute();
(int sum, domain.OneTimeOrderPosition position) => sum + position.count, });
);
final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}';
final double shiftCost = _calculateShiftCost(order);
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
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(<String>[shiftId]))
.execute());
} }
@override @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) { String _formatDate(DateTime dateTime) {
final String year = dateTime.year.toString().padLeft(4, '0'); final String year = dateTime.year.toString().padLeft(4, '0');
final String month = dateTime.month.toString().padLeft(2, '0'); final String month = dateTime.month.toString().padLeft(2, '0');

View File

@@ -11,7 +11,7 @@ import 'one_time_order_state.dart';
/// BLoC for managing the multi-step one-time order creation form. /// BLoC for managing the multi-step one-time order creation form.
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
with BlocErrorHandler<OneTimeOrderState>, SafeBloc<OneTimeOrderEvent, OneTimeOrderState> { with BlocErrorHandler<OneTimeOrderState>, SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._dataConnect) OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._service)
: super(OneTimeOrderState.initial()) { : super(OneTimeOrderState.initial()) {
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded); on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
on<OneTimeOrderVendorChanged>(_onVendorChanged); on<OneTimeOrderVendorChanged>(_onVendorChanged);
@@ -28,13 +28,13 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
_loadHubs(); _loadHubs();
} }
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase; final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
final dc.ExampleConnector _dataConnect; final dc.DataConnectService _service;
Future<void> _loadVendors() async { Future<void> _loadVendors() async {
final List<Vendor>? vendors = await handleErrorWithResult( final List<Vendor>? vendors = await handleErrorWithResult(
action: () async { action: () async {
final QueryResult<dc.ListVendorsData, void> result = final QueryResult<dc.ListVendorsData, void> result =
await _dataConnect.listVendors().execute(); await _service.connector.listVendors().execute();
return result.data.vendors return result.data.vendors
.map( .map(
(dc.ListVendorsVendors vendor) => Vendor( (dc.ListVendorsVendors vendor) => Vendor(
@@ -57,7 +57,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
final List<OneTimeOrderRoleOption>? roles = await handleErrorWithResult( final List<OneTimeOrderRoleOption>? roles = await handleErrorWithResult(
action: () async { action: () async {
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables> final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
result = await _dataConnect.listRolesByVendorId(vendorId: vendorId).execute(); result = await _service.connector.listRolesByVendorId(vendorId: vendorId).execute();
return result.data.roles return result.data.roles
.map( .map(
(dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption( (dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption(
@@ -79,13 +79,9 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
Future<void> _loadHubs() async { Future<void> _loadHubs() async {
final List<OneTimeOrderHubOption>? hubs = await handleErrorWithResult( final List<OneTimeOrderHubOption>? hubs = await handleErrorWithResult(
action: () async { action: () async {
final String? businessId = final String businessId = await _service.getBusinessId();
dc.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
return <OneTimeOrderHubOption>[];
}
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables> final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
result = await _dataConnect result = await _service.connector
.listTeamHubsByOwnerId(ownerId: businessId) .listTeamHubsByOwnerId(ownerId: businessId)
.execute(); .execute();
return result.data.teamHubs return result.data.teamHubs