# Grant Management System (GMS) — Architecture & Roadmap

This document is the **living** reference for how NGO ERP implements grant-centric operations in a **multi-tenant SaaS** model (Laravel API + Next.js). Update it when migrations, routes, or major UI surfaces change.

---

## 1. Purpose & principles

| Principle | Meaning in this repo |
|-----------|----------------------|
| **Grant as hub** | Funded activity (money, reporting, future spend) should remain traceable to a **grant** and its **budget lines** unless the product explicitly treats unrestricted general funds as first-class. |
| **SaaS only** | One codebase, many **organisations** (`organizations` + `users.organisation_id`). No per-customer forks—extend via **config**, **seeded defaults**, and **tenant-defined data** (dedicated tables per process, not a generic forms builder). |
| **Structure over legal text** | Encode deadlines, amounts, statuses, and approvals—not donor legal clauses. Narrative lives in attachments or free-text fields. |
| **Dynamic workflows (direction)** | Approvals and stages are **definition-driven** where the engine applies (`approval_definitions`); other flows use **controlled vocabulary** + dedicated tables (see roadmap). |
| **Employee portal** | Staff-first home at **`/me`** with live widgets from **`GET /api/me/overview`**; **`GET /api/user`** exposes `workspace_layout` (`full` \| `employee`) and HR **employment** summary when applicable. **Leave** stays on **`leave_requests`** / **`/hr`**. |

Related Cursor skills: `.cursor/skills/ngo-erp-saas-builder-persona/SKILL.md`, `.cursor/skills/senederp-ngo-grant-hub/SKILL.md`, `.cursor/skills/ux-ui-rich-product-task-breakdown/SKILL.md`.

---

## 2. Current implementation (as-built)

### 2.1 Tenancy & identity

| Concern | Location |
|---------|----------|
| Organisations | `app/Models/Organization.php`, `database/migrations/2026_04_15_190043_create_organizations_table.php` |
| Users (belong to org) | `app/Models/User.php`, `database/migrations/` (users + `organisation_id`) |
| API auth | Sanctum — `routes/api.php` (`login`, `register`, `auth:sanctum` group) |

### 2.2 Grant hub (implemented)

| Layer | Files |
|-------|--------|
| **Migrations** | `database/migrations/2026_04_15_210000_create_grants_table.php`, `…210001_create_grant_budget_lines_table.php`, `…210002_create_grant_amendments_table.php` |
| **Models** | `app/Models/Grant.php`, `GrantBudgetLine.php`, `GrantAmendment.php`; `Organization::grants()` |
| **Policy** | `app/Policies/GrantPolicy.php` |
| **HTTP** | `app/Http/Controllers/Api/GrantController.php`, `GrantBudgetLineController.php`, `GrantAmendmentController.php` |
| **Validation** | `app/Http/Requests/StoreGrantRequest.php`, `UpdateGrantRequest.php`, `StoreGrantBudgetLineRequest.php`, `UpdateGrantBudgetLineRequest.php`, `StoreGrantAmendmentRequest.php` |
| **Routes** | `routes/api.php` — `GET /api/grants/meta`, `apiResource('grants')`, nested `grants.budget-lines`, `GET|POST …/amendments` |
| **Tenancy guard** | `Grant::resolveRouteBinding`, `GrantBudgetLine::resolveRouteBinding` (scoped to authenticated user’s `organisation_id`) |
| **Tests** | `tests/Feature/GrantApiTest.php` |
| **Demo data** | `database/seeders/SenedOrganizationSeeder.php` (org `sened`, UUID `1274260b-5c8b-492e-9eb4-1795b5bf46e6` for stable platform URLs, primary admin from seeder config, sample grants/lines/amendment). Seeds **module flags** (travel, workplace, attendance core/IP/QR on; face/geo off) and a demo **attendance site** “Sened HQ (demo)”. Also seeds an **HR demo workforce**: line managers (`miriam.line@sened.demo` programme, `jean.line@sened.demo` finance), HR-only `hr.coordinator@sened.demo`, and staff with `reports_to_user_id` set to those managers (`jacob.field@`, `grace.field@`, `peter.finance@`, `anna.readonly@` — all `@sened.demo`, password **`password`** for local use). |

### 2.3 Employee portal & staff layout (implemented — MVP)

| Concern | Location |
|---------|----------|
| **Persona flags** | `User::usesFullWorkspaceNav()`, `workspace_layout` + `can_access_operations_nav` + `can_view_grants` in `User::toApiArray()` |
| **Overview API** | `GET /api/me/overview` — `App\Http\Controllers\Api\Me\MeOverviewController` (counts for approvals inbox, HR leave/time, grants, optional travel + attendance blocks) |
| **Frontend** | `frontend/src/app/me/page.tsx`; `dashboard-shell.tsx` builds **reduced nav** when `workspace_layout === "employee"`; `/dashboard` redirects employees to `/me` |
| **Tests** | `tests/Feature/UserWorkspaceLayoutApiTest.php`, `tests/Feature/MeOverviewApiTest.php` |

### 2.35 HR module (implemented — bounded context)

| Layer | Notes |
|-------|--------|
| **Module** | Tenant flag `hr` in `config/modules.php`; legacy `organisation_modules.module_key` value `hr_timesheets` is migrated to `hr` in `database/migrations/2026_04_16_200000_hr_module_time_and_leave.php`. |
| **Time** | Table `hr_time_entries` (`uuid`, `organisation_id`, optional `grant_id` / `grant_budget_line_id`, `user_id`, `worked_on`, `hours`, `notes`). Data migrated from removed `grant_time_entries`. API: `GET|POST /api/hr/time-entries`, `PATCH|DELETE /api/hr/time-entries/{uuid}` under `module:hr`. `GET` supports `?team=1` and optional `user_id` for **organisation admin** or **`manageHrEmployees`** (`OrganizationPolicy::manageHrEmployees`: org admin or `OrgRole::HR`). Grant allocation is optional when **Grants** (and **Budgets** for line picker) are enabled. |
| **Leave** | Tables `leave_requests` (uuid, dates, `leave_type`, optional `hr_leave_type_id`, `status`, optional `approval_instance_id`), `hr_leave_types`, `hr_leave_entitlements`. API: `GET|POST /api/hr/leave-requests`, `GET|PATCH …/{uuid}`, `POST …/{uuid}/submit`. Drafts resolve `hr_leave_type_uuid` (or legacy `leave_type` code). Submit calls `HrLeaveBalanceService::assertSubmitDoesNotExceedAllocated` (calendar vs **working** days per type `rules.day_counting`). Uses shared **approval engine** with `document_type` `leave_request`. If **Approvals** is off, submit records leave as **approved** without a chain. `GET /api/hr/leave-requests?team=1` lists the whole org for HR/admin. |
| **Leave admin** | `GET|POST /api/hr/leave-types`, `PATCH|DELETE /api/hr/leave-types/{uuid}`. `GET /api/hr/leave-entitlements?year=&user_id=`. `POST /api/hr/leave-entitlements`, `PATCH …/{uuid}`. `GET /api/hr/leave-balances?year=&user_id=`. Mutations require **`manageHrEmployees`**. **Pro-rata:** when `users.hr_fte` is set (0–1), `HrLeaveBalanceService` scales **allocated** days for balance display and submit validation (default rule; tenant-specific tables can extend later). **Life-event types** (civic, jury, bereavement, study) are seeded per org via migration `2026_04_23_100000_seed_hr_life_event_leave_types`. |
| **Work calendars (tenant-defined)** | `hr_work_calendars`, `hr_calendar_days`; org defaults on `organizations` (`hr_jurisdiction_country_code`, `hr_jurisdiction_region_code`, `hr_default_timezone`, `hr_default_work_calendar_id`); user overrides on `users` (`hr_jurisdiction_*`, `hr_timezone`, `hr_work_calendar_id`). `GET|POST /api/hr/work-calendars`, `PATCH|DELETE …/{uuid}`, `GET …/{uuid}/days?from=&to=`, `POST|PATCH|DELETE …/{uuid}/days` (manual exception days; idempotent CSV/ICS upsert via `whereDate`). `POST …/{uuid}/import` (`format`: `csv` \| `ics`, `payload` body). `GET|PATCH /api/hr/organization/work-settings` for org-wide HR location/timezone/default calendar. `GET /api/hr/working-day-preview` — preview calendar vs working-day counts. |
| **HR roster** | `GET /api/hr/employees?reports_to={user_id}` (optional: direct reports of a line manager), `GET /api/hr/employees/{id}` — payload includes `reports_to: { id, name } \| null` plus `role_keys`, HR jurisdiction fields, and **employment band** (`hr_employment_type`, `hr_fte`, `hr_contracted_hours_per_week`, effective dates). `PATCH /api/hr/employees/{id}/reporting`, `PATCH /api/hr/employees/{id}/hr-profile`. **Organisation admin** or **HR role** only. HR may also `PATCH /api/organization/users/{id}/reporting` (same rules as `manageSettings` for finance). |
| **Directory** | `GET /api/hr/directory` — id + name only (`OrganizationPolicy::viewMemberDirectory`). Full user list with email for assignee pickers stays on `GET /api/organization/users` (settings admins / finance). |
| **Roles** | `OrgRole::HR`; catalogue row `hr.manage_employees` in `OrgRolesPermissionsCatalog`. |
| **Policies** | `HrTimeEntryPolicy`, `LeaveRequestPolicy` (HR/admin team **view** on others’ leave & time), `OrganizationPolicy::manageHrEmployees` |
| **Frontend** | `frontend/src/app/hr/**` (overview, time, leave, people, **employees**); **Settings → HR & leave** (`/settings/hr-leave`) for calendars, org defaults, leave types, preview. Nav in `dashboard-shell.tsx`. Grant workspace **Time** tab removed. |
| **Tests** | `tests/Feature/HrApiTest.php`, `HrEmployeesAndCalendarsTest.php`, `OrganizationUserReportingTest` (HR reporting), `ApprovalDefinitionModuleGateTest` (leave + `hr` gating) |

### 2.36 Travel (`travel` module)

| Layer | Notes |
|-------|--------|
| **Tables** | `business_trips` (clearance + optional `trip_report`, `checklist` JSON) |
| **API** | `GET|POST /api/travel/trips`, `PATCH /api/travel/trips/{uuid}`, `POST …/submit`, `POST …/transition` under `module:travel` |
| **Approvals** | `document_type` **`business_trip`**; gated by `ApprovalDefinitionModuleGate` to **`travel`** |
| **UI** | `frontend/src/app/travel/page.tsx` (list); portal widget when travel enabled |

### 2.37 Attendance (`attendance_core` + method modules)

| Module key | Purpose |
|------------|---------|
| `attendance_core` | Sites (`attendance_sites`), user **records** (`attendance_records`), self check-in/out |
| `attendance_ip` | Per-site CIDR allowlists (`attendance_site_ip_allowlists`); `POST /api/attendance/ip/check-ins` |
| `attendance_qr` | Kiosk devices (`attendance_devices`); per-user `attendance_qr_token` on `users`; public `POST /api/public/attendance/qr/kiosk-check-in`; `GET /api/me/attendance-personal-qr` |
| `attendance_face` | Consent-first enrollment stub (`attendance_face_enrollments`, `POST /api/attendance/face/enrollments`) |
| `attendance_geo` | **Reserved** module key (geofence attestation — not implemented in API yet) |

Admin site CRUD: `GET|POST /api/attendance/sites` (`module:attendance_core`, `manageHrEmployees`).

### 2.4 Frontend (implemented)

| Surface | Path |
|---------|------|
| Grant list + filters | `frontend/src/app/grants/page.tsx` |
| New grant | `frontend/src/app/grants/new/page.tsx` |
| Grant workspace (overview, **Budget lines** tab, amendments) | `frontend/src/app/grants/[uuid]/page.tsx` |
| Budget lines panel (totals vs award, inline edit, add/delete) | `frontend/src/components/grant-budget-lines-panel.tsx` |
| Employee portal home | `frontend/src/app/me/page.tsx` |
| Types | `frontend/src/types/grants.ts`, `frontend/src/types/workspace-user.ts` |
| Money formatting | `frontend/src/lib/money.ts` (`formatMoney`) |
| Shell / nav | `frontend/src/components/dashboard-shell.tsx` |
| Status badge | `frontend/src/components/grant-status-badge.tsx` |
| Amendment readout | `frontend/src/components/amendment-changes-readout.tsx` |
| API client | `frontend/src/lib/api.ts` |

### 2.5 Behavioural summary (today)

- **Grants:** CRUD-style API; **archive** via `DELETE` (`archived_at`); **restore** via `PATCH` with `archived_at: null`.
- **Budget lines:** nested CRUD; amounts, categories, optional description and sort order; no **versioned budget** yet—single active set per grant. Grant UI surfaces allocation vs `amount_awarded` in `GrantBudgetLinesPanel`.
- **Amendments:** append-only records with `effective_on`, `summary`, optional structured `changes` JSON; UI builds common deltas without raw JSON (dates/amount + “other” text).
- **Employee portal:** `/me` aggregates **pending approvals you can act on**, **leave** drafts/pending, **recent time**, optional **grants** count (aligned with `GrantPolicy::view` via `Grant::viewableBy($user)`), **travel** triage, and **open attendance** when the corresponding modules are on (`GET /api/me/overview`). **Self check-in:** employees can list sites with `GET /api/me/attendance-sites` (when `attendance_core` is on) and post `POST /api/attendance/check-ins`; each **attendance site** may set `allowed_methods` (`self_attest`, `ip`, `qr_kiosk`) intersected with tenant modules — enforced in check-in and QR redeem paths.
- **Step-up:** `POST /api/me/step-up` re-checks the account password. For **Bearer (Sanctum personal access) tokens**, verification is stored in **cache** keyed by the current token id (15-minute TTL) so SPAs without a session cookie still receive `step_up_verified_at` in the JSON body. If there is no current API token (e.g. cookie session), the legacy **session** key `step_up_verified_at` is used instead.
- **Field / voice / workplace (stubs):** tenant tables created for WFH declarations, mileage, certifications, grievance intake, bank change requests, training pulse, DEI self-ID, anonymous suggestions, union acks, desk bookings, visitor passes, equipment issues, overtime requests, parental return checklist, wellbeing preferences — **`POST /api/me/*`** routes in `MePortalExtensionsController`; **desk/visitor/equipment** additionally require `module:workplace`.
- **Meta:** `GET /api/grants/meta` supplies vocabulary for selects (funding, status, reporting frequency).

### 2.6 Modular SaaS spine (per-organisation modules)

| Concern | Location |
|---------|----------|
| Canonical module keys + defaults | `config/modules.php` (`keys`, `default_enabled`, `requires` — e.g. `budgets` ⇒ `grants`) |
| Per-tenant flags | `organisation_modules` + `organisation_module_audits` (`App\Models\OrganisationModule`, `OrganisationModuleAudit`) |
| Resolution / seeding | `App\Services\TenantModuleService` |
| HTTP gates | `App\Http\Middleware\EnsureOrganisationModuleEnabled` (`module:…` on `routes/api.php` route groups) |
| User payload | `User::toApiArray()` includes `enabled_modules` |
| Platform operator | `users.is_platform_admin`; routes under `auth:sanctum` + `platform.admin`; `RejectPlatformActorFromTenantApi` blocks tenant APIs for platform accounts |
| Platform APIs | `GET/PATCH /api/platform/organizations`, `PATCH …/{organization:uuid}/modules` — `App\Http\Controllers\Api\Platform\PlatformOrganizationController` |
| Org UUID (outward) | `organizations.uuid` + `Organization::getRouteKeyName()` |
| Branding | `organizations` columns + `GET|POST /api/organization/branding`; signed `GET /api/public/organizations/{organization:uuid}/logo` |
| Target layout (incremental) | `app/Modules/*`, `frontend/src/modules/*` (placeholders for new feature code) |

---

## 3. Target logical architecture (modular monolith → services when needed)

```mermaid
flowchart LR
  subgraph client [Next.js]
    UI[App Router UI]
  end
  subgraph api [Laravel API]
    GMS[GMS: grants / budgets / amendments]
    IAM[Identity & org]
    PORTAL[Employee portal + HR/travel]
    FIN[Future: transactions & encumbrance]
    DOC[Future: documents]
    WF[Future: workflow engine]
  end
  subgraph data [Data]
    PG[(PostgreSQL / SQLite dev)]
    OBJ[(Object storage future)]
  end
  UI -->|REST| api
  IAM --> PG
  GMS --> PG
  PORTAL --> PG
  FIN --> PG
  DOC --> OBJ
```

**Recommendation:** keep **Laravel + Next** as the **modular monolith** until traffic or team boundaries force extraction. Introduce **bounded contexts** as namespaces/modules (`App\Domain\Grant\…`) before splitting services.

---

## 4. Conceptual data model (ERD)

**Solid lines = implemented in DB today.** Dashed = roadmap (e.g. transactions).

```mermaid
erDiagram
  ORGANIZATION ||--o{ USER : has
  ORGANIZATION ||--o{ GRANT : owns
  GRANT ||--o{ GRANT_BUDGET_LINE : budgets
  GRANT ||--o{ GRANT_AMENDMENT : amends
  USER ||--o{ GRANT_AMENDMENT : records
  ORGANIZATION ||--o{ BUSINESS_TRIP : travel
  USER ||--o{ BUSINESS_TRIP : requests
  GRANT_BUDGET_LINE }o..o{ TRANSACTION_ALLOCATION : future
```

**Implemented tables (simplified):**

- `organizations`, `users`, `grants` (org-scoped unique `code`, lifecycle, funding, currency, amounts, dates, `metadata`, `archived_at`).
- `grant_budget_lines` (`grant_id`, unique `line_code`, amounts, category, `description`, `sort_order`, `metadata`).
- `grant_amendments` (`grant_id`, `user_id`, `effective_on`, `summary`, `changes` JSON).
- `business_trips` (org + traveller, dates, status, optional `grant_id`, `approval_instance_id`, report fields, `checklist` JSON).
- `attendance_sites`, `attendance_site_ip_allowlists`, `attendance_devices`, `attendance_records`, `attendance_face_enrollments`.
- Portal extension tables (`user_wfh_declarations`, `user_mileage_logs`, `grievance_intakes`, …) — see migration `2026_04_17_160003_create_portal_extension_tables.php`.
- `hr_time_entries` (org-scoped time; optional `grant_id` / `grant_budget_line_id` for funded effort).
- `leave_requests` (HR leave; `approval_instance_id` when using approval engine).

---

## 5. API map (current vs planned)

| Area | Current | v1 target | Enterprise target |
|------|---------|-----------|---------------------|
| Grants | REST under `/api/grants` | + pagination filters stable contract; OpenAPI export | GraphQL `grantWorkspace` for composite screens |
| Budget | Single version per grant | `budget_versions` + approved snapshot | Real-time balances, encumbrance |
| Amendments | POST with structured `changes` | Validation rules per change type; link to document | Workflow-approved amendments only |
| Employee portal | `GET /api/user` (layout flags), `GET /api/me/overview`, `POST /api/me/*` extension routes | Richer widgets, notifications digest | Unified “my tasks” across document types |
| HR | `/api/hr/time-entries`, `/api/hr/leave-requests`, `/api/hr/directory` (`module:hr`); optional grant FK on time; employment fields on user + HR profile | Balances, periods, exports | HRIS connectors |
| Travel | `/api/travel/trips` (`module:travel`); approvals `business_trip` | Per-diem, itinerary, expense handoff | Mobile offline (stretch) |
| Attendance | `/api/attendance/*` split by `attendance_core` + `attendance_ip` / `attendance_qr` / `attendance_face` | Geo module `attendance_geo`, break/overtime flows | Vendor biometrics, hardware integrations |
| Actuals | — | `transactions` + `allocations` | ERP outbox, idempotent journals |

---

## 6. Phased roadmap (mapped to this monolith)

### Phase A — **MVP (foundation)** ✅ largely complete

**Goal:** One organisation can authenticate, maintain grants, budget lines, and amendment history with a usable UI.

| # | Deliverable | Repo alignment |
|---|-------------|----------------|
| A1 | Multi-tenant org + user | `Organization`, `User`, registration |
| A2 | Grant CRUD + archive + meta | `GrantController`, requests, policy, migrations |
| A3 | Budget lines nested CRUD | `GrantBudgetLineController`, migrations |
| A4 | Amendments + human-friendly change capture | `GrantAmendmentController`, `[id]/page.tsx`, `amendment-changes-readout.tsx` |
| A5 | Frontend grant workspace | `frontend/src/app/grants/**` |
| A6 | Automated tests for tenancy | `GrantApiTest.php` |

**Exit criteria:** All A-row features covered by smoke tests; no cross-tenant data leak on grant routes.

---

### Phase B — **v1 (operational GMS)**

**Goal:** NGOs can run day-to-day grant **operations** beyond static master data: structured operational records (HR, travel, attendance), stronger budget discipline, exports, and light RBAC.

| # | Deliverable | Suggested implementation notes |
|---|-------------|--------------------------------|
| B1 | **Roles & permissions** | Spatie Permission or native `roles`/`permissions` tables; policies on `Grant`, `BudgetLine`, HR/travel models; “finance vs programme” |
| B2 | **Operational capture (no generic Forms product)** | **Done (direction):** HR leave/time, travel trips, attendance shells + portal; future intakes as **named** bounded contexts |
| B3 | **Budget versioning** | `budget_versions` (draft/approved), `budget_lines.version_id`; grant detail UI version selector; migration path from current lines → v1 approved |
| B4 | **Spend / commitment tracking (minimal)** | `transactions` + `transaction_budget_line_allocations`; validate sum vs line ceiling; no full GL yet |
| B5 | **Reporting exports** | CSV/XLSX export for grant list, budget lines, amendments; PDF optional (Laravel Snappy / Gotenberg) |
| B6 | **Document metadata** | `documents` table + S3 disk; link `grant_id`, `amendment_id`; virus scan queue |
| B7 | **Audit event log** | `audit_events` polymorphic (`auditable_type`, `auditable_id`, `actor_id`, `payload`) for all mutations |

**Exit criteria:** Finance user cannot post spend without permission; HR/travel/attendance records remain tenant-scoped and policy-enforced; approved budget version immutable.

---

### Phase C — **Enterprise (scale & ecosystem)**

**Goal:** UN-scale portfolios, strict compliance, deep integrations, high availability.

| # | Deliverable | Notes |
|---|-------------|--------|
| C1 | Workflow engine (BPMN / Temporal) | Replace ad-hoc status transitions for sensitive flows |
| C2 | Multi-currency FX | Rate tables, revaluation policy, functional currency reporting |
| C3 | Integration layer | ERP/accounting connectors, webhooks, idempotent id keys |
| C4 | GraphQL BFF | Lighthouse or Node gateway for heavy dashboards |
| C5 | Search & analytics | OpenSearch; warehouse sync |
| C6 | HA / DR / data residency | Multi-region, tenant pinning, backup drills |
| C7 | M&E / LogFrame module | Indicators linked to grant + budget utilisation |
| C8 (stretch) | Multi-org consultant identity + offline field capture | Architecture-only until funded — hard tenant isolation is mandatory |

**Exit criteria:** SOC2/ISO control matrix mapped to features; RTO/RPO documented; integration certification path for one major ERP.

---

## 7. File-by-file alignment (quick index)

When you extend GMS, prefer touching these areas first:

| Concern | Backend | Frontend |
|---------|---------|----------|
| New grant field | `…210000_create_grants_table.php` (new migration), `Grant.php`, `Store/UpdateGrantRequest`, `Grant::toDetailArray` | `types/grants.ts`, `grants/new`, `grants/[id]` overview |
| Budget rule | `GrantBudgetLine` + requests + controller | `[id]` **Budget lines** tab → `grant-budget-lines-panel.tsx` |
| Employee portal | `MeOverviewController`, `User::toApiArray()` | `/me`, `workspace-user.ts`, `dashboard-shell.tsx` |
| Amendment shape | `StoreGrantAmendmentRequest`, `GrantAmendment` | `addAmendment` + `AmendmentChangesReadout` |
| New API route | `routes/api.php` + policy + test | `apiFetch` callers |
| Vocabulary for UI | `Grant::vocabulary()`, `GrantController@meta` | `GET /api/grants/meta` consumers |

---

## 8. Maintenance checklist (edit this doc when…)

- [ ] New migration alters `grants`, `grant_budget_lines`, `grant_amendments`, `hr_time_entries`, `leave_requests`, `business_trips`, or attendance tables
- [ ] New public route under `/api/grants`, `/api/hr`, `/api/me`, `/api/travel`, or `/api/attendance`
- [ ] New grant-related page or tab in `frontend/src/app/grants`, portal under `frontend/src/app/me`, or HR under `frontend/src/app/hr`
- [ ] RBAC or module keys extended (`config/modules.php` + platform toggles)
- [ ] Seeder changes for demo tenants (`SenedOrganizationSeeder`)

---

## 9. Suggested next tickets (ordered)

1. **Roles**: introduce `finance_officer`, `programme_manager`, enforce on grant mutate routes.  
2. **Portal widgets**: deeper links into travel reports, attendance checkout, and “my tasks” unification.  
3. **OpenAPI** document generation from Laravel routes for frontend codegen.  
4. **Budget version v0.5**: `budget_versions` with single “approved” pointer on `grants`.

---

*Document version: 1.4 — Employee portal (`/me`, `/api/me/overview`), travel (`travel`), attendance module keys, Forms product removed; HR employment + FTE pro-rata; aligned to repo as of April 2026.*
