Skip to content

refactor: implement refactor audit 2026-04-23 (P1-P3)#62

Merged
Exc1D merged 31 commits intomainfrom
refactor/audit-2026-04-23-continuation
Apr 24, 2026
Merged

refactor: implement refactor audit 2026-04-23 (P1-P3)#62
Exc1D merged 31 commits intomainfrom
refactor/audit-2026-04-23-continuation

Conversation

@Exc1D
Copy link
Copy Markdown
Owner

@Exc1D Exc1D commented Apr 23, 2026

Summary

Implements items from the refactor audit spec (docs/refactor-audit-2026-04-23.md).

P1 — High Risk

  • test: improve test reliability and assertions #3 Split inbound.ts into modules: Extracted gazetteer.ts, levenshtein.ts, auto-reply.ts, and renamed main parser to parser.ts. Eliminated all 6 @ts-expect-error comments. Cleaned stale lib/inbound.js build artifacts.
  • docs: reorganize and consolidate project documentation #4 Extract dispatch-responder.ts: Split into dispatch-responder-validation.ts, dispatch-responder-notify.ts, dispatch-responder-writes.ts. Added municipalityId runtime validation and fixed spread-order bug in validatedResponder.

P2 — Structural Debt

P3 — Cleanup

Verification

  • npx turbo run lint typecheckPASS (25/25)
  • pnpm --filter @bantayog/shared-sms-parser testPASS (13/13)
  • pnpm --filter @bantayog/shared-ui testPASS (11/11)

Rollback

To revert: git revert HEAD~4..HEAD on this branch, or restore the 4 deleted auth files from main.


Note: P2 #5 remaining gaps (admin-desktop, responder-app, shared-data, shared-types tests) are explicitly marked "Large (spread across sprints)" in the audit and are deferred.

Summary by CodeRabbit

  • New Features

    • Shared authentication and route-guarding used across apps; GPS-backed location with manual fallback; SMS parser with fuzzy barangay matching and contextual auto-replies; new municipality, barangay, and contact form components.
  • Refactor

    • Consolidated auth/provider and route protection into a shared UI package; large location/form component split into focused hooks and components; SMS parsing reorganized.
  • Tests

    • Added comprehensive tests for auth, protected routes, GPS, selectors, contact fields, and SMS parser.
  • Documentation

    • Added refactor audit, plan, and learnings.

claude and others added 12 commits April 23, 2026 20:39
- Extract FALLBACK_BARANGAYS to data/fallback-barangays.ts
- Extract storage error utilities to utils/storage-errors.ts
- Extract useMunicipalityBarangays hook
- Extract MunicipalitySelector, BarangaySelector, ContactFields components
- Simplify Step2WhoWhere from 707 to 289 lines (-59%)
- Add setLocationMethod to useGpsLocation return type

Verification: typecheck, lint, test all PASS (137 passed, 2 todo)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ispatch-responder modules

- P1 #3: Split inbound.ts into parser.ts, gazetteer.ts, levenshtein.ts, auto-reply.ts
- P1 #4: Extract dispatch-responder into validation, notify, and writes modules
- Add municipalityId runtime validation and fix spread-order bug in validatedResponder
…it items

- P2 #7: Convert 14 implicit-any catch patterns to catch (err: unknown)
- P2 #8: Replace console.log with console.info in shared-validators logging
- P3 #9: Confirm no source any types remain in production code
- P3 #10: Convert TODO to TICKET(BANTAYOG-PHASE6)
- Fix remaining catch (replyErr) in sms-inbound-processor.ts
- P2 #6: Move AuthProvider and ProtectedRoute to @bantayog/shared-ui
- AuthProvider accepts auth instance as prop, includes active guard and refreshClaims
- ProtectedRoute accepts allowedRoles, requireActive, requireMunicipalityIdForRoles as props
- Update admin-desktop and responder-app to consume shared components
- Delete 4 duplicated auth files from apps
- Add peer dependencies for firebase and react-router-dom
…ectedRoute

- P2 #5: Add 11 tests covering AuthProvider (6) and ProtectedRoute (5)
- AuthProvider: null user, authenticated user, signOut, refreshClaims,
  getIdTokenResult error, useAuth guard
- ProtectedRoute: redirect to login, allowed role, denied role,
  requireActive deny, requireMunicipalityIdForRoles deny
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @Exc1D, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Consolidates app-local auth and ProtectedRoute into packages/shared-ui; extracts and modularizes SMS parsing (parser/gazetteer/levenshtein/auto-reply); refactors citizen PWA Step2WhoWhere into hooks/components (GPS, municipality/barangay, contact fields); splits dispatch callable into validation/writes/notify modules; and standardizes catch typing to unknown across the repo.

Changes

Cohort / File(s) Summary
Shared UI — Auth & Routing
packages/shared-ui/src/auth-provider.tsx, packages/shared-ui/src/protected-route.tsx, packages/shared-ui/src/index.ts, packages/shared-ui/lib/*.d.ts, packages/shared-ui/package.json, packages/shared-ui/vitest.config.ts, packages/shared-ui/src/*.test.tsx
Adds/export AuthProvider({ auth }), useAuth(), ProtectedRoute (role/active/municipality checks); provider exposes refreshClaims and stable signOut; includes tests and Vitest config.
App wiring — migrate to shared-ui
apps/admin-desktop/src/App.tsx, apps/admin-desktop/src/routes.tsx, apps/admin-desktop/src/app/* (deleted), apps/responder-app/src/App.tsx, apps/responder-app/src/routes.tsx, apps/responder-app/src/app/* (deleted)
Replaces local auth/provider/guard with @bantayog/shared-ui exports; passes auth into AuthProvider; removes app-local AuthProvider/ProtectedRoute implementations.
Citizen PWA — Step2WhoWhere & UI components
apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx, .../MunicipalitySelector.tsx, .../BarangaySelector.tsx, .../ContactFields.tsx, .../location-constants.ts, .../data/fallback-barangays.ts
Extracts UI and data from Step2WhoWhere: adds MunicipalitySelector, BarangaySelector, ContactFields, and typed fallback barangay constants; replaces inlined datasets and consolidates contact/GPS UI.
Citizen PWA — Location hooks & tests
apps/citizen-pwa/src/hooks/useGpsLocation.ts, apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts, apps/citizen-pwa/src/hooks/*.test.ts
Adds useGpsLocation (GPS/manual flow, 10s timeout, error mapping, autoAttempt) and useMunicipalityBarangays (muni/barangay selection, validation); includes unit tests covering success, errors, and mount behavior.
Citizen PWA — Submit form tests
apps/citizen-pwa/src/components/SubmitReportForm/*.test.tsx
Adds tests for MunicipalitySelector, BarangaySelector, ContactFields (options, handlers, error visibility, patient counter, optional UI).
SMS parser — modularization & lib outputs
packages/shared-sms-parser/src/parser.ts, .../gazetteer.ts, .../levenshtein.ts, .../auto-reply.ts, packages/shared-sms-parser/lib/*, packages/shared-sms-parser/package.json, packages/shared-sms-parser/src/__tests__/*
Splits SMS parsing into parser/gazetteer/levenshtein/auto-reply modules; adds fallback gazetteer loader and Levenshtein; implements parser with fuzzy matching and auto-reply generation; updates package to publish lib/ artifacts and adds tests.
Dispatch callable — split responsibilities
functions/src/callables/dispatch-responder.ts, .../dispatch-responder-validation.ts, .../dispatch-responder-writes.ts, .../dispatch-responder-notify.ts
Refactors dispatch logic into validation, transactional writes, and notify/enqueue modules; centralizes SMS payload build and transaction writes; re-exports types.
Functions & services — error handling and robustness
functions/src/index.ts, functions/lib/firestore/sms-inbound-processor.js, functions/lib/http/sms-inbound.js, functions/lib/services/sms-providers/semaphore.js, functions/lib/services/fcm-send.js, functions/lib/triggers/on-media-finalize.js
Tightens error handling (typed catches unknown), adds decryption/preflight checks, logs errors into structured fields, treats some provider JSON parse failures as non-retryable, and records post-retry FCM errors.
Type-safety sweep
apps/*, functions/*, packages/* (various small edits)
Standardizes catch (err: unknown) across many files, adjusts subsequent guards, and fixes a few typed parameters (photoUpload, useReport, FCM token registration, etc.).
Utilities & storage guards
apps/citizen-pwa/src/utils/storage-errors.ts, apps/citizen-pwa/src/data/fallback-barangays.ts
Adds isQuotaExceededError and isSecurityError type guards for storage exceptions; republishes fallback barangays as typed export.
Tests, docs & infra
functions/lib/__tests__/*, docs/*.md, eslint.config.js, packages/shared-ui/* tests/config
Adds many tests for new modules, updates docs (learnings/progress/refactor audit), removes ESLint ignore for shared-sms-parser index, and configures Vitest/test deps for shared-ui and parser.

Sequence Diagram(s)

sequenceDiagram
    participant App as Client App
    participant Shared as `@bantayog/shared-ui`<br/>AuthProvider
    participant Firebase as Firebase Auth
    participant Token as getIdTokenResult

    App->>Shared: Mount <AuthProvider auth={auth}>
    Shared->>Firebase: onAuthStateChanged(auth)
    Firebase-->>Shared: auth state (user|null)
    alt user present
        Shared->>Token: getIdTokenResult()
        Token-->>Shared: token with claims
        Shared-->>App: context ready (user, claims, loading=false)
        App->>Shared: refreshClaims()
        Shared->>Token: getIdTokenResult(true)
        Token-->>Shared: refreshed claims
        Shared-->>App: updated claims
    else no user
        Shared-->>App: loading=false, user=null, claims=null
    end
Loading
sequenceDiagram
    participant Client as SMS sender
    participant Parser as Parser Module
    participant Gazetteer as Gazetteer
    participant Lev as Levenshtein
    participant Auto as AutoReply

    Client->>Parser: parseInboundSms("BANTAYOG <TYPE> <BARANGAY>")
    Parser->>Gazetteer: getBarangayGazetteer()
    Gazetteer-->>Parser: array of barangays
    Parser->>Parser: normalize, extract type & token
    alt exact match
        Parser-->>Client: { confidence: "high", parsed: {...}, autoReplyText }
    else fuzzy
        Parser->>Lev: levenshtein(candidate, token)
        Lev-->>Parser: distance
        Parser->>Auto: buildAutoReply(confidence)
        Auto-->>Parser: text
        Parser-->>Client: { confidence: "medium"|"low", parsed/candidates, autoReplyText }
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I hopped through imports, claims in paw,
Shared provider springs where locals thaw.
Parsers sniff typos, GPS finds true way,
Dispatch split tidy — tests cheer hooray.
A carrot nod for typesafe day! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing a refactor audit with specific phases (P1-P3), which is the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/audit-2026-04-23-continuation

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
functions/src/index.ts (1)

73-82: ⚠️ Potential issue | 🟠 Major

Unsafe casts in unknown catch block can throw inside the error handler and mask original errors.

Lines 74 and 81 use direct casts without type guards. If err is null, undefined, or a primitive value, accessing .code or .message will throw, replacing the original error with a new one.

Proposed fix
   } catch (err: unknown) {
-      const code = (err as { code?: string }).code
+      const code =
+        typeof err === 'object' && err !== null && 'code' in err
+          ? (err as { code?: string }).code
+          : undefined
       if (code === 'MEDIA_REJECTED_MIME' || code === 'MEDIA_REJECTED_CORRUPT') {
         return
       }
+      const message = err instanceof Error ? err.message : String(err)
       log({
         severity: 'ERROR',
         code: 'MEDIA_FINALIZE_FAILED',
-        message: `onMediaFinalize failed: ${(err as Error).message}`,
+        message: `onMediaFinalize failed: ${message}`,
       })
       throw err
     }

Per defensive programming guidelines: validate at the boundary and never swallow errors with unsafe type assertions that may themselves throw.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@functions/src/index.ts` around lines 73 - 82, The catch block in the
onMediaFinalize flow is using unsafe casts on err (lines referencing the catch
handling where err is read and logged) which can throw if err is null/undefined
or a primitive; change it to defensively inspect err (use typeof checks or an
isError-like guard) before accessing properties: safely derive code by checking
err && typeof (err as any).code === 'string' and derive message via (err
instanceof Error ? err.message : String(err || 'unknown error')), preserve the
MEDIA_REJECTED_* early return logic using the safely-read code, and pass the
safe message into the log call (the log invocation and MEDIA_FINALIZE_FAILED
code should remain the same).
functions/src/callables/dispatch-responder.ts (1)

165-171: 🧹 Nitpick | 🔵 Trivial

Dead code: the typeof claims.role !== 'string' check is unreachable.

Line 165 already validates that claims.role is exactly 'municipal_admin' or 'provincial_superadmin' using strict equality. If execution reaches line 169, claims.role is guaranteed to be one of those string literals. The type check on lines 169-171 can never trigger.

Either remove the redundant check, or reorder so the type check comes first if you want explicit string validation for clarity:

Option 1: Remove the redundant check
   if (claims.role !== 'municipal_admin' && claims.role !== 'provincial_superadmin') {
     throw new HttpsError('permission-denied', 'municipal_admin or provincial_superadmin required')
   }
   if (claims.active !== true) throw new HttpsError('permission-denied', 'account is not active')
-  if (typeof claims.role !== 'string') {
-    throw new HttpsError('permission-denied', 'role is required')
-  }
Option 2: Reorder to validate type first (if explicit check desired)
   if (!claims) throw new HttpsError('unauthenticated', 'token required')
+  if (typeof claims.role !== 'string') {
+    throw new HttpsError('permission-denied', 'role is required')
+  }
   if (claims.role !== 'municipal_admin' && claims.role !== 'provincial_superadmin') {
     throw new HttpsError('permission-denied', 'municipal_admin or provincial_superadmin required')
   }
   if (claims.active !== true) throw new HttpsError('permission-denied', 'account is not active')
-  if (typeof claims.role !== 'string') {
-    throw new HttpsError('permission-denied', 'role is required')
-  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@functions/src/callables/dispatch-responder.ts` around lines 165 - 171, The
check "typeof claims.role !== 'string'" in the dispatch-responder permission
block is unreachable because you already assert claims.role equals
'municipal_admin' or 'provincial_superadmin' earlier; remove the redundant
type-check branch (the HttpsError thrown there) so only the role and active
checks remain, or alternatively move the type validation to run before the
role-equality checks (validate typeof claims.role === 'string' first, then test
the specific values) — update the logic in the function containing claims.role
and the HttpsError usage accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/citizen-pwa/src/components/SubmitReportForm/ContactFields.tsx`:
- Around line 89-106: The Yes/No toggle buttons in the ContactFields component
only expose selection via CSS; add aria-pressed to both buttons so screen
readers get pressed-state semantics: set aria-pressed={anyoneHurt} on the "Yes"
button and aria-pressed={!anyoneHurt} on the "No" button (keep existing onClick
handlers and className logic intact so visual state and behavior remain
unchanged).
- Around line 40-71: Replace the plain <p> labels with explicit <label> elements
and associate them to the corresponding inputs by adding id attributes to the
inputs and htmlFor on the labels in ContactFields.tsx: change the "Your name"
<p> to a <label htmlFor="reporter-name"> and give the name input
id="reporter-name" (used by reporterName/onReporterNameChange/onNameErrorClear),
and change the "Phone number" <p> to a <label htmlFor="reporter-msisdn"> and
give the phone input id="reporter-msisdn" (used by
reporterMsisdn/onReporterMsisdnChange/onPhoneErrorClear); keep existing classes,
placeholders, required flags and error rendering intact.

In `@apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx`:
- Around line 80-83: The early return when locationMethod === 'manual' &&
!selectedMunicipalityId prevents proceeding but gives no UI feedback; add a
municipality selection validation error (e.g., municipalityError) when
selectedMunicipalityId is missing and pass that to MunicipalitySelector instead
of the GPS-specific locationError from useGpsLocation; update the logic that
computes canProceed/validation to consider municipalityError so the UI shows the
error message near MunicipalitySelector when the user tries to continue with no
municipality selected.
- Around line 219-224: The MunicipalitySelector is receiving the GPS-specific
locationError when locationMethod === 'manual', which shows a misleading GPS
error in the dropdown; update the code so MunicipalitySelector only gets
municipality-specific validation errors (or no error prop) by removing
error={locationError} from the MunicipalitySelector call in Step2WhoWhere and
either (a) implement and surface a municipality error from
useMunicipalityBarangays and pass that instead, or (b) omit the error prop
entirely if there is no dropdown validation; if you intend to show a GPS-failure
reason while in manual mode, add a separate prop (e.g., gpsNotice or
manualModeReason) and pass locationError to that prop rather than to
MunicipalitySelector.

In `@apps/citizen-pwa/src/hooks/useGpsLocation.test.ts`:
- Around line 60-90: Add a test case to cover GeolocationPositionError code 2 by
mirroring the existing tests for codes 1 and 3: in
apps/citizen-pwa/src/hooks/useGpsLocation.test.ts create an it('handles position
unavailable (code 2)') that sets mockGetCurrentPosition.mockImplementation to
call the error callback with { code: 2 } as GeolocationPositionError, then
renderHook(() => useGpsLocation()), await act(() =>
result.current.attemptGps()), and assert result.current.locationError equals
'Could not get location. Choose municipality manually.',
result.current.locationMethod is 'manual', and result.current.isLoading is
false.

In `@apps/citizen-pwa/src/hooks/useGpsLocation.ts`:
- Around line 49-58: The catch block in useGpsLocation.ts currently handles
GeolocationPositionError codes 1 and 3 but lets code 2 fall back to the generic
message; update the error handling in the catch (err: unknown) block to
explicitly handle code 2 (POSITION_UNAVAILABLE) by setting a clear message via
setLocationError (for example "Location unavailable. Choose municipality
manually.") and then call setLocationMethod('manual'), or if you prefer, add an
inline comment next to the existing code path that documents why code 2
intentionally uses the generic fallback; reference the existing error branch
that inspects (err as GeolocationPositionError).code and the functions
setLocationError and setLocationMethod to locate where to make the change.

In `@apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts`:
- Around line 19-20: The hook currently exports a raw setter
setSelectedBarangayId allowing callers to pick any id; change this to a
validated handler (e.g., setSelectedBarangay) that checks the id against the
current municipality's barangay list before applying it. Locate the exported
setSelectedBarangayId and the reset export in useMunicipalityBarangays.ts, make
the internal state setter private, and replace the exported setter with a
function that: returns early for undefined (or calls reset), verifies the id
exists in the current barangays collection (by comparing to barangay.id), and
only then updates state; if invalid, either ignore or call reset and optionally
surface an error/boolean result. Update the hook's returned type/signature to
expose only the validated setSelectedBarangay and keep reset as-is.

In `@docs/progress.md`:
- Line 5: The progress heading "Refactor Audit 2026-04-23 — Implementation
Continuation (2026-04-24)" uses a future date (2026-04-24); update that heading
so the continuation date matches the audit batch (e.g., change "(2026-04-24)" to
"(2026-04-23)" or remove the parenthetical date) to keep the timeline
consistent; edit the line that contains that exact heading text to reflect the
non-future date.

In `@functions/src/callables/dispatch-responder-validation.ts`:
- Around line 53-69: The current flow calls assertResponderOnShift before
verifying municipality ownership and existence checks, which can surface
INVALID_STATUS_TRANSITION instead of the intended NOT_FOUND or FORBIDDEN errors;
move the checks for deps.actor.claims.municipalityId (and throw
BantayogError(BantayogErrorCode.INVALID_ARGUMENT) if missing), and the existence
checks for reportSnap.exists and responderSnap.exists (throwing NOT_FOUND) to
run before calling assertResponderOnShift, then call
assertResponderOnShift(rtdb, deps.actor.claims.municipalityId,
deps.responderUid, ...) last so missing/unauthorized resources return the
correct error classes.

In `@functions/src/callables/dispatch-responder-writes.ts`:
- Around line 27-30: The current check allows whitespace-only phone strings to
pass; update the validation around consentData (where recipientMsisdn is
assigned) to trim and assert a non-empty phone value before proceeding: ensure
consentData?.phone is a string, compute const trimmedPhone =
consentData.phone.trim(), return null if trimmedPhone === '' and then set
recipientMsisdn = trimmedPhone; keep locale logic unchanged (locale =
consentData.locale === 'en' ? 'en' : 'tl') so downstream enqueue payloads never
receive blank/whitespace MSISDNs.

In `@packages/shared-sms-parser/lib/inbound.js`:
- Around line 1-155: The file contains a duplicated parser implementation (the
function parseInboundSms) which conflicts with the canonical parser in
parser.js; remove the duplicate body and instead delegate to or re-export the
single source of truth: import the existing parseInboundSms implementation from
the canonical module (parser) and export it from this module, ensuring any
symbols used by consumers (parseInboundSms, and any exported types/constants if
required) are preserved; update/remove any local references to the duplicate
helpers so this module just forwards to the shared implementation to avoid
divergence.

In `@packages/shared-sms-parser/src/gazetteer.ts`:
- Around line 315-325: Tighten the catch predicate so we only treat a true
"module not found" for the package `@bantayog/shared-data` as the fallback case:
keep the existing isModuleNotFound check but replace the loose
message.includes('@bantayog/shared-data') test (isSharedDataLoadFailure) with an
explicit match for the standard Node message for a missing module (e.g., test
that err is an Error and its message exactly equals or matches the pattern
"Cannot find module '@bantayog/shared-data'" using a strict equality or a regex
match), so that only that exact failure returns FALLBACK_BARANGAYS and all other
errors continue to be rethrown.

In `@packages/shared-sms-parser/src/levenshtein.ts`:
- Around line 20-22: The defensive if-check against undefined for deletion,
insertion, and substitution is misleading because Uint32Array returns 0 for OOB
reads, so remove the runtime check and instead add a concise comment above the
accesses (mentioning Uint32Array OOB → 0 and that loop/indices are
mathematically bounded) to satisfy reviewers and explain why
noUncheckedIndexedAccess is not a practical concern here; reference the
variables deletion, insertion, substitution used in the Levenshtein DP loop so
reviewers can find the change quickly.
- Around line 27-28: The nullish coalescing fallback on the Uint32Array lookup
(result ?? 0) is unreachable because dp[...] always yields a number; remove the
dead ?? 0 and either assert the index access is intentional with a non-null
assertion (use result = dp[m * (n + 1) + n]!) or keep the current expression but
add a short comment above the line explaining it exists only to satisfy
TypeScript's noUncheckedIndexedAccess rule; reference the variables dp, m, n and
the local variable result when making the change.

In `@packages/shared-sms-parser/src/parser.ts`:
- Around line 109-113: The current details extraction uses barangayIndex =
originalRest.toUpperCase().indexOf(barangayToken.toUpperCase()) which picks the
first occurrence and can be wrong if the token appears earlier; update the logic
in parser.ts (variables: originalRest, barangayToken, barangayIndex,
detailsStartIndex, details) to find the occurrence closest to the expected
details start — e.g., search for the last occurrence with lastIndexOf, or
iterate matches (using a case-insensitive regex with word boundaries) and pick
the match whose index + detailsStartIndex is within originalRest bounds or is
the first such match after any known prefix; then slice from that chosen index +
detailsStartIndex to compute details.
- Around line 151-160: The check for match === undefined inside the block
guarded by if (fuzzyMatches.length === 1) is unreachable; update the handling in
parse (or the function containing fuzzyMatches) to remove the redundant
undefined branch or use a non-null assertion when assigning const match =
fuzzyMatches[0] (e.g., const match = fuzzyMatches[0]!) so TypeScript knows it's
defined, and ensure you still return the expected structure (confidence, parsed,
candidates, autoReplyText via buildAutoReply) for the single-match path without
the unreachable conditional.

In `@packages/shared-ui/src/protected-route.tsx`:
- Around line 37-40: The authorization check in protected-route.tsx currently
treats whitespace-only municipalityId as valid; update the condition that uses
requireMunicipalityIdForRoles and claims?.municipalityId to reject values that
are only whitespace by trimming before checking length (e.g. replace the current
(typeof claims?.municipalityId !== 'string' || !claims.municipalityId) check
with a trimmed non-empty string check), so any municipalityId must be a string
and claims.municipalityId.trim().length > 0; ensure you modify the if branch
guarding the route (the block using requireMunicipalityIdForRoles and
claims?.municipalityId) accordingly and keep error handling defensive.

---

Outside diff comments:
In `@functions/src/callables/dispatch-responder.ts`:
- Around line 165-171: The check "typeof claims.role !== 'string'" in the
dispatch-responder permission block is unreachable because you already assert
claims.role equals 'municipal_admin' or 'provincial_superadmin' earlier; remove
the redundant type-check branch (the HttpsError thrown there) so only the role
and active checks remain, or alternatively move the type validation to run
before the role-equality checks (validate typeof claims.role === 'string' first,
then test the specific values) — update the logic in the function containing
claims.role and the HttpsError usage accordingly.

In `@functions/src/index.ts`:
- Around line 73-82: The catch block in the onMediaFinalize flow is using unsafe
casts on err (lines referencing the catch handling where err is read and logged)
which can throw if err is null/undefined or a primitive; change it to
defensively inspect err (use typeof checks or an isError-like guard) before
accessing properties: safely derive code by checking err && typeof (err as
any).code === 'string' and derive message via (err instanceof Error ?
err.message : String(err || 'unknown error')), preserve the MEDIA_REJECTED_*
early return logic using the safely-read code, and pass the safe message into
the log call (the log invocation and MEDIA_FINALIZE_FAILED code should remain
the same).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f0c14564-a7c2-468c-91f6-a05c91af0f5e

📥 Commits

Reviewing files that changed from the base of the PR and between 1ca85ac and 29e4eee.

⛔ Files ignored due to path filters (36)
  • functions/lib/__tests__/callables/https-error.test.d.ts.map is excluded by !**/*.map
  • functions/lib/__tests__/callables/https-error.test.js.map is excluded by !**/*.map
  • functions/lib/__tests__/triggers/dispatch-mirror-to-report.test.js.map is excluded by !**/*.map
  • functions/lib/firestore/sms-inbound-processor.d.ts.map is excluded by !**/*.map
  • functions/lib/firestore/sms-inbound-processor.js.map is excluded by !**/*.map
  • functions/lib/http/sms-inbound.d.ts.map is excluded by !**/*.map
  • functions/lib/http/sms-inbound.js.map is excluded by !**/*.map
  • functions/lib/services/fcm-send.d.ts.map is excluded by !**/*.map
  • functions/lib/services/fcm-send.js.map is excluded by !**/*.map
  • functions/lib/services/sms-providers/semaphore.d.ts.map is excluded by !**/*.map
  • functions/lib/services/sms-providers/semaphore.js.map is excluded by !**/*.map
  • functions/lib/triggers/on-media-finalize.d.ts.map is excluded by !**/*.map
  • functions/lib/triggers/on-media-finalize.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/__tests__/inbound.test.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/auto-reply.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/auto-reply.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/gazetteer.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/gazetteer.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/inbound.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/inbound.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/index.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/index.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/levenshtein.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/levenshtein.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.js.map is excluded by !**/*.map
  • packages/shared-ui/lib/auth-provider.d.ts.map is excluded by !**/*.map
  • packages/shared-ui/lib/auth-provider.test.d.ts.map is excluded by !**/*.map
  • packages/shared-ui/lib/index.d.ts.map is excluded by !**/*.map
  • packages/shared-ui/lib/protected-route.d.ts.map is excluded by !**/*.map
  • packages/shared-ui/lib/protected-route.test.d.ts.map is excluded by !**/*.map
  • packages/shared-validators/lib/logging.d.ts.map is excluded by !**/*.map
  • packages/shared-validators/lib/logging.js.map is excluded by !**/*.map
  • packages/shared-validators/lib/sms-templates.d.ts.map is excluded by !**/*.map
  • packages/shared-validators/lib/sms-templates.js.map is excluded by !**/*.map
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (86)
  • apps/admin-desktop/src/App.tsx
  • apps/admin-desktop/src/app/auth-provider.tsx
  • apps/admin-desktop/src/app/protected-route.tsx
  • apps/admin-desktop/src/pages/DispatchModal.tsx
  • apps/admin-desktop/src/pages/TriageQueuePage.tsx
  • apps/admin-desktop/src/routes.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/BarangaySelector.test.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/BarangaySelector.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/ContactFields.test.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/ContactFields.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/MunicipalitySelector.test.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/MunicipalitySelector.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/location-constants.ts
  • apps/citizen-pwa/src/data/fallback-barangays.ts
  • apps/citizen-pwa/src/hooks/useGpsLocation.test.ts
  • apps/citizen-pwa/src/hooks/useGpsLocation.ts
  • apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts
  • apps/citizen-pwa/src/hooks/useReport.ts
  • apps/citizen-pwa/src/lib/photoUpload.ts
  • apps/citizen-pwa/src/useCitizenShell.ts
  • apps/citizen-pwa/src/utils/storage-errors.ts
  • apps/responder-app/src/App.tsx
  • apps/responder-app/src/app/protected-route.tsx
  • apps/responder-app/src/hooks/useRegisterFcmToken.ts
  • apps/responder-app/src/pages/DispatchListPage.tsx
  • apps/responder-app/src/routes.tsx
  • apps/responder-app/src/services/fcm-client.ts
  • docs/learnings.md
  • docs/progress.md
  • docs/refactor-audit-2026-04-23.md
  • docs/superpowers/plans/2026-04-23-step2wherewhere-refactoring.md
  • eslint.config.js
  • functions/lib/__tests__/callables/https-error.test.d.ts
  • functions/lib/__tests__/callables/https-error.test.js
  • functions/lib/__tests__/triggers/dispatch-mirror-to-report.test.js
  • functions/lib/firestore/sms-inbound-processor.js
  • functions/lib/http/sms-inbound.js
  • functions/lib/services/fcm-send.js
  • functions/lib/services/sms-providers/semaphore.js
  • functions/lib/triggers/on-media-finalize.js
  • functions/src/callables/advance-dispatch.ts
  • functions/src/callables/decline-dispatch.ts
  • functions/src/callables/dispatch-responder-notify.ts
  • functions/src/callables/dispatch-responder-validation.ts
  • functions/src/callables/dispatch-responder-writes.ts
  • functions/src/callables/dispatch-responder.ts
  • functions/src/firestore/sms-inbound-processor.ts
  • functions/src/index.ts
  • functions/src/triggers/dispatch-sms-outbox.ts
  • packages/shared-sms-parser/lib/__tests__/inbound.test.js
  • packages/shared-sms-parser/lib/auto-reply.d.ts
  • packages/shared-sms-parser/lib/auto-reply.js
  • packages/shared-sms-parser/lib/gazetteer.d.ts
  • packages/shared-sms-parser/lib/gazetteer.js
  • packages/shared-sms-parser/lib/inbound.js
  • packages/shared-sms-parser/lib/index.d.ts
  • packages/shared-sms-parser/lib/index.js
  • packages/shared-sms-parser/lib/levenshtein.d.ts
  • packages/shared-sms-parser/lib/levenshtein.js
  • packages/shared-sms-parser/lib/parser.d.ts
  • packages/shared-sms-parser/lib/parser.js
  • packages/shared-sms-parser/package.json
  • packages/shared-sms-parser/src/__tests__/inbound.test.ts
  • packages/shared-sms-parser/src/auto-reply.ts
  • packages/shared-sms-parser/src/gazetteer.ts
  • packages/shared-sms-parser/src/index.ts
  • packages/shared-sms-parser/src/levenshtein.ts
  • packages/shared-sms-parser/src/parser.ts
  • packages/shared-sms-parser/vitest.config.ts
  • packages/shared-ui/lib/auth-provider.d.ts
  • packages/shared-ui/lib/auth-provider.test.d.ts
  • packages/shared-ui/lib/index.d.ts
  • packages/shared-ui/lib/protected-route.d.ts
  • packages/shared-ui/lib/protected-route.test.d.ts
  • packages/shared-ui/package.json
  • packages/shared-ui/src/auth-provider.test.tsx
  • packages/shared-ui/src/auth-provider.tsx
  • packages/shared-ui/src/index.ts
  • packages/shared-ui/src/protected-route.test.tsx
  • packages/shared-ui/src/protected-route.tsx
  • packages/shared-ui/vitest.config.ts
  • packages/shared-validators/lib/logging.js
  • packages/shared-validators/lib/sms-templates.js
  • packages/shared-validators/src/logging.ts
  • packages/shared-validators/src/sms-templates.ts
💤 Files with no reviewable changes (4)
  • apps/admin-desktop/src/app/protected-route.tsx
  • apps/responder-app/src/app/protected-route.tsx
  • apps/admin-desktop/src/app/auth-provider.tsx
  • eslint.config.js

Comment thread apps/citizen-pwa/src/components/SubmitReportForm/ContactFields.tsx Outdated
Comment thread apps/citizen-pwa/src/components/SubmitReportForm/ContactFields.tsx
Comment thread apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx
Comment thread apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx
Comment thread apps/citizen-pwa/src/hooks/useGpsLocation.test.ts
Comment thread packages/shared-sms-parser/src/levenshtein.ts Outdated
Comment on lines +27 to +28
const result = dp[m * (n + 1) + n]
return result ?? 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Nullish coalescing fallback is unreachable.

Uint32Array elements are always number (never undefined), so result ?? 0 is dead code. The ?? 0 likely exists to satisfy TypeScript's noUncheckedIndexedAccess, which is fine, but a brief comment would clarify intent.

Clarify intent with a comment
   const result = dp[m * (n + 1) + n]
-  return result ?? 0
+  // ?? 0 satisfies noUncheckedIndexedAccess; Uint32Array guarantees a number here
+  return result ?? 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = dp[m * (n + 1) + n]
return result ?? 0
const result = dp[m * (n + 1) + n]
// ?? 0 satisfies noUncheckedIndexedAccess; Uint32Array guarantees a number here
return result ?? 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared-sms-parser/src/levenshtein.ts` around lines 27 - 28, The
nullish coalescing fallback on the Uint32Array lookup (result ?? 0) is
unreachable because dp[...] always yields a number; remove the dead ?? 0 and
either assert the index access is intentional with a non-null assertion (use
result = dp[m * (n + 1) + n]!) or keep the current expression but add a short
comment above the line explaining it exists only to satisfy TypeScript's
noUncheckedIndexedAccess rule; reference the variables dp, m, n and the local
variable result when making the change.

Comment thread packages/shared-sms-parser/src/parser.ts Outdated
Comment thread packages/shared-sms-parser/src/parser.ts Outdated
Comment thread packages/shared-ui/src/protected-route.tsx
claude added 6 commits April 24, 2026 07:46
…w comments

- dispatch-responder-validation: reorder checks so NOT_FOUND/FORBIDDEN
  surface before assertResponderOnShift
- dispatch-responder-writes: add trim() + empty check for MSISDN
- dispatch-responder: remove dead typeof claims.role !== 'string' check
- index.ts: defensive err narrowing before accessing .code/.message
- gazetteer.ts: tighten MODULE_NOT_FOUND to regex match
- parser.ts: remove unreachable branch, fix details extraction with lastIndexOf
- levenshtein.ts: fix lint/typecheck loop with ?? 0 narrowing
- protected-route.tsx: add .trim().length > 0 for municipalityId
- Replace <p className="field-label"> with <label htmlFor="...">
  for name and phone inputs; add matching id attributes
- Add aria-pressed to Yes/No toggle buttons for screen reader state
- Step2WhoWhere: add municipalityError state, show error when user tries
  to continue without selecting a municipality in manual mode
- Step2WhoWhere: pass municipalityError (not GPS locationError) to
  MunicipalitySelector for clearer semantics
- useMunicipalityBarangays: replace raw setSelectedBarangayId with
  validated handleSelectBarangay that only accepts ids present in the
  current municipality's barangay list
…tion

- Add explicit error message for GeolocationPositionError code 2
- Add test case covering code 2 alongside existing codes 1 and 3
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
packages/shared-sms-parser/src/gazetteer.ts (1)

309-330: 🧹 Nitpick | 🔵 Trivial

Error handling pattern is sound but regex could be stricter.

The error matching has been improved from the previous substring check. The combined check of isModuleNotFound && isSharedDataLoadFailure is reasonable defensive coding. However, the regex /Cannot find module .@bantayog\/shared-data./ uses . which matches any character (not just the quote). Consider using an explicit character class for the quote:

-const isSharedDataLoadFailure = /Cannot find module .@bantayog\/shared-data./.test(message)
+const isSharedDataLoadFailure = /Cannot find module ['"]@bantayog\/shared-data['"]/.test(message)

This is minor since the isModuleNotFound check already gates this, but it would make the intent clearer and avoid any edge-case false positives from transitive dependency messages that happen to contain the package name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared-sms-parser/src/gazetteer.ts` around lines 309 - 330, The
regex in getBarangayGazetteer that tests the error message for "Cannot find
module ..." is too permissive; tighten it to explicitly match the surrounding
quote character so it only matches messages like "Cannot find module '...'" or
"Cannot find module \"...\"". Update the isSharedDataLoadFailure test to use a
character class for the quote (e.g., ['\"]) around `@bantayog`\/shared-data,
leaving the rest of the catch logic (isModuleNotFound, returning
FALLBACK_BARANGAYS, and rethrowing) unchanged and keep references to
mod.BARANGAY_GAZETTEER logic intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts`:
- Around line 34-37: handleSelectMunicipality currently accepts any truthy
muniId and commits it to state; instead validate the incoming muniId before
calling setSelectedMunicipalityId and clearing barangay state. Update
handleSelectMunicipality to verify the id is a valid municipality (e.g., matches
the expected id pattern and/or exists in the local municipalities list/lookup
used by this hook) and only then call setSelectedMunicipalityId(muniId) and
setSelectedBarangayId(undefined); if invalid, leave state unchanged or set an
explicit "none" value and optionally surface an error. Ensure you reference and
use the same municipalities collection or validator used elsewhere in this hook
so invalid ids cannot be forwarded to Step2WhoWhere or submitted payloads.

In `@functions/src/callables/dispatch-responder-validation.ts`:
- Around line 93-107: The code unsafely casts rawStatus to the literal
'verified' before validating it; instead, validate rawStatus against the allowed
status values (or use an appropriate type guard/enum) and only assign to from as
a typed status (e.g., 'verified' | other) after isValidReportTransition check
passes; update the block around rawStatus, from and the isValidReportTransition
call so you do not force-cast rawStatus to 'verified' — perform a runtime check
(e.g., allowedStatuses.includes(rawStatus) or a guard function) then set from =
rawStatus with the correct narrowed type, and keep throwing
BantayogError(BantayogErrorCode.INVALID_STATUS_TRANSITION, ...) when validation
fails.

In `@functions/src/callables/dispatch-responder-writes.ts`:
- Around line 39-51: The interface WriteDispatchDocsArgs uses the global
FirebaseFirestore.DocumentReference for dispatchRef, reportRef, reportEvRef and
dispatchEvRef; replace those type annotations with the imported
DocumentReference from 'firebase-admin/firestore' and add the corresponding
import at the top of the file. Update the four fields in WriteDispatchDocsArgs
(dispatchRef, reportRef, reportEvRef, dispatchEvRef) to use DocumentReference
and ensure the file imports DocumentReference (e.g., import { DocumentReference
} from 'firebase-admin/firestore') so the types are explicit and consistent.

In `@packages/shared-sms-parser/src/parser.ts`:
- Around line 103-107: When token1 is a municipality prefix
(MUNICIPALITY_PREFIXES.has(token1)) extend the logic to look for a third token
(e.g., token3 = tokens[3]) and try a three-word barangay first: build candidate
= token1 + ' ' + token2 + ' ' + token3, and if present set barangayToken =
candidate and detailsStartIndex = barangayToken.length; if the three-word
candidate is not available or token3 is missing, fall back to the existing
two-word combination (token1 + ' ' + token2) as before. Ensure you reference and
update the same variables used in the diff (tokens, token2, token3,
MUNICIPALITY_PREFIXES, barangayToken, detailsStartIndex) and prefer the longest
match to avoid leaving part of the barangay in extracted details.

---

Duplicate comments:
In `@packages/shared-sms-parser/src/gazetteer.ts`:
- Around line 309-330: The regex in getBarangayGazetteer that tests the error
message for "Cannot find module ..." is too permissive; tighten it to explicitly
match the surrounding quote character so it only matches messages like "Cannot
find module '...'" or "Cannot find module \"...\"". Update the
isSharedDataLoadFailure test to use a character class for the quote (e.g.,
['\"]) around `@bantayog`\/shared-data, leaving the rest of the catch logic
(isModuleNotFound, returning FALLBACK_BARANGAYS, and rethrowing) unchanged and
keep references to mod.BARANGAY_GAZETTEER logic intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: db61995f-978b-43b2-a112-8f1cf8328ea4

📥 Commits

Reviewing files that changed from the base of the PR and between 29e4eee and 62930e1.

⛔ Files ignored due to path filters (6)
  • packages/shared-sms-parser/lib/gazetteer.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/inbound.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/levenshtein.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/levenshtein.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.js.map is excluded by !**/*.map
📒 Files selected for processing (17)
  • apps/citizen-pwa/src/components/SubmitReportForm/ContactFields.tsx
  • apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx
  • apps/citizen-pwa/src/hooks/useGpsLocation.test.ts
  • apps/citizen-pwa/src/hooks/useGpsLocation.ts
  • apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts
  • docs/progress.md
  • functions/src/callables/dispatch-responder-validation.ts
  • functions/src/callables/dispatch-responder-writes.ts
  • functions/src/callables/dispatch-responder.ts
  • functions/src/index.ts
  • packages/shared-sms-parser/lib/gazetteer.js
  • packages/shared-sms-parser/lib/levenshtein.js
  • packages/shared-sms-parser/lib/parser.js
  • packages/shared-sms-parser/src/gazetteer.ts
  • packages/shared-sms-parser/src/levenshtein.ts
  • packages/shared-sms-parser/src/parser.ts
  • packages/shared-ui/src/protected-route.tsx

Comment thread apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts
Comment thread functions/src/callables/dispatch-responder-validation.ts
Comment thread functions/src/callables/dispatch-responder-writes.ts
claude added 6 commits April 24, 2026 08:02
- Add VALID_MUNICIPALITY_IDS Set from CAMARINES_NORTE_MUNICIPALITIES
- Guard handleSelectMunicipality against invalid/out-of-set ids
… validation

- Remove premature 'verified' cast before isValidReportTransition check
- Pass rawStatus as ReportStatus to validate transition
- Assign from = 'verified' as const only after validation passes
…estore

- Replace global FirebaseFirestore.DocumentReference with explicit import
- Add DocumentReference to existing type imports
… prefix

- When token1 is a municipality prefix, prefer longest match:
  try token1 + token2 + token3 first, fall back to token1 + token2
- Prevents partial barangay names from leaking into details
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
functions/src/callables/dispatch-responder-validation.ts (1)

102-109: 🛠️ Refactor suggestion | 🟠 Major

Make the 'verified' precondition explicit before returning from: 'verified'.

Line 102 validates transition permissibility, but Line 109 still hard-codes from to 'verified' without an explicit runtime equality check. If additional * -> assigned transitions are introduced later, this silently mislabels from.

Proposed minimal hardening
   const to = 'assigned' as const
-  if (!isValidReportTransition(rawStatus as ReportStatus, to)) {
+  if (rawStatus !== 'verified' || !isValidReportTransition(rawStatus as ReportStatus, to)) {
     throw new BantayogError(
       BantayogErrorCode.INVALID_STATUS_TRANSITION,
       `Cannot dispatch from status ${rawStatus}`,
     )
   }
   // After validation, we know rawStatus === 'verified'.
   const from = 'verified' as const
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@functions/src/callables/dispatch-responder-validation.ts` around lines 102 -
109, The code validates the transition with isValidReportTransition(rawStatus as
ReportStatus, to) but then unconditionally sets from = 'verified'; make the
precondition explicit by checking rawStatus at runtime (e.g., assert or
if-check) that rawStatus === 'verified' before returning/assigning from, and
throw a BantayogError (or similar) if it isn't; update the block around
isValidReportTransition, rawStatus and the from constant to perform this
explicit equality check to avoid silently mislabeling from when other
transitions are added.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts`:
- Around line 50-57: The select change handler in useMunicipalityBarangays
treats only undefined as a clear action so an emitted empty string ('') doesn't
clear the selection; update the handler that takes (id: string | undefined) to
treat '' as a clear by checking for falsy/empty id (e.g., if (!id) or id ===
''), call setSelectedBarangayId(undefined) and return; keep the existing
barangayOptions.some((b) => b.name === id) branch for valid ids.

In `@functions/src/callables/dispatch-responder-validation.ts`:
- Around line 18-23: Change assertResponderOnShift to accept a single object
param instead of four positional args: replace (rtdb: Database, municipalityId:
string, responderUid: string, message?: string) with a single options object
type { rtdb, municipalityId, responderUid, message? } and update the function
body to destructure those properties; update all call sites to pass an object
(e.g. { rtdb, municipalityId, responderUid, message }) and adjust any
tests/mocks accordingly so argument ordering mistakes are prevented while
preserving the same runtime behavior and default message.

In `@functions/src/callables/dispatch-responder-writes.ts`:
- Around line 24-30: The code builds an SMS payload without checking explicit
sms consent; before reading phone/locale (use consentSnap, consentData,
recipientMsisdn, locale) add a strict consent check (e.g., require
consentData?.smsConsent === true) and return null if consent is missing or false
so no SMS is prepared for opted-out users; keep the existing phone/locale
validations after that check and fail fast if consent is not granted.

In `@packages/shared-sms-parser/src/gazetteer.ts`:
- Around line 311-314: The code returns mod.BARANGAY_GAZETTEER after only
checking Array.isArray, which allows malformed items that cause
entry.name.toLowerCase() runtime errors; update the logic that reads
mod.BARANGAY_GAZETTEER to validate each item shape (e.g., ensure each item is an
object and has a non-empty string 'name' property and any other required fields
for BarangayEntry), filter out or reject invalid items, and only return the
array cast to BarangayEntry[] after validation; reference the existing symbols
mod, BARANGAY_GAZETTEER, BarangayEntry and the consumer usage
entry.name.toLowerCase() when implementing the checks and emit a warning/log for
dropped items rather than silently swallowing errors.

---

Duplicate comments:
In `@functions/src/callables/dispatch-responder-validation.ts`:
- Around line 102-109: The code validates the transition with
isValidReportTransition(rawStatus as ReportStatus, to) but then unconditionally
sets from = 'verified'; make the precondition explicit by checking rawStatus at
runtime (e.g., assert or if-check) that rawStatus === 'verified' before
returning/assigning from, and throw a BantayogError (or similar) if it isn't;
update the block around isValidReportTransition, rawStatus and the from constant
to perform this explicit equality check to avoid silently mislabeling from when
other transitions are added.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 84165254-de8d-4f08-8d18-f83a917877c1

📥 Commits

Reviewing files that changed from the base of the PR and between 62930e1 and d9a438f.

⛔ Files ignored due to path filters (3)
  • packages/shared-sms-parser/lib/gazetteer.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.js.map is excluded by !**/*.map
📒 Files selected for processing (7)
  • apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts
  • functions/src/callables/dispatch-responder-validation.ts
  • functions/src/callables/dispatch-responder-writes.ts
  • packages/shared-sms-parser/lib/gazetteer.js
  • packages/shared-sms-parser/lib/parser.js
  • packages/shared-sms-parser/src/gazetteer.ts
  • packages/shared-sms-parser/src/parser.ts

Comment thread apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts
Comment thread functions/src/callables/dispatch-responder-validation.ts Outdated
Comment thread functions/src/callables/dispatch-responder-writes.ts
Comment thread packages/shared-sms-parser/src/gazetteer.ts
Comment thread packages/shared-sms-parser/src/parser.ts
claude added 4 commits April 24, 2026 08:16
…erified precondition

- Change assertResponderOnShift to accept single options object
  { rtdb, municipalityId, responderUid, message? }
- Update call sites in dispatch-responder-validation.ts and dispatch-responder.ts
- Add explicit rawStatus !== 'verified' check before returning from
…angays against gazetteer

- Add isBarangayEntry type guard to validate shape before returning
  BARANGAY_GAZETTEER from shared-data
- Move gazetteer lookup earlier in parser; build uppercase-name Set
- Only accept 3-word municipality-prefix barangay if it exists in
  the gazetteer; fall back to 2-word otherwise
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
functions/src/callables/dispatch-responder.ts (1)

57-65: ⚠️ Potential issue | 🟠 Major

Remove the pre-transaction shift read.

This call can still convert a missing or out-of-municipality responder into INVALID_STATUS_TRANSITION before validateDispatchTransaction() reaches its NOT_FOUND / FORBIDDEN checks. The in-transaction recheck already covers the real race window, so keeping this preflight read regresses error classification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@functions/src/callables/dispatch-responder.ts` around lines 57 - 65, Remove
the pre-transaction shift read by deleting the call to assertResponderOnShift
that runs before the transaction (the block using
deps.actor.claims.municipalityId and deps.responderUid); rely on the
in-transaction check inside validateDispatchTransaction to perform the shift /
municipality validation so error classification remains correct (avoid
converting missing/out-of-municipality responder into INVALID_STATUS_TRANSITION
outside the transaction).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@functions/src/callables/dispatch-responder.ts`:
- Around line 34-35: The severity guard in isValidSeverity currently uses the
`in` operator which matches inherited keys; change the check to use
Object.hasOwn(DEADLINE_BY_SEVERITY, s) so only own properties are accepted.
Update the function isValidSeverity(s: unknown): s is keyof typeof
DEADLINE_BY_SEVERITY to return typeof s === 'string' &&
Object.hasOwn(DEADLINE_BY_SEVERITY, s), ensuring DEADLINE_BY_SEVERITY lookups
yield numeric values (used later when computing deadlineMs) rather than
inherited functions.

In `@packages/shared-sms-parser/src/parser.ts`:
- Around line 20-25: The ParsedFields interface currently exposes an unused
rawBarangay property; either remove rawBarangay from ParsedFields or, if you
intend it for future debugging, add a clear doc comment explaining its purpose
and ensure callers expect it. Locate the ParsedFields interface (symbol:
ParsedFields) and remove the rawBarangay declaration and any references, and
update parseInboundSms callers and tests accordingly; alternatively add a JSDoc
comment above rawBarangay explaining it is optional/debug-only and ensure
lint/tests acknowledge it.

---

Duplicate comments:
In `@functions/src/callables/dispatch-responder.ts`:
- Around line 57-65: Remove the pre-transaction shift read by deleting the call
to assertResponderOnShift that runs before the transaction (the block using
deps.actor.claims.municipalityId and deps.responderUid); rely on the
in-transaction check inside validateDispatchTransaction to perform the shift /
municipality validation so error classification remains correct (avoid
converting missing/out-of-municipality responder into INVALID_STATUS_TRANSITION
outside the transaction).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 586a275d-35d7-4ccf-9c07-714596541217

📥 Commits

Reviewing files that changed from the base of the PR and between d9a438f and 33ddb00.

⛔ Files ignored due to path filters (4)
  • packages/shared-sms-parser/lib/gazetteer.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/gazetteer.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.js.map is excluded by !**/*.map
📒 Files selected for processing (8)
  • apps/citizen-pwa/src/hooks/useMunicipalityBarangays.ts
  • functions/src/callables/dispatch-responder-validation.ts
  • functions/src/callables/dispatch-responder-writes.ts
  • functions/src/callables/dispatch-responder.ts
  • packages/shared-sms-parser/lib/gazetteer.js
  • packages/shared-sms-parser/lib/parser.js
  • packages/shared-sms-parser/src/gazetteer.ts
  • packages/shared-sms-parser/src/parser.ts

Comment thread functions/src/callables/dispatch-responder.ts Outdated
Comment thread packages/shared-sms-parser/src/parser.ts
…for severity guard

- Remove pre-transaction assertResponderOnShift call; rely on in-transaction
  check inside validateDispatchTransaction for correct error classification
- Replace 'in' operator with Object.hasOwn in isValidSeverity to avoid
  matching inherited prototype properties
- Remove unused rawBarangay field from ParsedFields interface and tests
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/shared-sms-parser/src/parser.ts (1)

114-121: ⚠️ Potential issue | 🟠 Major

Details extraction is incorrect when the barangay name appears again in details.

Using lastIndexOf picks the final token occurrence, so inputs like BANTAYOG FLOOD ANGAS flooding in Angas area can truncate details to only the tail segment. This affects parsed incident context. (Also propagates to packages/shared-sms-parser/lib/parser.js Line 84-Line 90.)

Proposed fix (derive details by token position, not substring search)
-  // Find the barangay token in originalRest using lastIndexOf to prefer the occurrence
-  // closest to where details would start (avoids matching an earlier instance of the token).
-  const barangayTokenUpper = barangayToken.toUpperCase()
-  const barangayIndex = originalRest.toUpperCase().lastIndexOf(barangayTokenUpper)
-  const details =
-    barangayIndex !== -1 && barangayIndex + detailsStartIndex < originalRest.length
-      ? originalRest.slice(barangayIndex + detailsStartIndex).trim()
-      : undefined
+  // Compute details from token offsets to avoid false matches when barangay repeats in details.
+  const restOriginal = originalRest.replace(/^BANTAYOG\b\s*/i, '')
+  const restOriginalTokens = restOriginal.split(/\s+/)
+  const barangayWordCount = barangayToken.split(/\s+/).length
+  const details =
+    restOriginalTokens.length > 1 + barangayWordCount
+      ? restOriginalTokens.slice(1 + barangayWordCount).join(' ').trim()
+      : undefined
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared-sms-parser/src/parser.ts` around lines 114 - 121, The current
details extraction uses lastIndexOf and can pick a repeated barangay token
inside the details; change it to locate the barangay token by
token/word-boundary match and derive details from the match position rather than
using lastIndexOf. Specifically, replace the barangayIndex lookup (barangayIndex
= originalRest.toUpperCase().lastIndexOf(barangayTokenUpper)) with a regex-based
search on originalRest.toUpperCase() using word boundaries (e.g.,
\b{barangayTokenUpper}\b) to find the correct match index (prefer the first
match that represents the header token), then compute details using that match
index + detailsStartIndex against originalRest.slice(...).trim() (keeping the
existing variables barangayTokenUpper, originalRest, detailsStartIndex, and
assigning to details).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@functions/src/callables/dispatch-responder.ts`:
- Around line 83-91: The callable reads process.env.SMS_MSISDN_HASH_SALT and
will silently skip SMS if it's missing; update the dispatchResponder deployment
to surface this secret by creating a Secret via defineSecret (name
SMS_MSISDN_HASH_SALT) and adding it to the dispatchResponder's secrets array
(the same place FCM_VAPID_PRIVATE_KEY is bound), or alternatively
document/ensure it is provided as a deploy-time environment variable; also
verify buildSmsPayload and the salt usage still work once the secret is bound.
- Around line 186-191: The current claims construction strips municipality
information for tokens that lack a string municipalityId (breaking
provincial_superadmin which supplies permittedMunicipalityIds); update the
claims assembly in dispatch-responder.ts to include permittedMunicipalityIds
when municipalityId is not a string (e.g., add
...(Array.isArray(claims.permittedMunicipalityIds) ? { permittedMunicipalityIds:
claims.permittedMunicipalityIds } : {})), and update the earlier authorization
validation (the block that currently rejects missing municipalityId) to accept
either a string municipalityId or a non-empty permittedMunicipalityIds array for
roles including provincial_superadmin so the request is not incorrectly
rejected.

In `@packages/shared-sms-parser/src/__tests__/inbound.test.ts`:
- Line 10: Add a regression test to inbound.test.ts that exercises the parser
with a message where the barangay token appears twice (e.g., "ANGAS flooding in
Angas area") and assert that result.parsed?.details preserves the full string
rather than being truncated; locate the existing assertion using
expect(result.parsed?.details).toBeUndefined() and add a new test case that
calls the same parser helper (the function used to produce result in this file)
with the repeated-barangay input and checks
expect(result.parsed?.details).toEqual("flooding in Angas area") or the full
expected details string to ensure repeated tokens don’t remove or truncate
details.

---

Duplicate comments:
In `@packages/shared-sms-parser/src/parser.ts`:
- Around line 114-121: The current details extraction uses lastIndexOf and can
pick a repeated barangay token inside the details; change it to locate the
barangay token by token/word-boundary match and derive details from the match
position rather than using lastIndexOf. Specifically, replace the barangayIndex
lookup (barangayIndex =
originalRest.toUpperCase().lastIndexOf(barangayTokenUpper)) with a regex-based
search on originalRest.toUpperCase() using word boundaries (e.g.,
\b{barangayTokenUpper}\b) to find the correct match index (prefer the first
match that represents the header token), then compute details using that match
index + detailsStartIndex against originalRest.slice(...).trim() (keeping the
existing variables barangayTokenUpper, originalRest, detailsStartIndex, and
assigning to details).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3869c806-79a0-4f03-8805-741f545f4d5d

📥 Commits

Reviewing files that changed from the base of the PR and between 33ddb00 and c78a720.

⛔ Files ignored due to path filters (3)
  • packages/shared-sms-parser/lib/__tests__/inbound.test.js.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.d.ts.map is excluded by !**/*.map
  • packages/shared-sms-parser/lib/parser.js.map is excluded by !**/*.map
📒 Files selected for processing (7)
  • functions/src/__tests__/sms-inbound.test.ts
  • functions/src/callables/dispatch-responder.ts
  • packages/shared-sms-parser/lib/__tests__/inbound.test.js
  • packages/shared-sms-parser/lib/parser.d.ts
  • packages/shared-sms-parser/lib/parser.js
  • packages/shared-sms-parser/src/__tests__/inbound.test.ts
  • packages/shared-sms-parser/src/parser.ts
💤 Files with no reviewable changes (1)
  • functions/src/tests/sms-inbound.test.ts

Comment thread functions/src/callables/dispatch-responder.ts
Comment thread functions/src/callables/dispatch-responder.ts
Comment thread packages/shared-sms-parser/src/__tests__/inbound.test.ts
…ncial_superadmin

- parser.ts: Replace lastIndexOf with word-boundary regex to find first
  barangay occurrence, preventing truncation when barangay repeats in details
- dispatch-responder.ts: Define SMS_MSISDN_HASH_SALT via defineSecret and
  add to onCall secrets array so Firebase injects it in production
- dispatch-responder.ts + validation.ts: Support provincial_superadmin by
  accepting permittedMunicipalityIds array; use getActorMunicipalityIds()
  helper for unified municipality checks; use responder.municipalityId for
  RTDB shift lookup
- inbound.test.ts: Add regression test for repeated barangay tokens
@Exc1D Exc1D merged commit 0211c04 into main Apr 24, 2026
14 checks passed
@Exc1D Exc1D deleted the refactor/audit-2026-04-23-continuation branch April 24, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants