feat: Migrate staff profile features from Data Connect to V2 REST API

- Removed data_connect package from mobile pubspec.yaml.
- Added documentation for V2 profile migration status and QA findings.
- Implemented new session management with ClientSessionStore and StaffSessionStore.
- Created V2SessionService for handling user sessions via the V2 API.
- Developed use cases for cancelling late worker assignments and submitting worker reviews.
- Added arguments and use cases for payment chart retrieval and profile completion checks.
- Implemented repository interfaces and their implementations for staff main and profile features.
- Ensured proper error handling and validation in use cases.
This commit is contained in:
Achintha Isuru
2026-03-16 22:45:06 -04:00
parent 4834266986
commit b31a615092
478 changed files with 10512 additions and 19854 deletions

View File

@@ -52,4 +52,175 @@
- BenefitsOverviewPage also has CircularProgressIndicator (not shimmer-ified yet)
- ShiftDetailsPage has a dialog-level spinner in the "applying" dialog -- this is intentional, not a page loading state
- Hub details/edit pages use CircularProgressIndicator as action overlays (save/delete) -- keep as-is, not initial load
- Client home page has no loading spinner; it renders with default empty dashboard data
- Client home page uses shimmer skeleton during loading (ClientHomePageSkeleton + ClientHomeHeaderSkeleton)
## V2 API Migration Patterns
- `BaseApiService` is registered in `CoreModule` as a lazy singleton (injected as `i.get<BaseApiService>()`)
- `BaseApiService` type lives in `krow_domain`; `ApiService` impl lives in `krow_core`
- V2 endpoints: `V2ApiEndpoints.staffDashboard` etc. from `krow_core/core.dart`
- V2 domain shift entities: `TodayShift`, `AssignedShift`, `OpenShift` (separate from core `Shift`)
- V2 `Benefit`: uses `targetHours`/`trackedHours`/`remainingHours` (int) -- old used `entitlementHours`/`usedHours` (double)
- Staff dashboard endpoint returns all home data in one call (todaysShifts, tomorrowsShifts, recommendedShifts, benefits, staffName)
- Navigator has `toShiftDetailsById(String shiftId)` for cases where only the ID is available
- `StaffDashboard` entity updated to use typed lists: `List<TodayShift>`, `List<AssignedShift>`, `List<OpenShift>`
- Staff home feature migrated (Phase 2): removed krow_data_connect, firebase_data_connect, staff_shifts deps
- [V2 Profile Migration](project_v2_profile_migration.md) -- entity mappings and DI patterns for all profile sub-packages
- Staff clock-in migrated (Phase 3): repo impl → V2 API, removed Data Connect deps
- V2 `Shift` entity: `startsAt`/`endsAt` (DateTime), `locationName` (String?), no `startTime`/`endTime`/`clientName`/`hourlyRate`/`location`
- V2 `AttendanceStatus`: `isClockedIn` getter (not `isCheckedIn`), `clockInAt` (not `checkInTime`), no `checkOutTime`/`activeApplicationId`
- `AttendanceStatus` constructor requires `attendanceStatus: AttendanceStatusType.notClockedIn` for default
- Clock-out uses `shiftId` (not `applicationId`) -- V2 API resolves assignment from shiftId
- `listTodayShifts` endpoint returns `{ items: [...] }` with TodayShift-like shape (no lat/lng, hourlyRate, clientName)
- `getCurrentAttendanceStatus` returns flat object `{ activeShiftId, attendanceStatus, clockInAt }`
- Clock-in/out POST endpoints return `{ attendanceEventId, assignmentId, sessionId, status, validationStatus }` -- repo re-fetches status after
- Geofence: lat/lng not available from listTodayShifts or shiftDetail endpoints (lives on clock_points table, not returned by BE)
- `BaseApiService` not exported from `krow_core/core.dart` -- must import from `krow_domain/krow_domain.dart`
## Staff Shifts Feature Migration (Phase 3 -- completed)
- Migrated from `krow_data_connect` + `DataConnectService` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_auth`, `firebase_data_connect`, `geolocator`, `google_maps_flutter`, `meta`
- State uses 5 typed lists: `List<AssignedShift>`, `List<OpenShift>`, `List<PendingAssignment>`, `List<CancelledShift>`, `List<CompletedShift>`
- ShiftDetail (not Shift) used for detail page -- loaded by BLoC via API, not passed as route argument
- Money: `hourlyRateCents` (int) / 100 for display -- all V2 shift entities use cents
- Dates: All V2 entities have `DateTime` fields (not `String`) -- no more `DateTime.parse()` in widgets
- AssignmentStatus enum drives bottom bar logic (accepted=clock-in, assigned=accept/decline, null=apply)
- Old `Shift` entity still exists in domain but only used by clock-in feature -- shift list/detail pages use V2 entities
- ShiftDetailsModule route no longer receives `Shift` data argument -- uses `shiftId` param only
- `toShiftDetailsById(String)` is the standard navigation for V2 (no entity passing)
- Profile completion: moved into feature repo impl via `V2ApiEndpoints.staffProfileCompletion` + `ProfileCompletion.fromJson`
- Find Shifts tab: removed geolocator distance filter and multi-day grouping (V2 API handles server-side)
- Renamed use cases: `GetMyShiftsUseCase``GetAssignedShiftsUseCase`, `GetAvailableShiftsUseCase``GetOpenShiftsUseCase`, `GetHistoryShiftsUseCase``GetCompletedShiftsUseCase`, `GetShiftDetailsUseCase``GetShiftDetailUseCase`
## Client Home Feature Migration (Phase 4 -- completed)
- Migrated from `krow_data_connect` + `DataConnectService` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_data_connect`, `intl`
- V2 entities: `ClientDashboard` (replaces `HomeDashboardData`), `RecentOrder` (replaces `ReorderItem`)
- `ClientDashboard` contains nested `SpendingSummary`, `CoverageMetrics`, `LiveActivityMetrics`
- Money: `weeklySpendCents`, `projectedNext7DaysCents`, `averageShiftCostCents` (int) / 100 for display
- Two API calls: `GET /client/dashboard` (all metrics + user/biz info) and `GET /client/reorders` (returns `{ items: [...] }`)
- Removed `GetUserSessionDataUseCase` -- user/business info now part of `ClientDashboard`
- `LiveActivityWidget` rewritten from StatefulWidget with direct DC calls to StatelessWidget consuming BLoC state
- Dead code removed: `ShiftOrderFormSheet`, `ClientHomeSheets`, `CoverageDashboard` (all unused)
- `RecentOrder` entity: `id`, `title`, `date` (DateTime?), `hubName` (String?), `positionCount` (int), `orderType` (OrderType)
- Module imports `CoreModule()` (not `DataConnectModule()`), injects `BaseApiService` into repo
- State has `dashboard` (ClientDashboard?) with computed getters `businessName`, `userName`
- No photoUrl in V2 dashboard response -- header shows letter avatar only
## Client Billing Feature Migration (Phase 4 -- completed)
- Migrated from `krow_data_connect` + `BillingConnectorRepository` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_data_connect`
- Deleted old presentation models: `BillingInvoice`, `BillingWorkerRecord`, `SpendingBreakdownItem`
- V2 domain entities used directly: `Invoice`, `BillingAccount`, `SpendItem`, `CurrentBill`, `Savings`
- Old domain types removed: `BusinessBankAccount`, `InvoiceItem`, `InvoiceWorker`, `BillingPeriod` enum
- Money: all amounts in cents (int). State has computed `currentBillDollars`, `savingsDollars`, `spendTotalCents` getters
- `Invoice` V2 entity: `invoiceId`, `invoiceNumber`, `amountCents` (int), `status` (InvoiceStatus enum), `dueDate`, `paymentDate`, `vendorId`, `vendorName`
- `BillingAccount` V2 entity: `accountId`, `bankName`, `providerReference`, `last4`, `isPrimary`, `accountType` (AccountType enum)
- `SpendItem` V2 entity: `category`, `amountCents` (int), `percentage` (double) -- server-side aggregation by role
- Spend breakdown: replaced `BillingPeriod` enum with `BillingPeriodTab` (local) + `SpendBreakdownParams` (startDate/endDate ISO strings)
- API response shapes: list endpoints return `{ items: [...] }`, scalar endpoints spread data (`{ currentBillCents, requestId }`)
- Approve/dispute: POST to `V2ApiEndpoints.clientInvoiceApprove(id)` / `clientInvoiceDispute(id)`
- Completion review page: `BillingInvoice` replaced with `Invoice` -- worker-level data not available in V2 (widget placeholder)
- `InvoiceStatus` enum has `.value` property for display and `fromJson` factory with safe fallback to `unknown`
## Client Reports Feature Migration (Phase 4 -- completed)
- Migrated from `krow_data_connect` + `ReportsConnectorRepository` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`
- 7 report endpoints: summary, daily-ops, spend, coverage, forecast, performance, no-show
- Old `ReportsSummary` entity replaced with V2 `ReportSummary` (different fields: totalShifts, totalSpendCents, averageCoveragePercentage, averagePerformanceScore, noShowCount, forecastAccuracyPercentage)
- `businessId` removed from all events/repo -- V2 API resolves from auth token
- DailyOps: old `DailyOpsShift` replaced with `ShiftWithWorkers` (from coverage_domain). `TimeRange` has `startsAt`/`endsAt` (not `start`/`end`)
- Spend: `SpendReport` uses `totalSpendCents` (int), `chart` (List<SpendDataPoint> with `bucket`/`amountCents`), `breakdown` (List<SpendItem> with `category`/`amountCents`/`percentage`)
- Coverage: `CoverageReport` uses `averageCoveragePercentage`, `filledWorkers`, `neededWorkers`, `chart` (List<CoverageDayPoint> with `day`/`needed`/`filled`/`coveragePercentage`)
- Forecast: `ForecastReport` uses `forecastSpendCents`, `averageWeeklySpendCents`, `totalWorkerHours`, `weeks` (List<ForecastWeek> with `week`/`shiftCount`/`workerHours`/`forecastSpendCents`/`averageShiftCostCents`)
- Performance: V2 uses int percentages (`fillRatePercentage`, `completionRatePercentage`, `onTimeRatePercentage`) and `averageFillTimeMinutes` (double) -- convert to hours: `/60`
- NoShow: `NoShowReport` uses `totalNoShowCount`, `noShowRatePercentage`, `workersWhoNoShowed`, `items` (List<NoShowWorkerItem> with `staffId`/`staffName`/`incidentCount`/`riskStatus`/`incidents`)
- Module injects `BaseApiService` via `i.get<BaseApiService>()` -- no more `DataConnectModule` import
## Client Hubs Feature Migration (Phase 5 -- completed)
- Migrated from `krow_data_connect` + `HubsConnectorRepository` + `DataConnectService` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_auth`, `firebase_data_connect`, `http`
- V2 `Hub` entity: `hubId` (not `id`), `fullAddress` (not `address`), `costCenterId`/`costCenterName` (flat, not nested `CostCenter` object)
- V2 `CostCenter` entity: `costCenterId` (not `id`), `name` only (no `code` field)
- V2 `HubManager` entity: `managerAssignmentId`, `businessMembershipId`, `managerId`, `name`
- API response shapes: `GET /client/hubs` returns `{ items: [...] }`, `GET /client/cost-centers` returns `{ items: [...] }`
- Create/update return `{ hubId, created: true }` / `{ hubId, updated: true }` -- repo returns hubId String
- Delete: soft-delete (sets status=INACTIVE). Backend rejects if hub has active orders (409 HUB_DELETE_BLOCKED)
- Assign NFC: `POST /client/hubs/:hubId/assign-nfc` with `{ nfcTagId }`
- Module no longer imports `DataConnectModule()` -- `BaseApiService` available from parent `CoreModule()`
- `UpdateHubArguments.id` renamed to `UpdateHubArguments.hubId`; `CreateHubArguments.address` renamed to `.fullAddress`
- `HubDetailsDeleteRequested.id` renamed to `.hubId`; `EditHubAddRequested.address` renamed to `.fullAddress`
- Navigator still passes full `Hub` entity via route args (not just hubId)
## Client Orders Feature Migration (Phase 5 -- completed)
- 3 sub-packages migrated: `orders_common`, `view_orders`, `create_order`
- Removed deps: `krow_data_connect`, `firebase_data_connect`, `firebase_auth` from all; kept `intl` in create_order and orders_common
- V2 `OrderItem` entity: `itemId`, `orderId`, `orderType` (OrderType enum), `roleName`, `date` (DateTime), `startsAt`/`endsAt` (DateTime), `requiredWorkerCount`, `filledCount`, `hourlyRateCents`, `totalCostCents` (int cents), `locationName` (String?), `status` (ShiftStatus enum), `workers` (List<AssignedWorkerSummary>)
- Old entities deleted: `OneTimeOrder`, `RecurringOrder`, `PermanentOrder`, `ReorderData`, `OneTimeOrderHubDetails`, `RecurringOrderHubDetails`
- `AssignedWorkerSummary`: `applicationId` (String?), `workerName` (String? -- nullable!), `role` (String?), `confirmationStatus` (ApplicationStatus?)
- V2 `Vendor` entity: field is `companyName` (not `name`) -- old code used `vendor.name`
- V2 `ShiftStatus` enum: only has `draft`, `open`, `pendingConfirmation`, `assigned`, `active`, `completed`, `cancelled`, `unknown` -- no `filled`/`confirmed`/`pending`
- `OrderType` enum has `unknown` variant -- must handle in switch statements
- View orders: removed `GetAcceptedApplicationsForDayUseCase` -- V2 returns workers inline with order items
- View orders cubit: date filtering now uses `_isSameDay(DateTime, DateTime)` instead of string comparison
- Create order BLoCs: build `Map<String, dynamic>` V2 payloads instead of old entity objects
- V2 create endpoints: `POST /client/orders/one-time` (requires `orderDate`), `/recurring` (requires `startDate`/`endDate`/`recurrenceDays`), `/permanent` (requires `startDate`/`daysOfWeek`)
- V2 edit endpoint: `POST /client/orders/:orderId/edit` -- creates edited copy, cancels original
- V2 cancel endpoint: `POST /client/orders/:orderId/cancel` with optional `reason`
- Reorder uses `OrderPreview` (from `V2ApiEndpoints.clientOrderReorderPreview`) instead of old `ReorderData`
- `OrderPreview` has nested `OrderPreviewShift` > `OrderPreviewRole` structure
- Query repo: `getHubs()` replaces `getHubsByOwner(businessId)` -- V2 resolves business from auth token
- `OneTimeOrderPosition` is now a typedef for `OrderPositionUiModel` from `orders_common`
- `OrderEditSheet` (1700 lines) fully rewritten: delegates to `IViewOrdersRepository` instead of direct DC calls
## Staff Authentication Feature Migration (Phase 6 -- completed)
- Migrated from `krow_data_connect` + `DataConnectService` + `firebase_data_connect` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_data_connect`, `firebase_core`
- KEPT `firebase_auth` -- V2 backend `startStaffPhoneAuth` returns `CLIENT_FIREBASE_SDK` mode for mobile, meaning phone verification stays client-side via Firebase SDK
- Auth flow: Firebase SDK phone verify (client-side) -> get idToken -> `POST /auth/staff/phone/verify` with `{ idToken, mode }` -> V2 hydrates session (upserts user, loads actor context)
- V2 verify response: `{ sessionToken, refreshToken, expiresInSeconds, user: { id, email, displayName, phone }, staff: { staffId, tenantId, fullName, ... }, tenant, requiresProfileSetup }`
- `requiresProfileSetup` boolean replaces old signup logic (create user/staff via DC mutations)
- Profile setup: `POST /staff/profile/setup` with `{ fullName, bio, preferredLocations, maxDistanceMiles, industries, skills }`
- Sign out: `POST /auth/sign-out` (server-side token revocation) + local `FirebaseAuth.signOut()`
- `AuthInterceptor` in `DioClient` stays as-is -- attaches Firebase Bearer tokens to all V2 API requests
- `AuthInterceptor` in `DioClient` stays as-is -- attaches Firebase Bearer tokens to all V2 API requests
- Pre-existing issue: `ExperienceSkill` and `Industry` enums deleted from domain but still referenced in `profile_setup_experience.dart`
## Client Authentication Feature Migration (Phase 6 -- completed)
- Migrated from `krow_data_connect` + `DataConnectService` + `firebase_data_connect` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `firebase_data_connect`, `firebase_core` from pubspec
- KEPT `firebase_auth` -- client-side sign-in needed so `AuthInterceptor` can attach Bearer tokens
- KEPT `krow_data_connect` -- only for `ClientSessionStore`/`ClientSession`/`ClientBusinessSession` (not yet extracted)
- Auth flow (Option A -- hybrid):
1. Firebase Auth client-side `signInWithEmailAndPassword` (sets `FirebaseAuth.instance.currentUser`)
2. `GET /auth/session` via V2 API (returns user + business + tenant context)
3. Populate `ClientSessionStore` from V2 session response
- Sign-up flow:
1. `POST /auth/client/sign-up` via V2 API (server-side: creates Firebase account + user/tenant/business/memberships in one transaction)
2. Local `signInWithEmailAndPassword` (sets local auth state)
3. `GET /auth/session` to load context + populate session store
- V2 session response shape: `{ user: { userId, email, displayName, phone, status }, business: { businessId, businessName, businessSlug, role, tenantId, membershipId }, tenant: {...}, vendor: null, staff: null }`
- Sign-out: `POST /auth/client/sign-out` (server-side revocation) + `FirebaseAuth.instance.signOut()` + `ClientSessionStore.instance.clear()`
- V2 sign-up error codes: `AUTH_PROVIDER_ERROR` with message containing `EMAIL_EXISTS` or `WEAK_PASSWORD`, `FORBIDDEN` for role mismatch
- Old Data Connect calls removed: `getUserById`, `getBusinessesByUserId`, `createBusiness`, `createUser`, `updateUser`, `deleteBusiness`
- Old rollback logic removed -- V2 API handles rollback server-side in one transaction
- Domain `User` entity: V2 uses `status: UserStatus` (not `role: String`) -- constructor: `User(id:, email:, displayName:, phone:, status:)`
- Module: `CoreModule()` (not `DataConnectModule()`), injects `BaseApiService` into `AuthRepositoryImpl`
## Client Settings Feature Migration (Phase 6 -- completed)
- Migrated sign-out from `DataConnectService.signOut()` to V2 API + local Firebase Auth
- Removed `DataConnectModule` import from module, replaced with `CoreModule()`
- `SettingsRepositoryImpl` now takes `BaseApiService` (not `DataConnectService`)
- Sign-out: `POST /auth/client/sign-out` + `FirebaseAuth.instance.signOut()` + `ClientSessionStore.instance.clear()`
- `settings_profile_header.dart` still reads from `ClientSessionStore` (now from `krow_core`)
## V2SessionService (Final Phase -- completed)
- `V2SessionService` singleton at `packages/core/lib/src/services/session/v2_session_service.dart`
- Replaces `DataConnectService` for session state management in both apps
- Uses `SessionHandlerMixin` from core (same interface as old DC version)
- `fetchUserRole()` calls `GET /auth/session` via `BaseApiService` (not DC connector)
- `signOut()` calls `POST /auth/sign-out` + `FirebaseAuth.signOut()` + `handleSignOut()`
- Registered in `CoreModule` via `i.addLazySingleton<V2SessionService>()` -- calls `setApiService()`
- Both `main.dart` files use `V2SessionService.instance.initializeAuthListener()` instead of `DataConnectService`
- Both `SessionListener` widgets subscribe to `V2SessionService.instance.onSessionStateChanged`
- `staff_main` package migrated: local repo/usecase via `V2ApiEndpoints.staffProfileCompletion` + `ProfileCompletion.fromJson`
- `krow_data_connect` removed from: staff app, client app, staff_main package pubspecs
- Session stores (`StaffSessionStore`, `ClientSessionStore`) now live in core, not data_connect

View File

@@ -0,0 +1,33 @@
---
name: V2 Profile Migration Status
description: Staff profile sub-packages migrated from Data Connect to V2 REST API - entity mappings and patterns
type: project
---
## Phase 2 Profile Migration (completed 2026-03-16)
All staff profile read features migrated from Firebase Data Connect to V2 REST API.
**Why:** Data Connect is being deprecated in favor of V2 REST API for all mobile backend access.
**How to apply:** When working on any profile feature, use `ApiService.get(V2ApiEndpoints.staffXxx)` not Data Connect connectors.
### Entity Mappings (old -> V2)
- `Staff` (old with name/avatar/totalShifts) -> `Staff` (V2 with fullName/metadata) + `StaffPersonalInfo` for profile form
- `EmergencyContact` (old with name/phone/relationship enum) -> `EmergencyContact` (V2 with fullName/phone/relationshipType string)
- `AttireItem` (removed) -> `AttireChecklist` (V2)
- `StaffDocument` (removed) -> `ProfileDocument` (V2)
- `StaffCertificate` (old with ComplianceType enum) -> `StaffCertificate` (V2 with certificateType string)
- `TaxForm` (old with I9TaxForm/W4TaxForm subclasses) -> `TaxForm` (V2 with formType string + fields map)
- `StaffBankAccount` (removed) -> `BankAccount` (V2)
- `TimeCard` (removed) -> `TimeCardEntry` (V2 with minutesWorked/totalPayCents)
- `PrivacySettings` (new V2 entity)
### Profile Main Page
- Old: 7+ individual completion use cases from data_connect connectors
- New: Single `ProfileRepositoryImpl.getProfileSections()` call returning `ProfileSectionStatus`
- Stats fields (totalShifts, onTimeRate, etc.) no longer on V2 Staff entity -- hardcoded to 0 pending dashboard API
### DI Pattern
- All repos inject `BaseApiService` from `CoreModule` (registered as `i.get<BaseApiService>()`)
- Modules import `CoreModule()` instead of `DataConnectModule()`

View File

@@ -2,3 +2,5 @@
## Project Context
- [project_clock_in_feature_issues.md](project_clock_in_feature_issues.md) — Critical bugs in staff clock_in feature: BLoC lifecycle leak, stale geofence override, dead lunch break data, non-functional date selector
- [project_client_v2_migration_issues.md](project_client_v2_migration_issues.md) — Critical bugs in client app V2 migration: reports BLoCs missing BlocErrorHandler, firebase_auth in features, no executeProtected, hardcoded strings, double sign-in
- [project_v2_migration_qa_findings.md](project_v2_migration_qa_findings.md) — Critical bugs in staff app V2 migration: cold-start session logout, geofence bypass, auth navigation race, token expiry inversion, shifts response shape mismatch

View File

@@ -0,0 +1,22 @@
---
name: Client V2 Migration QA Findings
description: Critical bugs and patterns found in the client app V2 API migration — covers auth, billing, coverage, home, hubs, orders, reports, settings
type: project
---
Client V2 migration QA analysis completed 2026-03-16. Key systemic issues found:
1. **Reports BLoCs missing BlocErrorHandler** — All 7 report BLoCs (spend, coverage, daily_ops, forecast, no_show, performance, summary) use raw try/catch instead of BlocErrorHandler mixin, risking StateError crashes if user navigates away during loading.
2. **firebase_auth in feature packages** — Both `client_authentication` and `client_settings` have `firebase_auth` in pubspec.yaml and import it in their repository implementations. Architecture rule says Firebase packages belong ONLY in `core`.
3. **No repository-level `executeProtected()` usage** — Zero client feature repos wrap API calls with `ApiErrorHandler.executeProtected()`. All rely solely on BLoC-level `handleError`. Timeout and network errors may surface as raw exceptions.
4. **Hardcoded strings scattered across home widgets**`live_activity_widget.dart`, `reorder_widget.dart`, `client_home_error_state.dart` contain English strings ("Today's Status", "Running Late", "positions", "An error occurred", "Retry") instead of localized keys.
5. **Double sign-in in auth flow** — signInWithEmail does V2 POST then Firebase signInWithEmailAndPassword. If V2 succeeds but Firebase fails (e.g. user disabled locally), the server thinks user is signed in but client throws.
6. **`context.t` vs `t` inconsistency** — Coverage feature uses `context.t.client_coverage.*` throughout, while home/billing use global `t.*`. Both work in Slang but inconsistency confuses maintainers.
**Why:** Migration from Data Connect to V2 REST API was a large-scale change touching all features simultaneously.
**How to apply:** When reviewing client features post-migration, check these specific patterns. Reports BLoCs are highest-risk for user-facing crashes.

View File

@@ -0,0 +1,27 @@
---
name: V2 API migration QA findings (staff app)
description: Critical bugs found during V2 API migration review of the staff mobile app — session cold-start logout, geofence bypass, auth race condition, token expiry inversion
type: project
---
V2 API migration introduced several critical bugs across the staff app (reviewed 2026-03-16).
**Why:** The migration from Firebase Data Connect to V2 REST API required rewiring every repository, session service, and entity. Some integration gaps were missed.
**Key findings (severity order):**
1. **Cold-start session logout**`V2SessionService.initializeAuthListener()` is called in `main.dart` before `CoreModule` injects `ApiService`. On cold start, `fetchUserRole` finds `_apiService == null`, returns null, and emits `unauthenticated`, logging the user out.
2. **Geofence coordinates always null**`ClockInRepositoryImpl._mapTodayShiftJsonToShift` defaults latitude/longitude to null because the V2 endpoint doesn't return them. Geofence validation is completely bypassed for all shifts.
3. **Auth navigation race** — After OTP verify, both `PhoneVerificationPage` BlocListener and `SessionListener` try to navigate (one to profile setup, the other to home). Creates unpredictable navigation.
4. **Token expiry check inverted**`session_handler_mixin.dart` line 215: `now.difference(expiryTime)` should be `expiryTime.difference(now)`. Tokens are only "refreshed" after they've already expired.
5. **Shifts response shape mismatch**`shifts_repository_impl.dart` casts `response.data as List<dynamic>` but other repos use `response.data['items']`. Needs validation against actual V2 contract.
6. **Attire blocking poll**`attire_repository_impl.dart` polls verification status for up to 10 seconds on main isolate with no UI feedback.
7. **`firebase_auth` in feature package** — `auth_repository_impl.dart` directly imports firebase_auth. Architecture rules require firebase_auth only in core.
**How to apply:** When reviewing future V2 migration PRs, check: (a) session init ordering, (b) response shape matches between repos and API, (c) nullable field defaults in entity mapping, (d) navigation race conditions between SessionListener and feature BlocListeners.