Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@ Asset Declaration Portal (ADLA) for Ghana's Article 286(5) compliance. Three act

`Declaration.status` drives the workflow: `CODE_GENERATED → FORM_COLLECTED → SUBMITTED → UNDER_REVIEW → APPROVED|REJECTED → SEALED → COMPLETED`. The applicant only initiates the declaration (`CODE_GENERATED`); a Schedule Officer/Admin drives `FORM_COLLECTED` (form collected from a `CollectionOffice`), `SUBMITTED` (filled form returned), and every later transition. A rejection issues a new unique code and a fresh `CODE_GENERATED` declaration (the applicant cannot have another active declaration while one is `CODE_GENERATED`/`FORM_COLLECTED`/`SUBMITTED`/`UNDER_REVIEW` — see `app/server/api/declarations/index.post.ts`).

`Declaration.reviewStatus` (`DeclarationReviewStatus`: `NEW → ISSUES_IDENTIFIED → ISSUES_RESOLVED`) is a **sub-status that tracks review progress while `status` stays `UNDER_REVIEW`**. `POST /api/reviews` flips it to `ISSUES_IDENTIFIED` when a section review flags issues; `PATCH /api/reviews/sections/[id]/resolve` flips it to `ISSUES_RESOLVED` once the last unresolved issue is cleared, signalling the declaration is ready for final approval.

**Lost form reissue:** while a declaration is `FORM_COLLECTED`, if the applicant loses the physical form they can submit a tracked reissue request (`FormReissueRequest`, status `PENDING→APPROVED|DECLINED`) from the declaration detail page. The applicant takes an offline letter to the Auditor General or a Regional Auditor; once approved, a Legal Unit officer records the decision in one combined action (uploads the scanned approved letter, picks the approver, optional reasons) which reissues the form. The declaration **stays `FORM_COLLECTED`** throughout (no status change) — the applicant then returns the reissued form via the normal Form Return step. All requests/decisions are recorded and surface on the declaration timeline via `DeclarationStatusHistory`.

**Applicant verification & info requests:** the Legal Unit reviews an applicant's profile/identity via `ApplicantVerificationReview` (`VerificationStatus`, including `MORE_INFO_REQUIRED`). When a review is set to `MORE_INFO_REQUIRED`, the applicant can upload supporting documents (`VerificationDocument`, stored in MinIO, linked back to the prompting `reviewId`) from their profile via `POST /api/applicant/verification/documents` — gated so uploads are only accepted while the latest review is outstanding (magic-byte file validation, max 5 docs). The reviewer then sees exactly what was supplied in response.

## Architecture: how the layers wire together

### Auth is enforced in two places, and they must stay aligned
Expand Down Expand Up @@ -97,9 +101,16 @@ State-changing endpoints follow a consistent pattern: validate auth → validate
- **Request validation**: `app/server/utils/validators.ts` — `validateBody(event, schema)` parses `readBody()` against a Zod schema and throws a 400 with `error.flatten()` on failure. All Zod schemas for the domain live in this file; add new ones here rather than inline in route handlers.
- `app/server/utils/audit.ts` — `createAuditLog(event, { userId, action, entityType, entityId, oldValues?, newValues? })` plus an `AuditActions` enum. Call it on every state transition; `audit_logs` is a compliance requirement, not a debug aid. When Redis is configured, the helper publishes to the `audit-logs` BullMQ queue drained by `server/plugins/audit-worker.ts` with retries; persistent failures sit in BullMQ's `failed`-set (7 days) for operator review. Falls back to an inline best-effort write when the queue is off or the enqueue throws.
- `app/server/utils/code-generator.ts` — `generateUniqueCode()` for declaration codes (with collision retry at the call site).
- `app/server/services/notification.service.ts` — high-level helpers like `notifyUniqueCodeGenerated`. The service fans out to `email.service.ts` / `sms.service.ts` based on `NotificationPreference` and writes `NotificationDeliveryLog` rows. Don't call `email.service` or `sms.service` directly from route handlers.
- `app/server/services/notification.service.ts` — high-level helpers like `notifyUniqueCodeGenerated`. The service fans out to `email.service.ts` / `sms.service.ts` based on `NotificationPreference` and writes `NotificationDeliveryLog` rows. Don't call `email.service` or `sms.service` directly from route handlers. Email/SMS delivery is **gated on verified contacts**: when the `notifications.requireVerifiedEmail` / `notifications.requireVerifiedPhone` settings are on (default), an unverified `User.emailVerified`/`User.phoneVerified` receives the in-app notification only — email/SMS is skipped. Security/transactional types (password reset, email verification) always bypass the gate. In-app notifications are never gated.
- `app/server/services/storage.service.ts` — MinIO uploads (Ghana Card images, receipt PDFs).
- `app/server/services/pdf.service.ts` — receipt generation via `pdf-lib`.
- `app/server/utils/phone.ts` — Ghana phone normalization. Users may supply either local (`0XXXXXXXXX`) or international (`+233XXXXXXXXX`) format; both are accepted and normalized. `sms.service.ts` routes Ghana numbers (`isGhanaPhone`) to local providers (Hubtel/Arkesel) using `formatGhanaPhoneLocal` (`0…`, no country code), and everything else to Twilio.

### Admin-managed global settings (DB override → env fallback)

Runtime behaviour toggles an admin can flip from **Admin → Settings** without a redeploy live in `app/server/utils/system-settings.ts`. Every flag is declared once in the `SETTING_FIELDS` registry (key, kind, env var, bounds, coercion, env fallback) and resolves through `getSetting(key)`: a valid `SystemSetting` DB row wins, otherwise the existing env/runtimeConfig value is used (absence of a row means "use env"). Values are plaintext (these are non-secret operational flags, unlike `NotificationCredential`). A 30s in-memory cache keeps the hot path (login, security headers) off the DB; the write endpoint calls `invalidateSettingsCache()` for same-pod immediacy and cross-pod converges within the TTL. `getSetting` never throws on a bad row — it falls through to env.

Registered settings: `auth.requireEmailVerificationForLogin`, `auth.requirePhoneVerificationForLogin`, `declarations.requireEmailVerification`, `declarations.requirePhoneVerification`, `security.cspEnforce`, `notifications.rateLimitPerHour`, `notifications.requireVerifiedEmail`, `notifications.requireVerifiedPhone`. To add one: add a field to `SETTING_FIELDS` and read it via `getSetting` at the consuming site — it auto-appears on the admin page. Admin endpoints: `app/server/api/admin/settings/index.get.ts` (status + resolution source per field) and `index.put.ts` (write/bounds-check).

### Server middleware execution order

Expand Down Expand Up @@ -128,6 +139,7 @@ The subsystem is toggled by `ANALYTICS_ENABLED`. Individual features (`captureEn
- Layouts: `default.vue` (public), `auth.vue` (login/register), `dashboard.vue` (post-login app shell).
- **API calls**: client-side code uses `authFetch()` from `app/utils/authFetch.ts` (or the reactive `useApiFetch()` composable). `authFetch` attaches the Bearer token, transparently retries on 401 with a single token refresh, and redirects to login on failure. Don't use raw `$fetch` or `useFetch` for authenticated endpoints.
- State: Pinia stores in `app/stores/` — `auth.ts` persists the token pair to `localStorage` under `adla_tokens` and exposes `isApplicant`/`isOfficer`/`isLegalUnit`/`isAdmin` computed flags used by both the client middleware and dashboard redirects.
- **Multi-role users** (a user holding more than one role) get an "Acting as" switcher in the profile menu (`components/app/HeaderActions.vue`). `auth.ts` tracks a persisted `activeRole` (localStorage `adla_active_role`, restored in `plugins/auth.ts`) and derives `effectiveRole` (the active role if still held, else the highest-precedence held role from `availableRoles`). The `isApplicant`/`isOfficer`/… flags and dashboard routing key off `effectiveRole`, so switching strictly scopes nav, dashboard, and route access to one role.
- UI: shadcn-vue is configured (`shadcn-nuxt` module, `componentDir: ./components/ui`, `prefix: ""`). The directory is empty until you add components with `npx shadcn-vue@latest add <component>`.
- Tailwind v4 via `@tailwindcss/vite` plugin (no `tailwind.config.js`); main stylesheet is `app/assets/css/main.css`.
- TypeScript is strict (`typescript.strict: true` in `nuxt.config.ts`) and `future.compatibilityVersion: 4` is on — code should target Nuxt 4 idioms.
Expand Down