# PRD — menu.mypass.zone

Evidence date: 2026-10-09. Source: `git@bitbucket.org:overdrive/menu.mypass.zone.git`, `master` at `ca37151` (2022-07-21).

There is no prior PRD in the repository or in Silicon Ark. Every requirement below was recovered from the code, views, and commit history. Provenance is marked on each item.

The gitignored database configuration is not in the clone. No credentials were available to copy, and none are recorded here.

## Product purpose

A visitor can browse food menus for venues that belong to one SA Rugby tournament. The home page asks them to select a venue. They then select a date, a session (a titled sitting with a start and end time), and read the dishes. A dish link opens nutritional information and allergen icons.

Provenance: **Implemented** (`IndexController`, `SessionController`, views under `application/modules/default/views/scripts/`).

The layout presents the page as SA Rugby / SARU. Provenance: **Documented** in `application/layouts/default.phtml` (title and footer). The relationship between this host and `www.sarugby.online` is **Inferred** from the QR code URL and **Requires Confirmation**.

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Anonymous visitor | Opens `/`, picks a venue, date, and session, and reads dishes and nutrition | `IndexController`, `SessionController`. Neither `init()` checks a session or identity | Implemented |
| Menu editor | Would insert, update, or delete venues, sessions, items, nutrition, or allergens | `insertData`, `updateData`, and `deleteData` on the food models | Implemented on the models. No controller action calls them |
| Authenticated user | Not a role of the pages this repository serves | `Zend_Auth` appears in `Positions.php` and `GirProfile.php` only. No controller instantiates those models | Unused in this request path |

No login, role, or permission check exists on the three controllers.

## Main workflows

### W1 — Choose a venue

Trigger: visitor opens `/` (`IndexController::indexAction`).

1. Load `FoodSessionVenues_view` where `TID` equals the literal `284`. Order by `Venue` ascending.
2. Render `index/index.phtml` inside the default layout.
3. Heading: "VENUES" and "PLEASE SELECT A VENUE TO VIEW THE MENU".
4. Each row shows `SmallImage`, `Venue`, and `Description`, and links to `/session/dates/id/{VID}`.
5. If the result set is empty, the headings render and the list is omitted. There is no empty-state message.

Provenance: **Implemented**.

### W2 — Choose a date

Trigger: GET `/session/dates/id/{VID}`.

1. `datesAction` passes the `id` parameter to `Model_TournamentFoodSession::getAllByVenue`.
2. The query reads `FoodSession_view` where `VID` equals that id, orders by `Date` then `StartTime`, and groups by `Date`.
3. Each date is a tile labelled `j F` (example shape: `9 July`) linking to `/session/menus/id/{SID}`.
4. A back control links to `/`.
5. An empty result renders the "DATES" / "PLEASE SELECT A DATE" headings and no tiles.

The `SID` on a date tile is the session id of one grouped row, not a date key. `menusAction` uses that row only to recover `VID` and `Date`. Which row MySQL keeps for the group depends on the server SQL mode. Provenance: **Implemented** query; the grouping side effect is **Inferred**.

### W3 — Choose a session

Trigger: GET `/session/menus/id/{SID}`.

1. Load the `FoodSession_view` row for that `SID`.
2. Load every `FoodSession_view` row with the same `VID` and `Date`, ordered by date then start time. This step does not group.
3. The heading links the venue name back to `/session/dates/id/{VID}` and shows the date as `j F Y`.
4. Each session shows `SmallImage`, `Title`, the date, `StartTime`, and `EndTime`, and links to `/session/menu/id/{SID}`.
5. The back control in the sub-heading is malformed HTML: `href="/` is not closed before `class=`. Provenance: **Implemented** defect in `session/menus.phtml`.

### W4 — Read a menu

Trigger: GET `/session/menu/id/{SID}`.

1. Load the session row and assign `ID` to the same id.
2. Load `SessionFood_Items` where `SID` equals the session's `SID`, ordered by `Position` ascending.
3. Render each item's `Title` and `Description` inside `.menu_content`.
4. The heading shows "SESSION", a link back to the venue dates, and the date. A QR image is requested from `https://chart.googleapis.com/chart` with the data `https://www.sarugby.online/menu/session/menu/id/{ID}/`.
5. The back control links to `/session/menus/id/{SID}` for this session, which is W3 for that session's date.
6. After load, script rewrites every anchor inside `.menu_content`. The visible text is kept, an info icon is added, and `onclick` calls `Nutritional`. The nutrition id is the second segment of the existing `href` split on `_`.

Provenance: **Implemented**. The description HTML is stored data. This repository does not show the href pattern that authors are expected to save. The split itself is **Implemented** client behaviour. See FR-006.

### W5 — Open nutrition and allergens

Trigger: click a rewritten dish link. The modal "Nutritional Info" opens and loads `/session/nutrition/id/{id}`.

`nutritionAction` disables the layout, loads `Nutritions` by `NID`, JSON-decodes `Allergens`, and loads `Allergens` rows whose `AID` is in that list.

The fragment shows:

- nutrition `Title`
- allergen `Image` and `Title` for each match, under the label "Allergens"
- nutrition `Description`
- `Portion`, `Energy_Cal` with `Energy_Kj` underneath, `Protein`, `Carbs`, `Fat`

Provenance: **Implemented** (`session/nutrition.phtml`).

There is no check for a missing nutrition row, invalid JSON, or an empty allergen list beyond `count($this->ALLERGENS) > 0`. A missing row would dereference `Title` on a non-object. Provenance: **Inferred** from the view. Not executed against a database during this inspection.

### W6 — Error page

Trigger: unknown controller, action, or route, or any other exception handled by the error plugin.

- Missing route, controller, or action: HTTP 404, message "Page not found".
- Other errors: HTTP 500, message "Application error".
- If no error handler payload is present: message "You have reached the error page".
- Logging runs only when the bootstrap has a `Log` resource. `Bootstrap` does not register one.
- Exception detail is shown only when `displayExceptions` is true. That flag lives in `application.ini`, which is not in Git.

Provenance: **Implemented** (`ErrorController`). Whether production shows exception text is **Unknown / Requires Confirmation**.

`SessionController::indexAction` is empty, so `/session` renders the layout with no content and is not a 404.

## Functional requirements

### FR-001 — Public venue list for one tournament

The home page lists venues from `FoodSessionVenues_view` where `TID` is `284`, ordered by venue name, and links each venue to its date list.

Actor: anonymous visitor. Trigger: GET `/`. Input: none. Output: HTML venue cards. Validation: the tournament id is a literal in code, not a request parameter. Error: a database failure is not caught in the action.

Evidence: `IndexController::indexAction`, `Model_MenuVenues::getAllByID`, `views/scripts/index/index.phtml`.

Implementation: Implemented. Confidence: High.

`getAll()` exists and is not called. It would list every venue with no tournament filter.

### FR-002 — Dates for a venue

`/session/dates/id/{VID}` lists distinct dates for that venue. Each date links to `/session/menus/id/{SID}` using the grouped row's session id. The visitor can go back to `/`.

Actor: anonymous visitor. Trigger: the venue link. Input: `id` (venue id). Output: date tiles. Validation: the id is concatenated into SQL. There is no numeric check and no check that the venue belongs to tournament `284`.

Evidence: `SessionController::datesAction`, `getAllByVenue`, `views/scripts/session/dates.phtml`.

Implementation: Implemented. Confidence: High.

A visitor who knows a venue id outside tournament `284` can request its dates. Provenance: **Inferred** from the lack of a tournament predicate on this query.

### FR-003 — Sessions on a date

`/session/menus/id/{SID}` shows every food session for the venue and date of that session id. Each card links to the menu. The venue name links back to the date list.

Actor: anonymous visitor. Input: `id` (a session id). Output: session cards with title, image, date, start, and end.

Evidence: `menusAction`, `getRow`, `getAllByVenueAndDates`, `views/scripts/session/menus.phtml`.

Implementation: Implemented. Confidence: High.

The back icon in the sub-heading does not have a valid `href`. See W3.

### FR-004 — Session dishes

`/session/menu/id/{SID}` lists `SessionFood_Items` for that session in `Position` order. Each row shows title and description. The page includes a QR code for the sarugby.online URL of the same id.

Actor: anonymous visitor. Input: `id`. Output: HTML menu plus an external QR image.

Evidence: `menuAction`, `Model_FoodSessionItems::getAll`, `views/scripts/session/menu.phtml`.

Implementation: Implemented. Confidence: High.

Item `Description` is printed unescaped. If editors store HTML links, the nutrition script depends on that HTML. Provenance: **Implemented**.

### FR-005 — Nutrition fragment

`/session/nutrition/id/{NID}` returns an HTML fragment, without the site layout, for one nutrition row and its allergens.

Actor: the menu page script. Trigger: modal load. Input: `id`. Output: fragment described in W5. The action does not check the HTTP method.

Evidence: `nutritionAction`, `Model_Nutritions::getRow`, `Model_Allergens::getInRange`, `views/scripts/session/nutrition.phtml`.

Implementation: Implemented. Confidence: High.

`Allergens` is JSON. `getInRange` binds the decoded list with `AID IN(?)`. The stored JSON shape is **Inferred** to be a list of allergen ids because that is what `IN(?)` receives. Sample rows were not in the repository.

### FR-006 — Dish links become nutrition popups

On the menu page, every anchor in the menu body is rewritten so a click opens the modal and loads `/session/nutrition/id/` plus the second underscore-separated piece of the original `href`.

Actor: browser script on `session/menu.phtml`. Input: anchors already present in item descriptions. Output: modal content from FR-005.

Implementation: Implemented. Confidence: High for the script. Confidence: Low for the href convention, because the convention is not written down and the descriptions are database content.

If an href has no underscore, the id segment is empty and the request is `/session/nutrition/id/`. Behaviour of `getRow` for an empty id was not executed. Provenance: **Inferred** edge case.

### FR-007 — Shared chrome

Every layout page uses the SA Rugby banner, the Springboks title, the SARU copyright year, and Sportsline stylesheets and scripts from `/wp-content` and `/wp-includes`. The banner links to `/menu`.

Evidence: `application/layouts/default.phtml`.

Implementation: Implemented. Confidence: High.

`/menu` has view partials (`menu/default.phtml`, `menu/template.phtml`) and no `MenuController`. The layout's call to the menu partial is commented out. A request to `/menu` is a missing controller. Provenance: **Implemented**. Expected HTTP status is 404 from FR-008, unless a rewrite outside this repository maps `/menu`. That mapping is **Unknown**.

### FR-008 — Unknown URLs

Unknown controllers and actions return HTTP 404 and "Page not found". Other exceptions return HTTP 500 and "Application error".

Evidence: `ErrorController::errorAction`.

Implementation: Implemented. Confidence: High. Display of the exception object depends on config that is not in Git.

## Integrations

| System | Purpose | Direction | Trigger | Auth | Failure |
|---|---|---|---|---|---|
| MySQL | Venues, sessions, items, nutrition, allergens | Read from this application | Each page action | Adapter in `application.ini` (not in Git) | Uncaught in the actions; surfaces through the error controller if the exception propagates |
| Google Chart Image API | QR code for the session URL | Browser loads the image URL | Menu page render | None in the URL | The menu HTML still renders; the image may be broken. Not handled in code |
| `www.sarugby.online` | Target encoded in the QR code | Outbound URL only | Menu page | None | This repository does not call that host |
| Google Fonts | Lora and Roboto stylesheets | Browser loads `http://fonts.googleapis.com` | Layout | None | Styles fall back. Not handled in code |
| AWS CodeDeploy via Bitbucket Pipelines | Deploy the tree | Pipeline pushes a zip to S3 and creates a deployment | Push to branch `qa` or `prod` | AWS keys from pipeline variables | `codedeploy_deploy.py` prints the AWS error and exits non-zero on client failures. Those branches are not on the remote |

No mail, queue, cron, or payment integration is referenced by the controllers.

## Constraints

- Timezone is `Africa/Johannesburg` (`public/index.php`).
- `IndexController` and `SessionController` set `memory_limit` to `180M` and turn `display_errors` on. `public/index.php` also sets `display_errors` to `1` and `error_reporting` to `E_ALL|E_STRICT`. The default `APPLICATION_ENV` is `production` when the environment variable is unset.
- A commented block at the top of `public/index.php` would replace the site with a maintenance message and exit. It is not active.
- The root `index.php` is empty. Routing depends on the root `.htaccess` sending traffic to `public/`.
- Request ids are interpolated into SQL strings in the models that the controllers call. There is no separate validation layer.
- Nutrition and allergen content is public to anyone who can request the ids.

Provenance: **Implemented**.

## Error behaviour

| Condition | Behaviour | Provenance |
|---|---|---|
| No venues, dates, sessions, or items | Headings render, list is empty, no message | Implemented |
| Unknown route | 404 "Page not found" | Implemented |
| Missing nutrition row | View uses object properties with no guard | Inferred failure |
| `Allergens` is not a JSON list | `json_decode` result is passed into `AID IN(?)` | Inferred failure or empty allergen set. Not executed |
| Database config missing | Zend cannot bootstrap | Inferred from the require of `application/configs/application.ini` and its absence from Git |
| QR or font host unavailable | Page still renders | Inferred |

## Planned behaviour

No TODO, roadmap, or ticket in the repository describes future work. Planned behaviour: **Unknown / Requires Confirmation**.
