# Product requirements — magusEventsXamarin

Evidence date: 2026-10-09. Inspected revision: `master` at `607005b` (2020-08-17).

Source: `git@bitbucket.org:overdrive/maguseventsxamarin.git`.

No requirements document existed in the source repository or in `silicon_ark/magusEventsXamarin/` before this file. Every requirement below is recovered from the implementation. Where the code is the only source, the status line says **Implemented** and the confidence notes an inference. Nothing here is a **Documented Requirement** from a product author.

The header label is **MAGUS | 1.6.0**. The product the code implements is an on-site scanner for one Footprint accreditation event at a time.

## Users and roles

The repository does not define accounts, roles, or a login.

| Actor | Evidence | Status |
|---|---|---|
| Scanner operator | Uses the scan screens. Barcode input is a hidden `Entry` (`ApplicationScan.xaml`, `VAPScan.xaml`). A hardware wedge is **Inferred**. | Implemented |
| Settings operator | Opens settings or mode only after a four-digit code (`OTPDialog.xaml.cs`). The same code is compiled into the binary. There is no per-person identity. | Implemented |

There is no server session. API calls send no `Authorization` header.

## Startup

**FR-001 — Choose the first screen from the saved mode**

- Trigger: process start (`App` constructor).
- Input: `Application.Current.Properties["Mode"]`, if present.
- Behaviour: `"0"` opens `ApplicationScan`. Any other saved value opens `VAPScan`. If the key is absent, the first screen is `EventScan`, whose only content is the label "- SELECT EVENT -".
- Output: one `MainPage`. `OnStart`, `OnSleep`, and `OnResume` are empty.
- Evidence: `App.xaml.cs`.
- Status: **Implemented**. Confidence: high.

`EventScan` does not scan. Settings and mode on that screen still go through the code dialog.

## Access to settings and mode

**FR-002 — Fixed code before settings or mode**

- Who: anyone holding the device.
- Trigger: the header SETTINGS button, or the mode path that still calls `ShowOTPDialog`.
- Input: four single-digit numeric entries.
- Behaviour: the four characters are compared to four literals in `OTPDialog.xaml.cs`. A match hides the dialog, hides the keyboard, and navigates to the page that was requested. A mismatch shows the dialog again with empty fields. The literals are not repeated in this document.
- Validation: each entry has `MaxLength` 1 and a numeric keyboard.
- Error: wrong code does not show a message. It resets the dialog.
- Evidence: `OTPDialog.xaml`, `OTPDialog.xaml.cs`, `ApplicationScan.xaml.cs`, `VAPScan.xaml.cs`, `EventScan.xaml.cs`.
- Status: **Implemented**. Confidence: high.

Once the operator is on a settings page, the header control navigates to the next settings page without asking for the code again (`ApplicationSettings` and `VAPSettings` header handlers).

**FR-003 — Persist the scan mode**

- Trigger: APPLICATION SCAN or VAP SCAN on `SelectMode`, or NEXT on `EventSettings`.
- Input: mode `"0"` (application) or `"1"` (VAP). `EventSettings` stores `picker_mode.SelectedIndex` as a string. The picker items are "APPLICATION MODE" then "VAP MODE", so index 0 and 1 match those strings.
- Behaviour: writes `Properties["Mode"]` and calls `SavePropertiesAsync`, then opens the matching scan page.
- Evidence: `SelectMode.xaml.cs`, `EventSettings.xaml.cs`, `EventSettings.xaml`.
- Status: **Implemented**. Confidence: high.

## Event selection

**FR-004 — Load events and save one active event**

- Trigger: `EventSettings` starts a one-second timer that calls `getEvents` until NEXT or the header settings action clears the flag.
- Input: online check is `WebClient.OpenRead` of `https://www.footprintapp.net/api/tourn/`. Event list is `GET {Server}api/Accreditation/getevents`.
- Behaviour when online: picker title becomes "LOADING EVENTS...", then each `AccreditationName` is added, and the row whose `AC` equals the active id is selected.
- Behaviour when offline: picker title becomes "SELECT EVENT". No rows are added.
- On NEXT, if a row is selected: every `Events` row with `AC > 0` is set `Active = 0`, then the chosen event is updated or inserted with `Active = 1`. Fields written: `AC`, `AccreditationName`, `DateFrom`, `DateTo`, `Location`, `Logo`, `MealDays` (from JSON `TotalDays`), `MealsPerDay`. `AppState.AID` is re-read. The header info text becomes the event name, truncated to 23 characters when longer than 24.
- Then the mode is saved and the flow opens application zone settings or VAP settings.
- Error: download and SQL failures are empty `catch` blocks. NEXT with no JSON and no selected index still saves the mode and navigates.
- Evidence: `EventSettings.xaml.cs`, `Webservices.CheckForInternetConnection`, `AppState`.
- Status: **Implemented**. Confidence: high.

**FR-005 — The active event id is the active SQLite row**

- `Database.GetAccreditationID` runs `SELECT * FROM Events Where Active = 1` and returns the `AC` of each row in turn, so the last row wins. No `ORDER BY`. If the query throws, the id is 0.
- `AppState` reads this once at construction. `EventSettings` updates the property after a successful save. Other pages captured `AID` in a field at construction.
- Evidence: `Database.GetAccreditationID`, `AppState`, `EventSettings.Button_next_Clicked`.
- Status: **Implemented**. Confidence: high.

**FR-006 — Exit kills the process**

- The EXIT button on event settings calls `Process.GetCurrentProcess().Kill()`.
- Evidence: `EventSettings.Button_exit_Clicked_1`.
- Status: **Implemented**. Confidence: high.

## Application mode

**FR-007 — Show zones for the active event and save Active and Lock**

- Trigger: `ApplicationSettings` opens and, when the internet check passes, downloads the same event list as FR-004.
- Behaviour: finds the active event, reads its `zones` array, and builds a grid of zone number, name, Active checkbox, and Lock checkbox. Existing SQLite `Lock` and `Active` are kept when a zone row already exists. `FromTime` and `ToTime` are set to `"00:00"` in the in-memory object.
- SUBMIT writes an `UPDATE` for each zone and, when that update reports no row, an `INSERT`. Navigation returns to `ApplicationScan`.
- Empty list: SUBMIT still navigates.
- Evidence: `ApplicationSettings.xaml.cs`. `Database.SaveZoneActive` and `SaveZoneLock` exist and are not called (the calls are commented). The SQL in SUBMIT is what runs.
- Status: **Implemented**. Confidence: high.

**FR-008 — Download applications for the active event**

- Trigger: DOWNLOAD APPLICATIONS.
- Behaviour:
  1. `DELETE FROM Persons WHERE AAID > 0`.
  2. Repeated `GET {Server}api/accreditation/getapplicantsandroid/ac/{AID}/id/{AASID}` until a page returns no rows or `save_to_db` returns 0.
  3. Then `GET {Server}api/accreditation/getaccreditationupdated/ac/{AID}`. If `AccrImportFileLastDateTime` is later than the local `{AID}.json` creation time, download `https://footprintapp.net/local_data/accreditations/{AID}.json` and import it.
- A person JSON object with an empty `Barcode` is not written (`Person.save_to_db`, comment dated 20190718).
- API rows are updated or inserted by `AASID` when `AASID > 0`. `RoleColor` is stored from JSON `Color`.
- Import-file rows: existing `Persons` with `AASID < 0` are deleted, then each barcoded row is inserted with `AASID` starting at `-1` and decreasing, and `AAID` `'0'`.
- Progress text is driven by a local counter, not by a reliable fraction of the download. Failure text is "Unable to download data".
- The paging id is the `AASID` of the last `Persons` row returned for the event, and only if that value is `> 0`. `GetPersonProfile` keeps the last row of an unordered query.
- Evidence: `ApplicationSettings.xaml.cs`, `Person.cs`.
- Status: **Implemented**. Confidence: high.
- The 240-second and 300-second timers that used to call `CheckUpdates` and `CheckAccreditations` from the scan screen are commented out, with the comment "TURNED OFF ACCREDITATIONS SYNC ... AND UPDATE TIMERS". Those methods still exist on `Webservices`.

**FR-009 — Scan an application barcode**

- Trigger: the hidden `BarcodeScanView` raises `Completed` (Enter from a wedge, **Inferred**).
- Preconditions: active event id `> 0` and a non-empty barcode. Otherwise the method returns without changing the screen.
- Lookup: `Persons` where `Barcode` and `AC` match. `GetByNFC` (lookup on `SCANID`) is only used when the caller passes `nfc = 1`. No page does that.
- When a row is found (`AASID != 0`): show first name, surname, organisation, category, and role. Role background is `RoleColor`, or `#00981E` when that string is empty.
- Zone decision (details in business rules BR-007 to BR-010): granted when at least one matching active zone is found, or when the all-access flags force that path. Otherwise denied.
- Granted: green `#77B13D`, text GRANTED, granted sound. Denied: red `#BF360C`, text DENIED, denied sound. Unknown barcode: labels "Invalid" / "N/A" and the same denied path.
- A missing person still writes a scan row. `Profile.AAID` is 0 in that case.
- About ten seconds after a completed scan, and also about ten seconds after the page is created, the page navigates to a new `ApplicationScan` (`BUSY` starts at 5 and a two-second timer decrements it).
- Sounds go through `IAudio`. Only Android registers an implementation. The calls are in `try/catch`.
- Evidence: `ApplicationScan.xaml.cs`, `Person.GetRow`, `Services/IAudio.cs`, `MagusEvents.Android/Services/Audio.cs`.
- Status: **Implemented**. Confidence: high.

**FR-010 — Write an application scan to ScanLog**

- After a grant or deny decision, `ApplicationScan` inserts one `ScanLog` row.
- `ScanType` is the literal `3`. `ScanStatus` is `1` for granted and `0` for denied. `ScanSentTag` is `'0'`. `ProfileID` is `Persons.AAID`.
- The value list does not line up with the column list after `ProfileApprovedZones`. `ProfileStartDate` receives the zone-number string. `ScanPrimaryZone` receives the literal `1`. `ProfileFullName` does receive "first surname". See BR-014.
- An earlier text-file log (`scan_{AID}.txt`) is commented out. The comment dates the table write to 20200804.
- Evidence: `ApplicationScan.UserProfile`.
- Status: **Implemented**. Confidence: high. The column alignment is what the source does. Whether it was intended is **Requires Confirmation**.

## VAP mode

**FR-011 — Loading zones and one gate**

- Trigger: `VAPSettings` downloads the event list when online.
- Behaviour: uses JSON `loadingzones` and `gates` for the active event. The selected index starts at 1 and is then reduced by 1, so a match on the first API row still addresses index 0. If the active id is not in the list, `selected_index` stays 0 and `PopulateGatesAndZones` returns without filling the grid (`UID > 0` fails).
- SUBMIT saves loading zones the same way FR-007 saves zones (`Loading_Zones`). If a gate is selected, `DELETE FROM Gates` then insert that one gate.
- Evidence: `VAPSettings.xaml.cs`.
- Status: **Implemented**. Confidence: high.

**FR-012 — Download permits and applicants**

- Trigger: DOWNLOAD PERMITS. The button starts both downloads.
- Permits: `DELETE FROM Permits WHERE PID > 0`, then pages `GET {Server}api/accreditation/getvapandroid/ac/{AID}/id/{PID}` until a page is empty. Permits with an empty barcode or `PID` not greater than 0 are skipped.
- Applicants: same delete-and-page behaviour as FR-008 step 1 and 2. This path does not download the accreditation JSON file.
- Failure text: "Unable to download Permits" or "Unable to download Applications".
- Evidence: `VAPSettings.xaml.cs`, `Vap.save_to_db`.
- Status: **Implemented**. Confidence: high.
- The 240-second VAP and applicant update timers on `VAPScan` are commented out. `Webservices.CheckVapUpdates` remains.

**FR-013 — Scan a vehicle permit**

- Trigger: `VehicleBarcode` text changed, and `busy_scanning` is false.
- Lookup: `Permits` where `Barcode` and `AC` match.
- Gate: load the single `Gates` row (`GetRow()` with no id). Access is allowed only when `Gates.Number` equals `Permits.Gate`. If no gate row exists, `Number` is null and access stays false.
- When `PID > 0` and the gate matches: show load type, date, company, time in and out, vehicle model, licence, service type, and gate. Zone text follows the loading-zone form of the application rules (business rules). Granted plays the granted sound and shows "VEHICLE GRANTED" in green `#147114`.
- If `DriverScanRequired` is `"1"`, the driver entry is focused and the prompt "SCAN DRIVER" is shown. Otherwise, if `DriverName` is non-empty, the driver name is shown, and the screen resets after five seconds.
- Denied: "VEHICLE & GATE ACCESS DENIED" when the gate flag is false, otherwise "VEHICLE DENIED", red background, denied sound, reset after five seconds.
- VAP scan does not insert `ScanLog`.
- Evidence: `VAPScan.xaml.cs`, `Vap.GetRow`, `Gates.GetRow`.
- Status: **Implemented**. Confidence: high.

**FR-014 — Scan the driver after a granted vehicle**

- Trigger: `DriverBarcode` text changed. This handler does not check `DriverScanRequired`. Any change in that entry runs the check.
- Lookup: `Persons` by barcode and active event id.
- Grant when a person exists and `Permits.AccreditedStaff` for the vehicle barcode contains that person's `AAID` as a substring.
- Deny with "DRIVER NOT LINKED TO PERMIT" or "DRIVER NOT FOUND".
- The screen resets after seven seconds on every path, including a failed lookup.
- Evidence: `VAPScan.DriverBarcode_TextChanged`.
- Status: **Implemented**. Confidence: high.
- `Contains` on the staff string is **Implemented**. Whether a short `AAID` must not match inside a longer one is **Requires Confirmation** (BR-018).

CANCEL on the VAP scan screen resets the page immediately.

## Scan sync

**FR-015 — Operator sync of unsynced scan rows**

- Trigger: SYNC SCANS on event settings, and the internet check passes.
- Behaviour: `SyncScanLog` loads up to three `ScanLog` rows for the active event with `ScanSentTag = '0'`, URL-encodes that list as JSON, and calls `GET {Server}api/accreditation/scanlog/scanlog/{json}`. It then sets `ScanSentTag = 1` on those rows before the download-completed handler runs. The handler body is commented out. The loop repeats until a fetch returns no rows.
- The button then shows "Scans synced succesfully" (spelling as in source) even when the HTTP call has not finished. The alert sits after the call, and `DownloadStringAsync` does not throw for a later HTTP failure. The `catch` shows "Scan upload failed".
- Evidence: `EventSettings.Button_upload_scans_Clicked`, `Webservices.SyncScanLog`, `Database.GetScanLog`, `ScanLog.updateScanSentTag`.
- Status: **Implemented**. Confidence: high.
- An older `UploadScanLog` FTP upload is still started every 30 minutes from `ApplicationScan`, and is commented out on the SYNC SCANS button. The text file it uploads is no longer written by the scan path. FTP credentials are literals in `Webservices.UploadScanLog` and are omitted here.

## Offline and empty states

**FR-016 — Offline settings**

- When `CheckForInternetConnection` is false, event, zone, and permit downloads do not start. Scan screens still query SQLite.
- A failed internet check is any exception from opening `https://www.footprintapp.net/api/tourn/`, including a non-success that throws. It is not a general socket check.
- Evidence: `Webservices.CheckForInternetConnection` and the settings pages.
- Status: **Implemented**. Confidence: high.

## Integrations

| Call | Direction | Auth | Used by |
|---|---|---|---|
| `GET {Server}api/Accreditation/getevents` | Footprint → device | none in this client | Event, application, and VAP settings |
| `GET {Server}api/accreditation/getapplicantsandroid/ac/{AC}/id/{AASID}` | Footprint → device | none | Application and VAP settings downloads |
| `GET {Server}api/accreditation/getaccreditationupdated/ac/{AC}` | Footprint → device | none | Application settings, and `Webservices.CheckAccreditations` (timer off) |
| `GET https://footprintapp.net/local_data/accreditations/{AC}.json` | Footprint → device | none | Application settings import. Host is not `AppState.Server` |
| `GET {Server}api/accreditation/getvapandroid/ac/{AC}/id/{PID}` | Footprint → device | none | VAP settings |
| `GET {Server}api/accreditation/getapplicantsupdatesandroid/ac/{AC}/date/{stamp}` | Footprint → device | none | `CheckUpdates` (timer off) |
| `GET {Server}api/accreditation/getvapupdatesandroid/ac/{AC}/date/{stamp}` | Footprint → device | none | `CheckVapUpdates` (timer off) |
| `GET {Server}api/accreditation/scanlog/scanlog/{json}` | device → Footprint | none | SYNC SCANS |
| `FTP` upload of `scan_{AC}.txt` | device → Footprint | username and password literals | 30-minute timer on application scan |
| `GET https://www.footprintapp.net/api/tourn/` | connectivity probe | none | every online check |

`Server` is `https://footprintapp.net/` (`AppState`). Expected JSON fields are whatever `save_to_db` reads. Response codes are not checked. Failures are swallowed.

The API server itself is not in this repository.

## Constraints visible in the client

- One active event (`Events.Active = 1`).
- One gate row after VAP settings submit (`DELETE FROM Gates` then one insert).
- Application profiles and permits without a barcode are dropped on save.
- Settings and the mode picker from a scan screen require the compiled code.
- Android min SDK 21, target SDK 28. iOS minimum 8.0.
- No automated tests are in the tree.

## Removed or dormant behaviour (still in source)

- Text scan log file. Write is commented. FTP upload of that file is still scheduled.
- Periodic applicant, accreditation, and VAP updates. Timers commented. Methods remain.
- Meal tables and `GetFoodProfile` / `GetEvent` readers. The methods return an empty object. `MealDays` and `MealsPerDay` are still copied onto `Events` and are not shown on a scan screen.
- NFC lookup. Unreachable from the UI.
- `GetBarcodeLength` body is commented and the method returns 0. No screen enforces a barcode length.

## Open points

See the confirmation list in `project-plan.md`. The ones that change a requirement if answered differently are the VAP name, the meaning of zone `312` and zone type `24`, the meaning of scan type `3`, and whether the scan-log column shift and the early `ScanSentTag` update are accepted behaviour or defects.
