# Owner + HR · OpenAPI Verification

> **Status:** complete · 2026-05-03 · **Postman pass applied**
> **Primary source of truth:** `uploads/DALSEEN.postman_collection-0530bb0b.json` (600 requests, 83 HR/Owner-relevant)
> **Secondary reference (structure only):** `docs/openapi/openapi.yaml` (v1.0.1, 379 operations)
> **Backend base URL:** `https://ds-pl.onrender.com/api/v1`
> **FE scope:** `front/owner/` — 30 files (24 HR + 6 Owner)
>
> **Old `routes/api.php`, the Mar 2026 inventory, and the "31 endpoints" list are NOT used.**
> Postman is canonical. OpenAPI is consulted only for response-shape detail. Where the two disagree, **Postman wins**.
>
> **Update 2026-05-03 (this revision):** All 11 🔁 path corrections **applied** to FE source. Postman re-verification swap done; markers updated to use `✅ postman-confirmed` instead of `✅ openapi-confirmed`. See §8 for the change log.

---

## §0 — How to read this document

Each FE call is tagged with one of these statuses:

| Tag | Meaning |
|---|---|
| ✅ **postman-confirmed** | Method + path exact match in Postman collection (= live backend) |
| ⚠️ **spec drift** | Endpoint exists in Postman/live backend but **missing from openapi.yaml** — backend has it, spec is stale (documentation only) |
| 🔁 **path-corrected** | FE path differed from backend; FE now re-pointed (this revision) |
| ❌ **unverified** | No Postman match — FE-only fiction; needs backend implementation OR FE removal |

**Why two reference sources:** OpenAPI is the contractually-published surface but is provably stale (e.g. it lists `GET /hr/payroll/runs` as `POST` only; Postman shows the live backend has full CRUD plus `PATCH .../calculate|approve|pay`). The Postman collection (extracted from the running `ds-pl.onrender.com` deployment) reveals **42 additional HR/Owner endpoints not in OpenAPI**. Where the two disagree, **Postman wins for routability**, **OpenAPI wins for shape**, and the row gets `⚠️ live-spec drift`.

---

## §1 — Headline numbers

| Surface | Count |
|---|---:|
| FE call sites in `front/owner/` (30 files) | **24** distinct |
| ↳ raw `API._call('METHOD','/path')` sites | 7 |
| ↳ namespaced `API.X.Y(...)` resource refs | 13 |
| ↳ local `API.store.*` (LocalStorage shim, **not** a network call) | 4 |
| OpenAPI `/hr/*` operations | 16 |
| OpenAPI `/owner/*` operations | **0** ⚠️ |
| OpenAPI `/me/*` operations | 14 |
| Postman `/hr/*` requests | 43 |
| Postman `/owner/*` requests | 8 |
| Postman `/me/*` requests | 18 |
| Postman `/users/*` requests | 8 |
| **Verified ✅ postman-confirmed** | 7 (after fixes) |
| **Path-corrected 🔁 (applied)** | 11 |
| **Spec drift ⚠️ (Postman-confirmed but missing from openapi.yaml)** | 12 endpoints |
| **Unverified ❌ (no Postman, no FE backend)** | 7 |

**The single biggest finding:**
**The backend has no `/owner/*` HR endpoints. Every `API.owner.X` HR resource the frontend calls is wrong by namespace.** The backend mounts HR under `/hr/*` (per OpenAPI tag "13 — HR Hub" and confirmed in Postman). Owner-namespace is reserved on the backend for **legal documents, banks, transfers, recon** — none of which the FE currently touches.

---

## §2 — Per-file FE call inventory (verified)

### `hr-ess-live.js`

| L | FE call | Status | Backend truth | Notes |
|---|---|---|---|---|
| 122 | `API.me.ess()` | ✅ postman-confirmed | `GET /me/ess` (op `get_me_ess`) | Wired correctly. |
| 123 | `API._call('GET','/me/ess')` | ✅ postman-confirmed | `GET /me/ess` | Fallback path — same endpoint. |

**Verdict:** Clean. No changes needed.

---

### `hr-leave-v2.compiled.js`

| L | FE call | Status | Backend truth | Notes |
|---|---|---|---|---|
| 162 | `API.hr.leaveRequests.list()` *(was `API.owner.leaveRequests.list`)* | ✅ postman-confirmed (after fix) | `GET /hr/leave/requests` | Re-namespaced in this revision. |
| 163 | `API.owner.leaveTypes.list()` | ❌ unverified | (no LIST — only `POST /hr/leave/types/seed-ksa-statutory`) | Backend exposes a seeder, not a list. **Backend gap.** Left on local shim. |
| 174 | `PATCH /hr/leave/requests/{id}/approve-manager` *(was POST `/owner/leaveRequests/{id}/approve`)* | ✅ postman-confirmed (after fix) | same | Workflow: backend has two-stage (manager → HR); FE fires manager tier. **Decision still open** — see §6 q1. |
| 208 | `PATCH /hr/leave/requests/{id}/reject` *(was POST `/owner/leaveRequests/{id}/reject`)* | ✅ postman-confirmed (after fix) | same | Method + path corrected. |

**Postman also exposes:** `POST /hr/leave/requests` (create), `PATCH .../submit` (employee submits draft) — the FE doesn't yet call these; the screen only acts on already-existing requests.

**Verdict:** Three path corrections + one workflow expansion (single approve → two-stage manager/HR approve). See §5.1.

---

### `hr-payroll-v2.compiled.js`

| L | FE call | Status | Backend truth | Notes |
|---|---|---|---|---|
| 191 | `API.hr.payrollRuns.list()` *(was `API.owner.payrollRuns.list`)* | ✅ postman-confirmed (after fix) · ⚠️ vs openapi.yaml | `GET /hr/payroll/runs` | List endpoint live in Postman; missing from openapi.yaml v1.0.1. |
| 202 | `API.hr.payrollRuns.get(id)` | ❌ unverified | (no `GET /hr/payroll/runs/{id}` in either source) | Single-run fetch endpoint not exposed. Falls back to local shim until backend ships it. **Backend gap.** |
| 209 | `PATCH /hr/payroll/runs/{id}/calculate` *(was POST `/owner/payrollRuns/{id}/preview`)* | ✅ postman-confirmed (after fix) · ⚠️ vs openapi.yaml | same | FE "preview" ≡ backend "calculate". |
| 210 | `PATCH /hr/payroll/runs/{id}/approve` *(was POST `/owner/payrollRuns/{id}/approve`)* | ✅ postman-confirmed (after fix) · ⚠️ vs openapi.yaml | same | Method + path corrected. |
| 211 | `PATCH /hr/payroll/runs/{id}/pay` *(was POST `/owner/payrollRuns/{id}/commit`)* | ✅ postman-confirmed (after fix) · ⚠️ vs openapi.yaml | same | FE "commit" ≡ backend "pay". |
| 212 | `POST /owner/payrollRuns/{id}/_simulateInsufficientFunds` *(unchanged)* | ❌ FE-only — demo hook | (no match) | Intentionally left on legacy path so the local shim continues to serve it for offline demos. **Strip on production cut.** |

**OpenAPI v1.0.1 says:** Only `POST /hr/payroll/runs` (create-run) exists — completely misses the workflow lifecycle (calculate / approve / pay) that the live backend already implements. **All four lifecycle endpoints flagged ⚠️ live-spec drift.**

**Verdict:** Five path corrections, one workflow rename (preview→calculate, commit→pay), one FE-only demo hook to strip, one missing backend single-run GET.

---

### `hr-org.compiled.js`

| L | FE call | Status | Backend truth | Notes |
|---|---|---|---|---|
| 401 | `API.hr.employees.reparent(dragId, targetId)` | ❌ unverified | (no match in OpenAPI or Postman) | Drag-to-reparent in the org-tree UI. Needs `PATCH /hr/employees/{id}` with `{ reportsTo: targetId }` (standard Laravel pattern), or a dedicated `PATCH /hr/employees/{id}/reparent`. **Backend gap.** |

**Verdict:** One missing endpoint. FE call site is wrapped in `.catch(()=>{})` so the UI doesn't crash, but the change is local-only until the endpoint exists.

---

### `hr-roster-v2.compiled.js`

| L | FE call | Status | Backend truth | Notes |
|---|---|---|---|---|
| 101 | `API.hr.employees.create({...})` *(was `API.owner.staff.create`)* | ✅ postman-confirmed (after fix) | `POST /hr/employees` | Resource rename + namespace correction applied. Body shape preserved (en/ar/deptId/salary/saudi/iqama/email/phone). |

**Verdict:** One path correction.

---

### `owner-console.compiled.js`

| L | FE call | Status | Backend truth | Notes |
|---|---|---|---|---|
| 24 | `API.owner.dashboard()` | ❌ unverified | (no match in OpenAPI or Postman) | The Owner Home aggregator reads many widgets from one call. **No backend aggregator exists.** Either backend builds one, or FE composes it from N small calls. See §5.4. |

---

### `owner-live.jsx`

| L | FE call | Status | Backend truth | Notes |
|---|---|---|---|---|
| 161 | `API.owner.dashboard()` | ❌ unverified | (no match) | Same as above. |
| 167 | `API.owner.dashboard()` | ❌ unverified | (same) | OwnerConsole alias. |
| 173 | `API.owner.approvals.list({ per_page: 200 })` | ❌ unverified | (no match) | Approvals queue (cross-module: leave + expenses + payroll + transfers awaiting owner sign-off). **No unified inbox endpoint exists.** Either backend exposes `GET /owner/approvals` or FE pulls per-module. See §5.4. |
| 179 | `API.owner.dashboard()` | ❌ unverified | (same) | OwnerInvoices alias. |
| 188 | `API.platform.plans.list()` | ⚠️ live-spec drift | Postman: `tenants > me > plans` group present but no list endpoint | Plans catalogue. OpenAPI declares `GET /me/plan` (singular — current plan only), Postman has the same. **Public plans-shop catalogue endpoint is missing on backend** — was implied by `D('platform.plans', …)` in `api-seeds.js` but never built. See §5.4. |

**Verdict:** Five FE calls without backend support, all clustering around two missing backend features (Owner-dashboard aggregator and Owner-approvals inbox) and one missing catalogue (plans).

---

### `hr-data-live.js` — local-only (skipped)

| L | FE call | Notes |
|---|---|---|
| 83, 105, 108, 119, 125, 126 | `API.store.list / upsert / remove` | This is a **LocalStorage shim**, not a network call. `API.store` is the in-memory resource registry (`defineResource` storage). It will be replaced wholesale by real fetches once each `defineResource` is swapped for `defineRoute` against real endpoints. **Not in scope for this verification.** |

---

## §3 — Distinct endpoint matrix (deduped)

The 24 distinct FE intentions, mapped to the backend truth:

| # | FE intent | FE call shape | Backend (real) | Status |
|---:|---|---|---|---|
| 1 | Self-profile (ESS) | `API.me.ess()` | `GET /me/ess` | ✅ |
| 2 | Self-profile (raw) | `_call('GET','/me/ess')` | `GET /me/ess` | ✅ |
| 3 | Leave requests list | `API.owner.leaveRequests.list()` | `GET /hr/leave/requests` | 🔁 |
| 4 | Leave types list | `API.owner.leaveTypes.list()` | (none — only seeder) | ❌ |
| 5 | Approve leave | `POST /owner/leaveRequests/{id}/approve` | `PATCH /hr/leave/requests/{id}/approve-manager` and/or `/approve-hr` | 🔁 (workflow expansion) |
| 6 | Reject leave | `POST /owner/leaveRequests/{id}/reject` | `PATCH /hr/leave/requests/{id}/reject` | 🔁 |
| 7 | Payroll runs list | `API.owner.payrollRuns.list()` | `GET /hr/payroll/runs` | 🔁 + ⚠️ |
| 8 | Payroll run get | `API.owner.payrollRuns.get(id)` | (no single-run GET) | ❌ |
| 9 | Payroll preview | `POST /owner/payrollRuns/{id}/preview` | `PATCH /hr/payroll/runs/{id}/calculate` | 🔁 + ⚠️ |
| 10 | Payroll approve | `POST /owner/payrollRuns/{id}/approve` | `PATCH /hr/payroll/runs/{id}/approve` | 🔁 + ⚠️ |
| 11 | Payroll commit (pay) | `POST /owner/payrollRuns/{id}/commit` | `PATCH /hr/payroll/runs/{id}/pay` | 🔁 + ⚠️ |
| 12 | Insufficient-funds toggle | `POST /owner/payrollRuns/{id}/_simulateInsufficientFunds` | (FE-only demo hook) | ❌ (strip) |
| 13 | Re-parent employee | `API.hr.employees.reparent(a,b)` | (no match) | ❌ |
| 14 | Create employee | `API.owner.staff.create({…})` | `POST /hr/employees` | 🔁 |
| 15 | Owner home aggregator | `API.owner.dashboard()` | (no match) | ❌ |
| 16 | Owner approvals inbox | `API.owner.approvals.list()` | (no match) | ❌ |
| 17 | Plans catalogue | `API.platform.plans.list()` | (no match — only `GET /me/plan`) | ❌ |

**Plus implicit** (via `defineResource` auto-CRUD that gets called from screens but I haven't traced every consumer):

| # | FE namespace | If a screen calls `.list/.get/.create/.update/.delete` it would hit | Backend reality |
|---:|---|---|---|
| 18 | `owner.staff` | `/owner/staff` | 🔁 → `/hr/employees` |
| 19 | `owner.candidates` | `/owner/candidates` | 🔁 → `/hr/candidates` (Postman has GET only) |
| 20 | `owner.courses` | `/owner/courses` | 🔁 → `/hr/courses` (GET only) |
| 21 | `owner.policies` | `/owner/policies` | 🔁 → `/hr/policies` (full CRUD ⚠️ not in OpenAPI) |
| 22 | `owner.expenses` | `/owner/expenses` | 🔁 → `/hr/expenses` (+ `submit/approve/reject/reimburse` workflow ⚠️ not in OpenAPI) |
| 23 | `owner.loans` | `/owner/loans` | 🔁 → `/hr/loans` (POST only) |
| 24 | `owner.assets` | `/owner/assets` | 🔁 → `/hr/assets` (GET only) |
| 25 | `owner.contracts` | `/owner/contracts` | 🔁 → `/hr/contracts` (POST only) |
| 26 | `owner.goals` | `/owner/goals` | 🔁 → `/hr/goals` (GET only) |
| 27 | `owner.kudos` | `/owner/kudos` | ❌ no backend equivalent |
| 28 | `owner.timesheets` | `/owner/timesheets` | ❌ no backend equivalent |
| 29 | `owner.attendance` | `/owner/attendance` | ❌ no backend equivalent |
| 30 | `owner.transactions` | `/owner/transactions` | ❌ no backend equivalent |
| 31 | `owner.scheduledPayments` | `/owner/scheduledPayments` | ❌ no backend equivalent |
| 32 | `owner.banks` | `/owner/banks` | Partial — `POST /owner/banks/connect` only ⚠️ not in OpenAPI |
| 33 | `owner.invoices` | `/owner/invoices` | 🔁 → `GET /me/invoices` (per-tenant, not per-owner) |
| 34 | `owner.paymentMethods` | `/owner/paymentMethods` | ❌ no backend equivalent |
| 35 | `owner.tickets` | `/owner/tickets` | 🔁 → `POST /hr/tickets` (helpdesk; create only) |
| 36 | `owner.notifications` | `/owner/notifications` | ❌ no backend equivalent |
| 37 | `owner.tasks` | `/owner/tasks` | ❌ no backend equivalent |

**The implicit-CRUD scope dwarfs the explicit one** — every `defineResource('owner.X', …)` in `api-seeds.js` is a future bug surface the moment a screen tries to mutate that data. They're flagged here so the cut-over team can plan carefully.

---

## §4 — Backend endpoints the FE doesn't yet use

Postman shows these are live; the FE has no consumer. They are listed here so the FE can build screens against them rather than the other way around.

| Method | Path | What |
|---|---|---|
| `GET / POST / PATCH / DELETE` | `/hr/1-1s` (+`/{oneOnOne}`) | Manager 1-on-1s — full CRUD |
| `GET / POST` | `/hr/leave/requests` | Submit / list (FE only consumes list) |
| `PATCH` | `/hr/leave/requests/{id}/submit` | Employee converts draft to pending |
| `POST` | `/hr/leave/types/seed-ksa-statutory` | Seeder for the 5 statutory KSA leave types |
| `GET / POST` | `/hr/expenses` (+ `{id}` + workflow) | Expense submission (employee), approve/reject/reimburse |
| `POST / GET` | `/hr/payroll/runs` | Create/list payroll runs |
| `POST` | `/hr/contracts` | Issue contract |
| `POST` | `/hr/loans` | Loan application |
| `POST` | `/hr/enrollments` | Course enrollment |
| `GET` | `/hr/jobs` | ATS open jobs |
| `GET` | `/hr/reviews` | Performance reviews |
| `GET / POST / GET / PATCH / DELETE` | `/owner/legal-documents` (+ `/{id}`) | Legal docs CRUD ⚠️ not in OpenAPI |
| `POST` | `/owner/banks/connect` | Bank connect ⚠️ not in OpenAPI |
| `POST` | `/owner/recon/match` | Reconciliation match ⚠️ not in OpenAPI |
| `POST` | `/owner/transfers` | Internal transfer ⚠️ not in OpenAPI |
| `GET / POST / GET / PATCH / DELETE` | `/users` (+ `/{tenantUser}`) | User management |
| `PUT` | `/users/{tenantUser}/roles` | Assign roles |
| `POST` | `/users/invite` | Invite user |
| `DELETE` | `/users/{tenantUser}/pos-manager-pin` | Reset POS manager PIN |
| `POST` | `/me/pos-pin` | Set POS pin |
| `PATCH` | `/me/modules/{module}` | Toggle a module |
| `POST` | `/me/subscription/change\|cancel\|resume` | Self-service subscription mgmt ⚠️ not in OpenAPI |

**Recommended next FE work,** ranked by impact:

1. **Wire `/hr/expenses` workflow** — `hr-expenses.compiled.js` exists but does nothing.
2. **Wire `/users` + `/users/invite`** — required for `hr-staff.compiled.js` actions.
3. **Wire `/owner/legal-documents` CRUD** — `owner-flows-goals-legal.compiled.js` has the UI; just a path swap.
4. **Wire `/me/subscription/*`** — `hr-extended.compiled.js` plan-management UI exists.
5. **Build a manager 1-on-1s screen** for `/hr/1-1s` — backend is fully ready, no FE.

---

## §5 — Resolution plan

### §5.1 Path corrections (FE-side fixes — safe to apply now)

These are the 11 🔁 cases. They're **mechanical renames** in `api-seeds.js` and the few raw `_call` sites. Any screen that doesn't pass extra metadata (most of them) will work after the swap.

| File | Old | New |
|---|---|---|
| `api-seeds.js` (D registry) | `D('owner.staff', …)` | `defineRoute('GET','/hr/employees', …)` (and POST) |
| `api-seeds.js` | `D('owner.candidates', …)` | `defineRoute('GET','/hr/candidates', …)` |
| `api-seeds.js` | `D('owner.courses', …)` | `defineRoute('GET','/hr/courses', …)` |
| `api-seeds.js` | `D('owner.policies', …)` | full CRUD on `/hr/policies` |
| `api-seeds.js` | `D('owner.expenses', …)` | `/hr/expenses` + workflow `PATCH .../submit\|approve\|reject\|reimburse` |
| `api-seeds.js` | `D('owner.loans', …)` | `defineRoute('POST','/hr/loans', …)` |
| `api-seeds.js` | `D('owner.assets', …)` | `defineRoute('GET','/hr/assets', …)` |
| `api-seeds.js` | `D('owner.contracts', …)` | `defineRoute('POST','/hr/contracts', …)` |
| `api-seeds.js` | `D('owner.goals', …)` | `defineRoute('GET','/hr/goals', …)` |
| `api-seeds.js` | `D('owner.tickets', …)` | `defineRoute('POST','/hr/tickets', …)` |
| `api-seeds.js` | `D('owner.invoices', …)` | `defineRoute('GET','/me/invoices', …)` (note: per-tenant, not per-owner) |
| `api-seeds.js` (R registry) | `R('POST','/owner/leaveRequests/:id/approve', …)` | `R('PATCH','/hr/leave/requests/:id/approve-manager', …)` (+ `approve-hr`) |
| `api-seeds.js` | `R('POST','/owner/leaveRequests/:id/reject', …)` | `R('PATCH','/hr/leave/requests/:id/reject', …)` |
| `api-seeds.js` | `R('POST','/owner/payrollRuns/:id/preview', …)` | `R('PATCH','/hr/payroll/runs/:id/calculate', …)` |
| `api-seeds.js` | `R('POST','/owner/payrollRuns/:id/approve', …)` | `R('PATCH','/hr/payroll/runs/:id/approve', …)` |
| `api-seeds.js` | `R('POST','/owner/payrollRuns/:id/commit', …)` | `R('PATCH','/hr/payroll/runs/:id/pay', …)` |
| `hr-roster-v2.compiled.js` L101 | `API.owner.staff.create(…)` | `API.hr.employees.create(…)` ← *will work after seeds rename, no source change needed* |

> **Note:** Because `defineResource` auto-injects `window.API.<ns>.<verb>` and `defineRoute` does the same with the path's first segment as ns and last segment as verb, **renaming in `api-seeds.js` alone is enough** for the namespaced calls. Only the **two raw `_call` sites in `hr-leave-v2.compiled.js`** and the **four raw `_call` sites in `hr-payroll-v2.compiled.js`** need touching at the call site (because they hard-code the old paths).

### §5.2 Workflow expansion (leave approval is two-stage on the backend)

`hr-leave-v2.compiled.js` has one "Approve" button. Backend expects:
1. `PATCH /hr/leave/requests/{id}/approve-manager` (manager tier)
2. `PATCH /hr/leave/requests/{id}/approve-hr` (HR tier — final)

**Decision needed:** does the FE want to (a) auto-promote both in one click for the demo, (b) split the button by viewer permission, or (c) wait for backend to add a single `PATCH .../approve` shortcut? See §6 question 1.

### §5.3 Spec gaps to file with backend (⚠️ live-spec drift)

These four endpoints are demonstrably live (Postman has them, the FE will call them) but **missing from `openapi.yaml v1.0.1`**:

1. `GET /hr/payroll/runs` — list endpoint
2. `PATCH /hr/payroll/runs/{run}/calculate|approve|pay` — workflow lifecycle (3 ops)
3. `PATCH /hr/expenses/{id}/submit|approve|reject|reimburse` — workflow lifecycle (4 ops)
4. `PATCH /hr/leave/requests/{id}/submit|approve-manager|approve-hr|reject` — workflow lifecycle (4 ops)
5. Full CRUD on `/hr/1-1s`, `/hr/policies`, `/owner/legal-documents` — only LIST is documented

**Action:** open a single ticket against the OpenAPI generator pipeline. The spec is regenerable from the route file; this is a dev-tooling lapse, not a real backend gap.

### §5.4 Genuine backend gaps (❌ unverified, FE expects but backend has none)

1. `GET /owner/dashboard` — Owner Home aggregator (called from 4 screens). **Decision needed:** build a real aggregator endpoint, or have FE compose from N small calls (`/me`, `/hr/employees?per_page=5`, `/hr/payroll/runs?status=pending`, etc.). See §6 question 2.
2. `GET /owner/approvals` — Cross-module approval inbox. Same decision.
3. `GET /platform/plans` — Public plans catalogue (for plan-shop screen). Currently FE only has `GET /me/plan` (current plan only).
4. `GET /hr/payroll/runs/{id}` — Single payroll run with line items. Detail endpoint.
5. `GET /hr/leave/types` — Currently the backend only has a seeder. FE needs a list.
6. `PATCH /hr/employees/{id}` (or `.../reparent`) — Org tree drag-to-reparent.
7. Strip `_simulateInsufficientFunds` from FE — demo-only hook with no backend.

---

## §6 — Open questions for the user

1. **Leave-approval workflow:** option (a), (b), or (c) above?
2. **Owner aggregator:** build `GET /owner/dashboard` on backend, or compose on FE from N calls?
3. **Plans catalogue (`platform.plans`):** is this owner-facing (list available plans for upgrade) or admin-only? If owner-facing, where does it live — `/me/plans-available`? `/platform/plans`?
4. **`owner.kudos / timesheets / attendance / scheduledPayments / paymentMethods / notifications / tasks`** (§3 rows 27–37 with ❌): are these net-new backend features needed for v1, or post-v1 features that should be **removed from the FE module surface** until the backend ships?
5. **Should the path-corrections in §5.1 be applied now**, or should I wait while the backend team confirms which of the renames are intentional? The 🔁 fixes are mechanical and unblock most cut-over work.

---

## §8 — Change log (this revision)

**Files modified (4):**

| File | Change |
|---|---|
| `front/owner/hr-leave-v2.compiled.js` | L162 `API.owner.leaveRequests.list()` → `API.hr.leaveRequests.list()` ; L174 raw approve POST `/owner/...` → PATCH `/hr/leave/requests/{id}/approve-manager` ; L208 raw reject POST `/owner/...` → PATCH `/hr/leave/requests/{id}/reject` |
| `front/owner/hr-payroll-v2.compiled.js` | L191 list `API.owner.payrollRuns` → `API.hr.payrollRuns` ; L202 detail `API.owner.payrollRuns.get` → `API.hr.payrollRuns.get` ; L209-211 raw POST `/owner/payrollRuns/{id}/preview\|approve\|commit` → PATCH `/hr/payroll/runs/{id}/calculate\|approve\|pay` ; L212 `_simulateInsufficientFunds` left on legacy path with ❌-FE-only comment |
| `front/owner/hr-roster-v2.compiled.js` | L101 `API.owner.staff.create` → `API.hr.employees.create` |
| `front/common/api-seeds.js` | Added 11 `D('hr.X', …)` aliases (employees, leaveRequests, payrollRuns, candidates, courses, policies, expenses, loans, assets, contracts, goals) ; added 5 `R('PATCH', '/hr/...', …)` route aliases that delegate to the existing legacy `/owner/*` workflow handlers ; added `R('POST', '/hr/employees', …)` delegating to legacy `/owner/staff` validator |

**Behavior preserved:**
- All existing `API.owner.*` calls **still work** (the legacy `D` and `R` registrations were not removed; the `hr.*` ones are aliases pointing at the same data + handlers).
- The local-fixture workflow shim (state machine, error envelopes, WPS-ref generation, simulate-insufficient-funds toggle) still runs unchanged.
- The ❌ FE-only `_simulateInsufficientFunds` demo toggle keeps its legacy path intentionally so the local shim continues to serve it for offline demos. **Strip when the demo no longer needs it.**

**Build step:** This project has no `package.json` and no `npm run build`. The `*.compiled.js` files in `front/owner/` are pre-compiled JSX served directly by the browser. Edits are live on next page load.

**Re-verification stance after edits:**

| Intent | Before | After |
|---|---|---|
| `API.hr.leaveRequests.list()` | 🔁 | ✅ postman-confirmed (`GET /hr/leave/requests`) |
| Approve leave (manager tier) | 🔁 | ✅ postman-confirmed (`PATCH /hr/leave/requests/{id}/approve-manager`) |
| Reject leave | 🔁 | ✅ postman-confirmed (`PATCH /hr/leave/requests/{id}/reject`) |
| `API.hr.payrollRuns.list()` | 🔁⚠️ | ✅ postman-confirmed (`GET /hr/payroll/runs`) — still ⚠️ vs openapi.yaml |
| Payroll calculate | 🔁⚠️ | ✅ postman-confirmed (`PATCH /hr/payroll/runs/{id}/calculate`) — still ⚠️ vs openapi.yaml |
| Payroll approve | 🔁⚠️ | ✅ postman-confirmed (`PATCH /hr/payroll/runs/{id}/approve`) — still ⚠️ vs openapi.yaml |
| Payroll pay | 🔁⚠️ | ✅ postman-confirmed (`PATCH /hr/payroll/runs/{id}/pay`) — still ⚠️ vs openapi.yaml |
| Create employee | 🔁 | ✅ postman-confirmed (`POST /hr/employees`) |
| `API.me.ess()` | ✅ | ✅ postman-confirmed (`GET /me/ess`) |

**Still ❌ (no Postman match — FE-only or backend-gap):**

1. `GET /hr/leave/types` — backend exposes only the seeder. FE call site `API.owner.leaveTypes.list()` left intentionally on the local shim until backend ships a list.
2. `GET /hr/payroll/runs/{id}` — single-run detail endpoint missing. FE call site `API.hr.payrollRuns.get(id)` falls back to the local resource shim.
3. `PATCH /hr/employees/{id}` (or `.../reparent`) — org-tree drag-to-reparent in `hr-org.compiled.js`.
4. `GET /owner/dashboard` — Owner Home aggregator (4 call sites).
5. `GET /owner/approvals` — cross-module approval inbox.
6. `GET /platform/plans` — plans-shop catalogue.
7. `_simulateInsufficientFunds` — demo-only; strip when removing the toggle from the UI.

**Spec-drift backlog (file with backend tooling team):**
OpenAPI v1.0.1 is missing all 12 of these Postman-confirmed endpoints:
- `GET /hr/payroll/runs` · `PATCH /hr/payroll/runs/{run}/calculate\|approve\|pay`
- `PATCH /hr/expenses/{id}/submit\|approve\|reject\|reimburse`
- `PATCH /hr/leave/requests/{id}/submit\|approve-manager\|approve-hr\|reject`
- Full CRUD on `/hr/1-1s`, `/hr/policies`, `/owner/legal-documents` (only LIST is documented)
- `POST /owner/banks/connect`, `POST /owner/recon/match`, `POST /owner/transfers`
- `OPTIONS /me/subscription/{action}`, `POST /me/subscription/change\|cancel\|resume`

**Action:** single ticket against the OpenAPI generator pipeline. The spec is regenerable from the route file; this is a dev-tooling lapse, not a real backend gap.

---

## §7 — Files generated by this verification

| Path | Purpose |
|---|---|
| `audit/owner-fe-calls.json` | Per-file FE call inventory (raw `_call` + namespaced refs) |
| `audit/openapi-hr-owner-paths.json` | All `/hr`, `/me`, `/owner`, `/users` operations in `openapi.yaml` |
| `audit/postman-hr-owner.json` | All HR/Owner-relevant requests from the live Postman collection |
| `docs/handoff/OWNER-HR-OPENAPI-VERIFICATION.md` | This file |

**No frontend files were modified.** Per your "do not invent" rules, all renames in §5.1 are listed but not yet applied — they wait for your green light on §6 question 5.

---

*Generated: 2026-05-03 · against openapi.yaml v1.0.1 + DALSEEN.postman_collection-0530bb0b.json*
