# Permit cost — what the state charged

Written 2026-09-24 from Nash's brief. The number answers one question per permit: **what did the issuing state actually charge for this permit?** It is never the permitting company's service charge, a card/processing fee, or a routing fee.

## Words to use (§24)

| Where | Wording |
|---|---|
| One permit | **State Permit Fee** |
| Trip total, every fee known | **Total Permit Fees** |
| Trip total, one or more unknown | **Known Permit Fees** + the missing-fee warning |
| Missing fee | `—` with the hint "Permit fee was not found on the uploaded permit." Never `$0.00`. |

Never "Trip Cost", "Total Trip Cost" or "Permit Processing Cost" — they imply charges we do not calculate.

## Where things live

| Piece | Path |
|---|---|
| Rules: extraction, confidence, visibility, totals, dedupe | `src/lib/domain/permit-cost.ts` · tests `tests/permit-cost.test.ts` |
| I/O: extraction fields, corrections, history | `src/lib/data/permit-cost.ts` |
| UI: fee line + trip summary | `src/components/app/permit-cost.tsx` · tests `tests/permit-cost-render.test.tsx` |
| Correction endpoint | `src/app/api/trips/[id]/permits/[permitId]/cost/` |
| Migration | `supabase/migrations/0033_permit_cost.sql` · `scripts/verify-migration-0033.mjs` |
| Historical backfill | `scripts/backfill-permit-costs.mjs` |

## Extraction (§2, §3)

On upload, the same pass that reads state, number, dates and dimensions also reads the fee:

1. An explicit fee the extractor names (`permit_fee`, `permit_cost`, `fee_amount`) is trusted → status `found`, confidence high, source `api`.
2. Otherwise the permit's text is searched. A label on the **same line** names the amount; the line scope stops "Credit card fee $3.50" from poisoning "Permit Fee: $92.00" on the next line.
3. Strong labels (permit fee, permit cost, state fee, permit amount, total fee…) beat weak ones (amount paid, total, fee…). A number with no currency marker and no money label is a measurement, not a fee.
4. Bonds, fines, insurance, escort, service, processing, card, routing and weight amounts are excluded outright.
5. One clearly labelled amount → `found` / high. One weakly labelled amount → `found` / medium. Several strong amounts with a single "total" → `found` / medium. Anything else → **`needs_review`, amount left null. The system never guesses.**

## Trip total (§8, §10, §11)

`tripPermitFees` sums only the fees it is confident about:

- Every permit has a fee → **Total Permit Fees**, no warning.
- Any permit missing or needing review → **Known Permit Fees** plus "Permit fee was not detected for N permits. The actual trip permit total may be higher."
- Double counting is prevented two ways: a permit another permit `supersedes_permit_id` points at drops out, and the same state + permit number uploaded twice counts once (newest copy kept). A permit can also be flagged `cost_counts_toward_total = false` by hand for a revision that carried no new charge.

## Visibility (§12, §13, §14)

Broker, carrier dispatcher, carrier driver and HeavyHaul staff see the fee. **Pilot roles never do**, whatever trip, state or permit access they hold.

Enforced in three places, not only the UI:

1. `stripPermitCostFor` removes every cost field in the trip page loaders before the payload leaves the server.
2. The correction endpoint refuses a pilot outright.
3. `canSeePermitCost` gates both UI components, so they render nothing.

## AI (§15)

The trip sync sends the permit rows plus a computed `permit_fees` block carrying the label, the total, the warning and the disclaimer sentence. Each question tells the backend `viewer_role` and `permit_cost_visible`. For a pilot that flag is false, and as a last line of defence the chat route redacts any currency amount from an answer to a pilot and logs it.

## Manual correction (§4)

Broker or dispatcher on the trip, plus internal staff. A driver reads but never edits. The extracted amount stays in `permit_cost_extracted_amount`; the correction lands in `permit_cost_manual_override` and is what shows. Status becomes `corrected` (there was an extracted value) or `manually_entered` (there was not). Every change is written to `permit_cost_history` with who, when and why, and to the trip timeline as `permit_cost_corrected`.

## Historical permits (§20)

`node scripts/backfill-permit-costs.mjs` re-reads each stored extraction with the same rules. Dry run by default, `--apply` to write, `--trip <uuid>` to scope it. Permits already carrying a value or a manual correction are never touched.

## Deferred

A permit-fee metric on the trip overview (§19), a Synchron API payload fee as an extra source (§21), and per-state fee analytics.
