feat: complete centralized error handling system with documentation

This commit is contained in:
2026-02-11 10:36:08 +05:30
parent 7570ffa3b9
commit 3e212220c7
43 changed files with 1144 additions and 2858 deletions

View File

@@ -10,7 +10,9 @@ import '../../domain/repositories/certificates_repository.dart';
///
/// This class handles the communication with the backend via [ExampleConnector].
/// It maps raw generated data types to clean [domain.StaffDocument] entities.
class CertificatesRepositoryImpl implements CertificatesRepository {
class CertificatesRepositoryImpl
with DataErrorHandler
implements CertificatesRepository {
/// The generated Data Connect SDK client.
final ExampleConnector _dataConnect;
@@ -24,16 +26,17 @@ class CertificatesRepositoryImpl implements CertificatesRepository {
required ExampleConnector dataConnect,
required FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
_firebaseAuth = firebaseAuth;
@override
Future<List<domain.StaffDocument>> getCertificates() async {
final User? currentUser = _firebaseAuth.currentUser;
if (currentUser == null) {
throw Exception('User not authenticated');
}
return executeProtected(() async {
final User? currentUser = _firebaseAuth.currentUser;
if (currentUser == null) {
throw domain.NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
try {
// Execute the query via DataConnect generated SDK
final QueryResult<ListStaffDocumentsByStaffIdData,
ListStaffDocumentsByStaffIdVariables> result =
@@ -46,10 +49,7 @@ class CertificatesRepositoryImpl implements CertificatesRepository {
.map((ListStaffDocumentsByStaffIdStaffDocuments doc) =>
_mapToDomain(doc))
.toList();
} catch (e) {
// In a real app, we would map specific exceptions to domain Failures here.
throw Exception('Failed to fetch certificates: $e');
}
});
}
/// Maps the Data Connect [ListStaffDocumentsByStaffIdStaffDocuments] to a domain [domain.StaffDocument].

View File

@@ -35,7 +35,19 @@ class CertificatesPage extends StatelessWidget {
if (state.status == CertificatesStatus.failure) {
return Scaffold(
body: Center(child: Text('Error: ${state.errorMessage}')),
appBar: AppBar(title: const Text('Certificates')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
state.errorMessage != null
? translateErrorKey(state.errorMessage!)
: 'Error loading certificates',
textAlign: TextAlign.center,
style: const TextStyle(color: UiColors.textSecondary),
),
),
),
);
}

View File

@@ -7,7 +7,9 @@ import 'package:krow_core/core.dart';
import '../../domain/repositories/documents_repository.dart';
/// Implementation of [DocumentsRepository] using Data Connect.
class DocumentsRepositoryImpl implements DocumentsRepository {
class DocumentsRepositoryImpl
with DataErrorHandler
implements DocumentsRepository {
final ExampleConnector _dataConnect;
final FirebaseAuth _firebaseAuth;
@@ -19,10 +21,12 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
@override
Future<List<domain.StaffDocument>> getDocuments() async {
final User? currentUser = _firebaseAuth.currentUser;
if (currentUser == null) {
throw Exception('User not authenticated');
}
return executeProtected(() async {
final User? currentUser = _firebaseAuth.currentUser;
if (currentUser == null) {
throw domain.NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
/// MOCK IMPLEMENTATION
/// To be replaced with real data connect query when available
@@ -49,22 +53,7 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
),
];
/*
try {
final QueryResult<ListStaffDocumentsByStaffIdData,
ListStaffDocumentsByStaffIdVariables> result =
await _dataConnect
.listStaffDocumentsByStaffId(staffId: currentUser.uid)
.execute();
return result.data.staffDocuments
.map((ListStaffDocumentsByStaffIdStaffDocuments doc) =>
_mapToDomain(doc))
.toList();
} catch (e) {
throw Exception('Failed to fetch documents: $e');
}
*/
});
}
domain.StaffDocument _mapToDomain(

View File

@@ -52,11 +52,17 @@ class DocumentsPage extends StatelessWidget {
}
if (state.status == DocumentsStatus.failure) {
return Center(
child: Text(
t.staff_documents.list.error(
message: state.errorMessage ?? 'Unknown',
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
state.errorMessage != null
? (state.errorMessage!.contains('errors.')
? translateErrorKey(state.errorMessage!)
: t.staff_documents.list.error(message: state.errorMessage!))
: t.staff_documents.list.error(message: 'Unknown'),
textAlign: TextAlign.center,
style: UiTypography.body1m.copyWith(color: UiColors.textSecondary),
),
style: UiTypography.body1m.copyWith(color: UiColors.textError),
),
);
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
@@ -8,7 +9,9 @@ import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/tax_forms_repository.dart';
import '../mappers/tax_form_mapper.dart';
class TaxFormsRepositoryImpl implements TaxFormsRepository {
class TaxFormsRepositoryImpl
with dc.DataErrorHandler
implements TaxFormsRepository {
TaxFormsRepositoryImpl({
required this.firebaseAuth,
required this.dataConnect,
@@ -21,46 +24,58 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
String _getStaffId() {
final auth.User? user = firebaseAuth.currentUser;
if (user == null) {
throw Exception('User not authenticated');
throw const NotAuthenticatedException(
technicalMessage: 'Firebase User is null',
);
}
final String? staffId = dc.StaffSessionStore.instance.session?.staff?.id;
if (staffId == null || staffId.isEmpty) {
throw Exception('Staff profile is missing or session not initialized.');
throw const StaffProfileNotFoundException(
technicalMessage: 'Staff ID missing in SessionStore',
);
}
return staffId;
}
@override
Future<List<TaxForm>> getTaxForms() async {
final String staffId = _getStaffId();
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables>
result =
await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute();
final List<TaxForm> forms = result.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
// Check if required forms exist, create if not.
final Set<TaxFormType> typesPresent = forms.map((TaxForm f) => f.type).toSet();
bool createdNew = false;
if (!typesPresent.contains(TaxFormType.i9)) {
await _createInitialForm(staffId, TaxFormType.i9);
createdNew = true;
}
if (!typesPresent.contains(TaxFormType.w4)) {
await _createInitialForm(staffId, TaxFormType.w4);
createdNew = true;
}
if (createdNew) {
return executeProtected(() async {
final String staffId = _getStaffId();
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables>
result2 =
await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute();
return result2.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
}
result = await dataConnect
.getTaxFormsByStaffId(staffId: staffId)
.execute();
return forms;
final List<TaxForm> forms =
result.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
// Check if required forms exist, create if not.
final Set<TaxFormType> typesPresent =
forms.map((TaxForm f) => f.type).toSet();
bool createdNew = false;
if (!typesPresent.contains(TaxFormType.i9)) {
await _createInitialForm(staffId, TaxFormType.i9);
createdNew = true;
}
if (!typesPresent.contains(TaxFormType.w4)) {
await _createInitialForm(staffId, TaxFormType.w4);
createdNew = true;
}
if (createdNew) {
final QueryResult<
dc.GetTaxFormsByStaffIdData,
dc.GetTaxFormsByStaffIdVariables> result2 =
await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute();
return result2.data.taxForms
.map(TaxFormMapper.fromDataConnect)
.toList();
}
return forms;
});
}
Future<void> _createInitialForm(String staffId, TaxFormType type) async {
@@ -80,45 +95,62 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
@override
Future<void> updateI9Form(I9TaxForm form) async {
final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id);
_mapCommonFields(builder, data);
_mapI9Fields(builder, data);
await builder.execute();
return executeProtected(() async {
final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder =
dataConnect.updateTaxForm(id: form.id);
_mapCommonFields(builder, data);
_mapI9Fields(builder, data);
await builder.execute();
});
}
@override
Future<void> submitI9Form(I9TaxForm form) async {
final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id);
_mapCommonFields(builder, data);
_mapI9Fields(builder, data);
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
return executeProtected(() async {
final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder =
dataConnect.updateTaxForm(id: form.id);
_mapCommonFields(builder, data);
_mapI9Fields(builder, data);
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
});
}
@override
Future<void> updateW4Form(W4TaxForm form) async {
final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id);
_mapCommonFields(builder, data);
_mapW4Fields(builder, data);
await builder.execute();
return executeProtected(() async {
final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder =
dataConnect.updateTaxForm(id: form.id);
_mapCommonFields(builder, data);
_mapW4Fields(builder, data);
await builder.execute();
});
}
@override
Future<void> submitW4Form(W4TaxForm form) async {
final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id);
_mapCommonFields(builder, data);
_mapW4Fields(builder, data);
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
return executeProtected(() async {
final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder =
dataConnect.updateTaxForm(id: form.id);
_mapCommonFields(builder, data);
_mapW4Fields(builder, data);
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
});
}
void _mapCommonFields(dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
if (data.containsKey('firstName')) builder.firstName(data['firstName'] as String?);
if (data.containsKey('lastName')) builder.lastName(data['lastName'] as String?);
if (data.containsKey('middleInitial')) builder.mInitial(data['middleInitial'] as String?);
if (data.containsKey('otherLastNames')) builder.oLastName(data['otherLastNames'] as String?);
void _mapCommonFields(
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
if (data.containsKey('firstName'))
builder.firstName(data['firstName'] as String?);
if (data.containsKey('lastName'))
builder.lastName(data['lastName'] as String?);
if (data.containsKey('middleInitial'))
builder.mInitial(data['middleInitial'] as String?);
if (data.containsKey('otherLastNames'))
builder.oLastName(data['otherLastNames'] as String?);
if (data.containsKey('dob')) {
final String dob = data['dob'] as String;
// Handle both ISO string and MM/dd/yyyy manual entry
@@ -131,8 +163,8 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
final List<String> parts = dob.split('/');
if (parts.length == 3) {
date = DateTime(
int.parse(parts[2]),
int.parse(parts[0]),
int.parse(parts[2]),
int.parse(parts[0]),
int.parse(parts[1]),
);
}
@@ -145,70 +177,90 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
}
}
if (data.containsKey('ssn') && data['ssn']?.toString().isNotEmpty == true) {
builder.socialSN(int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ?? 0);
builder.socialSN(
int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ??
0);
}
if (data.containsKey('email')) builder.email(data['email'] as String?);
if (data.containsKey('phone')) builder.phone(data['phone'] as String?);
if (data.containsKey('address')) builder.address(data['address'] as String?);
if (data.containsKey('aptNumber')) builder.apt(data['aptNumber'] as String?);
if (data.containsKey('address'))
builder.address(data['address'] as String?);
if (data.containsKey('aptNumber'))
builder.apt(data['aptNumber'] as String?);
if (data.containsKey('city')) builder.city(data['city'] as String?);
if (data.containsKey('state')) builder.state(data['state'] as String?);
if (data.containsKey('zipCode')) builder.zipCode(data['zipCode'] as String?);
if (data.containsKey('zipCode'))
builder.zipCode(data['zipCode'] as String?);
}
void _mapI9Fields(dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
void _mapI9Fields(
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
if (data.containsKey('citizenshipStatus')) {
final String status = data['citizenshipStatus'] as String;
// Map string to enum if possible, or handle otherwise.
// Generated enum: CITIZEN, NONCITIZEN_NATIONAL, PERMANENT_RESIDENT, ALIEN_AUTHORIZED
try {
builder.citizen(dc.CitizenshipStatus.values.byName(status.toUpperCase()));
builder.citizen(
dc.CitizenshipStatus.values.byName(status.toUpperCase()));
} catch (_) {}
}
if (data.containsKey('uscisNumber')) builder.uscis(data['uscisNumber'] as String?);
if (data.containsKey('passportNumber')) builder.passportNumber(data['passportNumber'] as String?);
if (data.containsKey('countryIssuance')) builder.countryIssue(data['countryIssuance'] as String?);
if (data.containsKey('preparerUsed')) builder.prepartorOrTranslator(data['preparerUsed'] as bool?);
if (data.containsKey('signature')) builder.signature(data['signature'] as String?);
if (data.containsKey('uscisNumber'))
builder.uscis(data['uscisNumber'] as String?);
if (data.containsKey('passportNumber'))
builder.passportNumber(data['passportNumber'] as String?);
if (data.containsKey('countryIssuance'))
builder.countryIssue(data['countryIssuance'] as String?);
if (data.containsKey('preparerUsed'))
builder.prepartorOrTranslator(data['preparerUsed'] as bool?);
if (data.containsKey('signature'))
builder.signature(data['signature'] as String?);
// Note: admissionNumber not in builder based on file read
}
void _mapW4Fields(dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
void _mapW4Fields(
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
if (data.containsKey('cityStateZip')) {
final String csz = data['cityStateZip'] as String;
// Extremely basic split: City, State Zip
final List<String> parts = csz.split(',');
if (parts.length >= 2) {
builder.city(parts[0].trim());
final String stateZip = parts[1].trim();
final List<String> szParts = stateZip.split(' ');
if (szParts.isNotEmpty) builder.state(szParts[0]);
if (szParts.length > 1) builder.zipCode(szParts.last);
}
final String csz = data['cityStateZip'] as String;
// Extremely basic split: City, State Zip
final List<String> parts = csz.split(',');
if (parts.length >= 2) {
builder.city(parts[0].trim());
final String stateZip = parts[1].trim();
final List<String> szParts = stateZip.split(' ');
if (szParts.isNotEmpty) builder.state(szParts[0]);
if (szParts.length > 1) builder.zipCode(szParts.last);
}
}
if (data.containsKey('filingStatus')) {
// MARITIAL_STATUS_SINGLE, MARITIAL_STATUS_MARRIED, MARITIAL_STATUS_HEAD
try {
final String status = data['filingStatus'] as String;
// Simple mapping assumptions:
if (status.contains('single')) builder.marital(dc.MaritalStatus.SINGLE);
else if (status.contains('married')) builder.marital(dc.MaritalStatus.MARRIED);
else if (status.contains('head')) builder.marital(dc.MaritalStatus.HEAD);
} catch (_) {}
try {
final String status = data['filingStatus'] as String;
// Simple mapping assumptions:
if (status.contains('single')) builder.marital(dc.MaritalStatus.SINGLE);
else if (status.contains('married'))
builder.marital(dc.MaritalStatus.MARRIED);
else if (status.contains('head'))
builder.marital(dc.MaritalStatus.HEAD);
} catch (_) {}
}
if (data.containsKey('multipleJobs')) builder.multipleJob(data['multipleJobs'] as bool?);
if (data.containsKey('qualifyingChildren')) builder.childrens(data['qualifyingChildren'] as int?);
if (data.containsKey('otherDependents')) builder.otherDeps(data['otherDependents'] as int?);
if (data.containsKey('multipleJobs'))
builder.multipleJob(data['multipleJobs'] as bool?);
if (data.containsKey('qualifyingChildren'))
builder.childrens(data['qualifyingChildren'] as int?);
if (data.containsKey('otherDependents'))
builder.otherDeps(data['otherDependents'] as int?);
if (data.containsKey('otherIncome')) {
builder.otherInconme(double.tryParse(data['otherIncome'].toString()));
builder.otherInconme(double.tryParse(data['otherIncome'].toString()));
}
if (data.containsKey('deductions')) {
builder.deductions(double.tryParse(data['deductions'].toString()));
builder.deductions(double.tryParse(data['deductions'].toString()));
}
if (data.containsKey('extraWithholding')) {
builder.extraWithholding(double.tryParse(data['extraWithholding'].toString()));
builder.extraWithholding(
double.tryParse(data['extraWithholding'].toString()));
}
if (data.containsKey('signature')) builder.signature(data['signature'] as String?);
if (data.containsKey('signature'))
builder.signature(data['signature'] as String?);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -62,7 +63,15 @@ class TaxFormsPage extends StatelessWidget {
if (state.status == TaxFormsStatus.failure) {
return Center(
child: Text(state.errorMessage ?? 'Error loading forms'),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
child: Text(
state.errorMessage != null
? translateErrorKey(state.errorMessage!)
: 'Error loading forms',
textAlign: TextAlign.center,
),
),
);
}