# ADR-140 — If You May Approve It, You May Create It: Job/Tour Parity Inside RentVeh; TMs See Team Drafts

| | |
|---|---|
| **Status** | Accepted |
| **Date** | 2026-08-14 |
| **Decision maker** | hkl |
| **Builds on** | ADR-133 (`sales.create_job` / `sales.approve_job_tour_request` — the single door), ADR-125 (request→approve flow), ADR-139 (drafts sync) |
| **Applies to** | `/rentveh/`; the principle generalises to any PWA that fronts an approval queue |

## Context

Three things surfaced on 2026-08-14, all from the same field session.

**1. Creating a job/tour from RentVeh was not at parity with Sales.** In the Sales
module you fill a form and the job exists. In RentVeh you could only *raise a request*
and wait — and because submitting an expense requires a tour, that wait is where real
work piles up. Mukesh's expenses sat in draft for exactly this reason.

The sharp edge: the rule applied **even to the people who hold the right to approve**.
A TM who needed a tour had to raise a request to themselves, leave the expense form,
open the 🗂 review queue, find their own request, and approve it. Same authority, four
extra steps, and a queue entry that existed only to be immediately cleared by its
author.

**2. A TM could not see the team's drafts.** `visibleExpenses()` filtered other
people's drafts out of Team mode. ADR-139 had just made drafts reach the server, but
the TM still could not see them — so the row most likely to be *stuck* was the one row
management could not look at. hkl: *"I (Harish) can't see the draft mode expense under
Team tab."*

**3. Approval rights were wrong in the data.** `hub.employee_pwa_access` granted
`tour_approve` to **mukesh** (granted by `system`, 2026-08-01). hkl: *"Harish and
Pramod (are Top management) have approval rights (not mukesh)."* So an installation
user could approve tour requests and see the review queue. Nobody had noticed because
he never used it.

## Decision

### 1. If you hold the right to approve it, the button creates it

The dialog reads your permission and changes what it does:

| You hold | Button | What happens |
|---|---|---|
| the matching right | **Create now** | created immediately, attached to the open expense |
| nothing | **Submit request** | pending request, exactly as before |

The gate is the **identical** `canApproveJob` / `canApproveTour` check the approve
dialog already used — `sales` group for anything involving a new job, `tour_approve`
for a tour against an existing job. **Nobody gains a capability they did not already
have.** The only thing removed is the detour.

The request row is **still written**, then approved in the same flow. That is
deliberate: it is the audit record (`requested_by` and `reviewed_by` both stamped, both
you), and it is the key the RPC's idempotency guard works on. Creation still goes
through `sales.approve_job_tour_request` → `sales.create_job`, so a self-served job is
structurally identical to a Sales-form job (`status='operational_draft'`) and ADR-133's
single-door property is preserved.

If the create call fails, the request row survives and the flow falls back to the
normal approval queue, saying so plainly rather than implying success.

### 2. Team mode shows the team's drafts

`visibleExpenses()` no longer filters other people's drafts in Team mode, and the
employee filter lists people whose only entry is a draft.

Drafts stay **out of the money stats**. `renderStats()` still counts only
submitted/approved and still excludes drafts from the value total — a half-typed draft
is not approvable value, and letting it inflate a KPI would trade one blind spot for a
worse one. Drafts remain read-only to a TM: editing is still `own`-gated, and approve
buttons still appear only for `submitted`.

### 3. Approval rights are top management

`tour_approve` revoked from `mukesh`. Approval = Harish + Pramod.

## Why

**The permission is the decision; the workflow should not re-litigate it.** An approval
queue exists to put a second person in the loop. When the requester *is* the approver
there is no second person, so the queue is pure ceremony — and ceremony in a field app
is where work goes to die. Parity here does not mean "everyone can create"; it means
**the same authority buys the same directness it buys in Sales.**

Keeping the request row rather than calling `create_job` directly costs one insert and
buys a complete audit trail. A self-served tour is not invisible: it is a request whose
requester and reviewer happen to match, which is exactly what happened and exactly what
someone auditing it should see.

**On drafts and TMs:** ADR-139 argued a draft is "private by convention". One day of
field use disproved the useful half of that. A draft is not a secret — it is unfinished
work, and unfinished work is precisely what a manager needs visibility of to unblock.
The privacy that matters (not editing someone else's row) is preserved; the privacy that
merely obstructed is gone.

## Consequences

- A TM sees every team member's half-finished entry, including amounts that will change.
  Accepted deliberately: they are badged `draft` and excluded from totals.
- Mukesh loses the review queue he should never have had. He is *more* blocked on
  job/tour creation than before, not less — he must now request and wait for Harish or
  Pramod. **That is the intended authorisation model**, and the reason his drafts
  accumulated is now a management queue rather than a UI dead end.
- Self-serve is two HTTP calls (insert request, then approve). Only the second is
  transactional. A failure between them leaves an orphaned *pending* request — visible,
  approvable, recoverable, not data loss.
- `job_tour_requests` will accumulate rows whose requester and reviewer are the same
  person. That is a feature (audit), but it makes "how many approvals did management
  actually review?" a question you must now ask with `requested_by <> reviewed_by`.

## Does NOT govern

- **Who holds which access group.** Grants are hkl's, made in
  `hub.employee_pwa_access`, not in code. This ADR only makes the UI honour them.
- **Rakesh.** He holds `sales` *and* `tour_approve` (both from `system`). Under the
  "top management only" rule those look wrong, but revoking `sales` also changes what he
  can do in the Sales module, so it is left for an explicit decision. **Open.**
- **The Sales module and Hub.** Their own creation flows are untouched.
- **Editing or approving someone else's expense.** Unchanged — still `own`-gated.
- **Prod.** `/rentveh/` remains dev-only.

## Revisit If

- **Someone with rights creates a job they should not have** — the check is a
  permission, not a review. If self-served jobs turn out to need oversight, the answer
  is a report over `requested_by = reviewed_by`, not restoring the detour for everyone.
- **A second approver becomes genuinely required** for some class of request (say, above
  a value threshold) — then eligibility stops being a single boolean and this needs a
  policy, not a permission.
- **Team draft visibility leaks something sensitive** — the current model is a UI filter
  over data every client can already read. If drafts ever carry something that must not
  be seen, the fix is row-level security, not a client-side filter.
- **`job_tour_requests` self-approvals drown the audit trail** — consider a distinct
  `created_directly` marker instead of inferring it from matching names.
- **Rakesh's grants are settled** — update the table in §Context and this section.

## Verification

`tests/rentveh-parity.spec.js` — 5/5 passing:

| Test | Guards |
|---|---|
| TM sees a team member's draft under Team | the reported gap; asserts against live DB rows, not a fixture |
| owner still sees their own draft in My mode | the change did not break the owner's view |
| non-approver is offered a REQUEST | Mukesh gets "Submit request" — the revoke is honoured in the UI |
| approver is offered a direct CREATE | Harish gets "Create now" |
| approver creates job+tour in ONE step | tour attaches to the expense; job is `operational_draft` (same shape as Sales); request row shows requested_by = reviewed_by = harish |

Spec-created `sales.*` rows are tagged `ZZ-SPEC-` and reaped by psql afterwards
(web_anon has no DELETE on `sales.jobs` since ADR-133 C8). Baseline restored to 2 jobs /
2 tours after the run.
