# MuveOne Pricer — Use Cases & User Stories

> End-to-end functional coverage of the MuveOne Pricer application, derived from
> [ARCHITECTURE.md](ARCHITECTURE.md). "Customs clearance" (cc) refers to the
> border clearance fees applied to cross-border moves.

---

## 1. Actors

There are only two human roles. The external API has no human user — it is
consumed by another application.

| Actor | Description |
|-------|-------------|
| **Admin** | Owns the account setup and system configuration: accounts and API keys, routing rules (waypoints, exceptions, avoidance flags), zone geometry, pricing formulas, and oversight of logs and change notifications. |
| **Operator** | Works in the dashboard day to day, constantly adjusting prices: ferry prices, zone prices, customs clearance (cc) fees, per-country pricing data, and post code charges. Uses the Map page to test-quote and verify each adjustment. |
| **Consumer App** *(system)* | The external application (e.g. the booking/quotation website) that requests quotes via `POST /api/quote_request` using the account's `App-Key`. No human interacts with this API directly. |
| **Google Maps API** *(external)* | Provides routing, distances, geocoding, and border-crossing information. |
| **Algolia Places** *(external)* | Address autocomplete on the web UI. |
| **SMTP / Mail service** *(external)* | Delivers change-notification and auth emails. |

> Note: the application has no role/permission system today — Admin and Operator
> both log in as the same account user. The role split below is organisational
> (who does what), and doubles as the requirement set if roles are ever enforced
> in software.

---

## 2. Use case overview

```
                     ┌─────────────────────────────────────────────────┐
                     │                 MuveOne Pricer                  │
  Admin ───────────► UC-01 Manage account, credentials & API key      │
  Admin ───────────► UC-02 Configure routing (waypoints, exceptions,  │
                     │       avoid ferries/highways/tolls)             │
  Admin ───────────► UC-03 Define pricing zones (geometry)            │
  Admin ───────────► UC-04 Author & manage pricing formulas           │
  Admin ───────────► UC-05 Review logs, audit trail & change emails   │
                     │                                                 │
  Operator ────────► UC-06 Adjust ferry prices                        │
  Operator ────────► UC-07 Adjust zone prices                         │
  Operator ────────► UC-08 Adjust customs clearance (cc) fees         │
  Operator ────────► UC-09 Adjust per-country pricing data            │
  Operator ────────► UC-10 Adjust post code charges (CCZ/ULEZ)        │
  Operator ────────► UC-11 Test a quote on the Map page               │
                     │                                                 │
  Consumer App ────► UC-12 Request a quote via the external API       │
                     │                                                 │
  System ──────────► UC-13 Calculate a quote (shared pipeline)        │
  System ──────────► UC-14 Restrict quotes for blocked cities         │
  System ──────────► UC-15 Notify pricing changes by email            │
  System ──────────► UC-16 Auto-detect new ferries on routes          │
                     │         (uses: Google Maps, Algolia, SMTP)      │
                     └─────────────────────────────────────────────────┘
```

---

## 3. Use cases (high level)

### Admin

#### UC-01 — Manage account, credentials & API key
- **Goal:** Set up and maintain the account the whole system hangs off: login credentials, password resets, and the `APP_KEY` the Consumer App authenticates with.
- **Flow:** Register account → an 18-char `APP_KEY` is generated → log in → Settings modal exposes the API key; the key is handed to the Consumer App's team.
- **Notes:** Account type is resolved from the email address (MuveOne-only since 2026-07-17; unknown emails throw). It gates access — the engine itself is the single MuveOne pricing kernel.

#### UC-02 — Configure routing
- **Goal:** Make the calculated route match how jobs are actually driven, since every price derives from the route.
- **Flows:**
  - **Waypoints** (`/waypoint`): force routes between given country pairs through specific stops (e.g. always cross via Dover→Calais).
  - **Exceptions** (`/exception`): remove a waypoint for a specific country pair where the general rule shouldn't apply.
  - **Avoidance flags** (Settings → `POST /updateMap`): avoid ferries / highways / tolls on all route requests.

#### UC-03 — Define pricing zones (geometry)
- **Goal:** Establish the geographic surcharge areas the Operator will price.
- **Flow:** On `/zone`, draw a polygon with the Google Maps Drawing Manager or upload GeoJSON; choose the charging model — **fixed** (flat fee when a route passes through) or **per-km** (price × kilometres driven inside the zone).

#### UC-04 — Author & manage pricing formulas
- **Goal:** Define the pricing logic that turns route, ferry, zone, and cc data into final prices. This is the heart of the engine and the most sensitive thing an Admin touches.
- **Flows:**
  - **Keyword DSL editor** (`POST /formula`): form-based tokens (`start`/`end`/`IF`/`ELSE`/…) transpiled to PHP.
  - **Raw PHP editor** (`POST /php_formula`): PHP entered directly for complex logic (VAT place-of-supply, import/export margins, per-volume tiers).
  - Both are validated against mock data (distance 500 km, mass 50, volumes [5,10,15], sample ferries/zones) before saving.
- **Available inside a formula:** `$mass`, `$volume`, `$distance`, `$distance_by_country`, `$from`/`$to`, country codes, `$trip_duration`, `$ferries`, `$zones`, and the account's pricing JSON (`$pricingData` or `$muveOnePricingData` + `$distanceType`).

#### UC-05 — Review logs, audit trail & change emails
- **Goal:** Oversee what the system quoted and what the Operator changed.
- **Flow:** Every quote writes a `logs` row; raw Google responses are archived to `public/google_responses/{date}.json`. Pricing/formula changes email `CHANGE_NOTIFIER_EMAIL` with user, IP, and timestamp. `/logs` exposes the application log viewer (log-reader API restricted to `LOG_READ_FOR` emails).

### Operator

#### UC-06 — Adjust ferry prices
- **Goal:** Keep ferry crossing prices current as carrier rates change.
- **Flow:** CRUD on `/ferry` (name, coordinates, price). Newly auto-detected ferries (UC-16) appear at £0 and must be priced promptly — until then, quotes crossing them under-charge.

#### UC-07 — Adjust zone prices
- **Goal:** Tune the fee on each zone the Admin has drawn.
- **Flow:** Edit a zone's `price` (and fixed/per-km flag if the model changes) on `/zone`.

#### UC-08 — Adjust customs clearance (cc) fees
- **Goal:** Keep cross-border clearance charges (price and cost) current per origin→destination country pair.
- **Flow:** CRUD on `/customs_price` (origin, destination, price, cost, isDefault).
- **Matching order at quote time:** exact pair → partial (origin-only or destination-only) → the `isDefault` row. The Operator must maintain a sensible default so unconfigured pairs never go out uncharged.

#### UC-09 — Adjust per-country pricing data
- **Goal:** Update the per-country rate JSON that formulas consume (`$pricingData` / `$muveOnePricingData`), the primary lever for day-to-day price changes.
- **Flow:** Edit the JSON table on `/formula/pricing-data` → save via `/formula/pricing-data-update` (gated by `PRICING_DATA_UPDATE`) → a change-notification email goes out (UC-15).

#### UC-10 — Adjust post code charges
- **Goal:** Keep UK congestion/clean-air surcharges (CCZ/ULEZ) and vehicle-type surcharges current per post code.
- **Flow:** CRUD on `/post_code_charges`.

#### UC-11 — Test a quote on the Map page
- **Goal:** Verify a price adjustment took effect before the Consumer App serves it to real customers.
- **Flow:**
  1. Open `/map`, enter From/To (Algolia autocomplete), mass, volume options, and pick a formula.
  2. `POST /send_location` runs the shared calculation pipeline (UC-13).
  3. The UI draws the route polyline and zone overlays and shows the full breakdown: total and per-country distance, ferries with prices, zone charges, cc fee, price per volume, total.
- **Alternate flow:** address not found (ZERO_RESULTS) → normalized and retried once.

### Consumer App (system-to-system)

#### UC-12 — Request a quote via the external API
- **Goal:** The Consumer App prices a customer's move automatically, with no human in the loop.
- **Preconditions:** Valid `App-Key` header; formula ID owned by that account.
- **Main flow:**
  1. `POST /api/quote_request` with `from`, `to`, `mass` (numeric), `volume[]` (5/10/15/30), `formula`.
  2. `apikey` middleware authenticates; payload is validated.
  3. The shared pipeline (UC-13) calculates the quote.
  4. Response JSON: distance + per-country distances, `ferries[]` + `ferriesCost`, `zones[]`, per-volume `prices`, `total_price`, `custom_clearance {price, cost}`, and the Google `routes[]` so the Consumer App can render the journey.
  5. An audit log row is written (UC-05).
- **Alternate flows:** invalid key → rejected; validation failure → error response; restricted city → zeroed 200 (UC-14).

### System

#### UC-13 — Calculate a quote (shared pipeline)
Both entry points (UC-11 web, UC-12 API) converge here:
1. Fetch route from Google Maps Directions, applying the Admin's waypoints, exceptions, and avoidance flags.
2. Compute total distance/duration; match ferry legs to the ferry table by coordinates.
3. Compute per-country km from Google's "Entering {country}" instructions.
4. Point-in-polygon test against zones; compute km inside each matched zone.
5. Resolve the customs clearance fee for cross-border moves (exact → partial → default), with country names normalized via the geocoding cache.
6. Execute the selected formula with every input injected; it returns `price`, `cost`, per-volume `prices`, `total_price`.
7. Merge in `custom_clearance`, respond, and write the audit log.

#### UC-14 — Restrict quotes for blocked cities
- The `cityRestrict` middleware checks requests against `public/json/localization.json`; restricted cities receive a zeroed 200 response rather than an error.

#### UC-15 — Notify pricing changes by email
- On any pricing-data or formula save, an email with user, IP, and timestamp is sent to `CHANGE_NOTIFIER_EMAIL`, so the Admin sees every Operator adjustment.

#### UC-16 — Auto-detect new ferries
- When a route response contains a ferry leg not in the `ferries` table, it is inserted at £0 pending Operator pricing (UC-06).

---

## 4. User stories by epic

Format: *As a \<actor\>, I want \<capability\>, so that \<value\>.* Each story
carries acceptance criteria (AC).

### Epic A — Account & access (Admin)

- **A1.** As an Admin, I want to register the account and receive an API key, so that the Consumer App can be connected to the pricer.
  - AC: Registration creates a user with a unique 18-character `APP_KEY`; the key is visible in the Settings modal.
- **A2.** As an Admin, I want to log in with email and password and reset a forgotten password, so that the pricing configuration is protected.
  - AC: All dashboard routes require an authenticated session; password reset works via email.
- **A3.** As an Admin, I want the account's configuration (waypoints, ferries, zones, cc prices, formulas) scoped to the account, so that each account's rules are independent.
  - AC: All configuration queries are scoped to the account's `user_id`; deleting the account cascades to all its configuration.
- **A4.** As the system, I want the account type resolved from the account email (MuveOne allowlist, throw for unknown), so that only provisioned MuveOne accounts can quote. (Historical: multi-engine selection retired 2026-07-17.)
  - AC: MuveOne accounts price via the parameterized kernel (muveone_pricing.json + DB constants store); unknown accounts fail explicitly.

### Epic B — Routing configuration (Admin)

- **B1.** As an Admin, I want to force routes through specific waypoints per country pair, so that quotes reflect the operational route (e.g. Dover→Calais crossing).
  - AC: Waypoint CRUD on `/waypoint`; quotes between the matching countries route via the waypoint (`optimize:true` on the Directions request).
- **B2.** As an Admin, I want to add exceptions that remove a waypoint for a given country pair, so that special routes aren't forced through inappropriate stops.
  - AC: Exception CRUD on `/exception`; excepted waypoints are omitted from the Directions request for matching routes.
- **B3.** As an Admin, I want to toggle avoidance of ferries, highways, and tolls, so that routes match the fleet's operating rules.
  - AC: Toggles persist on the account's `maps` row and are applied as `avoid=` parameters on every route request.

### Epic C — Zone geometry (Admin)

- **C1.** As an Admin, I want to draw pricing polygons on a map or upload GeoJSON, so that specific geographic areas (city centres, islands, remote regions) can carry a surcharge.
  - AC: Zone CRUD on `/zone`; polygons persist as GeoJSON files; both drawing and upload are supported.
- **C2.** As an Admin, I want to choose per zone between a fixed fee and a per-km fee, so that the surcharge model matches the cost driver.
  - AC: `fixed` zones add a flat fee when the route passes through; per-km zones add `price × km driven inside the zone`.

### Epic D — Pricing formulas (Admin) — **RETIRED**

> This epic is historical. The formula-authoring editor (D1–D5) was removed and
> the `eval()` execution path was retired (`feat/retire-eval-path`): pricing is
> now versioned code in `app/Pricing/` (see ARCHITECTURE.md §4), and `formulas`
> rows are inert routing metadata. The stories below document the old surface.

- **D1.** As an Admin, I want to build pricing formulas with a guided keyword editor, so that pricing logic can be defined without writing code.
  - AC: The DSL (`start`/`end`/`IF`/`ELSE`/`exit`…) transpiles to executable logic; the form state is preserved for later editing.
- **D2.** As an Admin, I want to write formulas directly in PHP, so that complex logic (VAT place-of-supply, import/export margins, per-volume tiers) can be expressed.
  - AC: Raw PHP formulas can be created via `/php_formula` and selected for quotes. *(Risk: executed via `eval()` — see ARCHITECTURE.md §8.1; a sandboxed evaluator is the target state.)*
- **D3.** As an Admin, I want new/edited formulas validated against sample data before saving, so that a broken formula can't take quoting down.
  - AC: Saving runs the formula against mock inputs; failures block the save with an error.
- **D4.** As an Admin, I want every routing/pricing input available inside a formula, so that any pricing scheme can be expressed.
  - AC: `$mass`, `$volume`, `$distance`, `$distance_by_country`, `$from`/`$to`, country codes, `$trip_duration`, `$ferries`, `$zones`, and the account pricing JSON are all injected.
- **D5.** As a MuveOne Admin, I want distance tiers (`low`/`mid`/`high`/`above_high`) available to the formula, so that rates can step by journey length.
  - AC: `$distanceType` is injected for MuveOne accounts based on total distance.

### Epic E — Price adjustments (Operator)

The Operator's core job: these are the levers adjusted constantly.

- **E1.** As an Operator, I want to update ferry crossing prices, so that quotes track carrier rate changes.
  - AC: Ferry CRUD on `/ferry`; the new price applies to the next quote crossing that ferry.
- **E2.** As an Operator, I want auto-detected ferries flagged at £0, so that I can find and price new crossings before they leak revenue.
  - AC: Unknown ferry legs create a £0 row (UC-16); £0 ferries are visible in the ferry list for pricing.
- **E3.** As an Operator, I want to change a zone's price (and fixed/per-km model), so that area surcharges stay current without redrawing geometry.
  - AC: Editing a zone's price on `/zone` takes effect on the next quote through that zone.
- **E4.** As an Operator, I want to set customs clearance fees (price and cost) per origin→destination pair, so that cross-border moves carry the correct clearance charge.
  - AC: CC CRUD on `/customs_price`; a quote for a matching cross-border pair includes that fee in `custom_clearance`.
- **E5.** As an Operator, I want partial-match and default cc fees, so that pairs I haven't explicitly configured still get a sensible charge.
  - AC: Matching order is exact pair → origin-only/destination-only → the `isDefault` row.
- **E6.** As an Operator, I want to edit the per-country pricing data JSON in a table UI, so that routine rate changes don't require a developer or a formula edit.
  - AC: `/formula/pricing-data` shows the JSON as an editable table; saving requires the `PRICING_DATA_UPDATE` gate and triggers the notification email.
- **E7.** As an Operator, I want to maintain UK post code charges (CCZ/ULEZ and vehicle-type surcharges), so that London-area jobs include statutory charges.
  - AC: CRUD on `/post_code_charges`; charges are available to the pricing flow.

### Epic F — Quote verification (Operator)

- **F1.** As an Operator, I want to run a test quote from the Map page after adjusting a price, so that I can confirm the change is live before the Consumer App serves it.
  - AC: `/map` accepts From/To (autocomplete), mass, volume (5/10/15/30), and a formula; the result reflects the adjusted price.
- **F2.** As an Operator, I want the quote to show a full breakdown — total and per-country distance, ferries with prices, zone charges, cc fee, and price per volume — so that I can see exactly which component my adjustment moved.
  - AC: Response includes `distance`, `distance_by_country`, `ferries[]` + `ferriesCost`, `zones[]`, `custom_clearance {price, cost}`, `prices[]` per volume, `total_price`.
- **F3.** As an Operator, I want the route drawn on a map with zone overlays, so that I can visually confirm the route and zone hits behind the price.
  - AC: The polyline reflects the returned Google route including forced waypoints; matched zones are highlighted.
- **F4.** As an Operator, I want misspelled addresses normalized and retried automatically, so that minor input errors don't fail a test quote.
  - AC: On ZERO_RESULTS the address is reformatted and retried once before an error is shown.

### Epic G — External API (Consumer App)

- **G1.** As the Consumer App, I want to request a quote with an API key, so that customer moves are priced automatically with no human in the loop.
  - AC: `POST /api/quote_request` with a valid `App-Key` header returns the full quote JSON; a missing/invalid key is rejected.
- **G2.** As the Consumer App, I want request validation errors clearly reported, so that integration faults are diagnosable.
  - AC: `from`, `to` required; `mass` numeric; `volume` an array of allowed values (5/10/15/30); `formula` must be an ID owned by the authenticated account; violations return validation errors.
- **G3.** As the Consumer App, I want the response to include the route geometry, so that the journey can be rendered in its own UI.
  - AC: The Google `routes[]` payload is included in the response.
- **G4.** As the business, I want quotes for restricted cities suppressed, so that the Consumer App never prices jobs we cannot serve.
  - AC: Requests for cities in the restriction list return a zeroed quote. *(Improvement candidate: return an explicit "restricted" status instead of a silent zero — see ARCHITECTURE.md §8.6 — so the Consumer App can show "not served" rather than £0.)*
- **G5.** As the business, I want every API quote written to the audit log, so that any price the Consumer App showed a customer can be reproduced later.
  - AC: Each quote writes a `logs` row; raw Google responses are archived to `public/google_responses/{date}.json`.

### Epic H — Oversight & notifications (Admin)

- **H1.** As an Admin, I want an email whenever pricing data or a formula changes, so that every Operator adjustment (and any unauthorized change) is visible immediately.
  - AC: The notification includes who changed what, from which IP, and when, sent to `CHANGE_NOTIFIER_EMAIL`.
- **H2.** As an Admin, I want access to application logs restricted, so that operational data isn't exposed.
  - AC: Log-reader API requires an email in `LOG_READ_FOR`. *(Gap: the `/logs` web route currently lacks auth middleware — see ARCHITECTURE.md §8.5.)*
- **H3.** As an Admin, I want the quote audit trail queryable per day, so that disputes and pricing regressions can be investigated.
  - AC: `logs` rows and archived Google responses are retrievable by date.

### Epic I — Operations *(supporting)*

- **I1.** As an Admin, I want all external keys (Google, Algolia, SMTP, DB) in environment configuration, so that secrets are not in source control. *(Currently violated — see ARCHITECTURE.md §8.2.)*
- **I2.** As an Admin, I want the app deployable on shared hosting with a simple git pull, so that releases fit the current hosting arrangement.
- **I3.** As a developer, I want the golden-master corpus (79 offline cases) plus the kernel unit suites, so that pricing regressions are caught before release. (Historical: the per-engine eval formula tests were retired with the engines, 2026-07-17.)
  - AC: The formula test suite runs in CI. *(Currently commented out of `phpunit.xml`.)*

---

## 5. End-to-end scenarios

### Scenario 1 — Operator adjusts a price and verifies it (daily loop)

1. The carrier raises the Dover→Calais ferry rate. The Operator opens `/ferry` and updates the price (E1).
2. The Operator opens `/map` and runs a test quote London → Paris with the production formula (F1).
3. The breakdown shows the new ferry price in `ferries[]` and the higher total (F2); the route on the map confirms the Dover→Calais crossing was used (F3).
4. The change is done — the next Consumer App request (Scenario 2) picks it up automatically. If the change had been to pricing data instead, the Admin would also receive the change-notification email (H1).

### Scenario 2 — Consumer App prices a customer move (no human in the loop)

1. A customer on the booking site requests London → Berlin, 250 kg, volumes 10 and 15 m³.
2. The Consumer App sends `POST /api/quote_request` with its `App-Key` and the formula ID (G1).
3. The key authenticates the account; neither city is restricted (UC-14).
4. The route is fetched from Google Maps honouring the Admin's Dover→Calais waypoint and avoid-tolls flag (UC-02).
5. The Channel ferry leg is matched and priced with the rate the Operator set in Scenario 1 (E1).
6. Per-country distances are computed (GB/FR/BE/DE); the route crosses the "Greater London" per-km zone (C1/E3).
7. GB→DE is cross-border, so the customs clearance fee is resolved: exact GB→DE row if present, else partial, else the Operator's default (E4/E5).
8. The formula runs with all inputs plus `$muveOnePricingData` and the distance tier (D4/D5), returning a price per volume and a total.
9. The JSON response (breakdown + route geometry + `custom_clearance`) goes back to the Consumer App (G3); an audit row is written (G5).

---

## 6. Traceability

| Use case | Actor | Epics / stories | Key code |
|----------|-------|-----------------|----------|
| UC-01 | Admin | A1–A4 | `Auth::routes()`, `User::accountType()` |
| UC-02 | Admin | B1–B3 | `WaypointController`, `ExceptionController`, `MapController::UpdateMapSettings` |
| UC-03 | Admin | C1–C2 | `ZoneController`, `zone.blade.php` |
| UC-04 | Admin | D1–D5 | `FormulaController`, `Formula::codeGen/codeValidate` |
| UC-05 | Admin | H1–H3 | `Log::report`, `/logs`, `LogMiddleware`, `changeNotificationMail` |
| UC-06 | Operator | E1–E2 | `FerryController`, `ferries` |
| UC-07 | Operator | E3 | `ZoneController` |
| UC-08 | Operator | E4–E5 | `CustomsPriceController`, `CustomPriceRepository::getClearancePrice` |
| UC-09 | Operator | E6 | `FormulaController::viewPricingData/savePricingData` |
| UC-10 | Operator | E7 | `/post_code_charges` |
| UC-11 | Operator | F1–F4 | `MapController::getLocationDistance`, `map.blade.php` |
| UC-12 | Consumer App | G1–G5 | `ApiController::getQuote`, `QuoteRepository`, `AuthKey` |
| UC-13 | System | (pipeline behind F1/G1) | `Map::makeApiRequest/getDistance/distanceByCounty/calCulateData/calcFinalPrice`, `Zone::detectZones` |
| UC-14 | System | G4 | `CityRestrict` middleware |
| UC-15 | System | H1 | `changeNotificationMail` |
| UC-16 | System | E2 | `Ferry::detectFerry` |
