# Business rules — scoringApp

Evidence date: 2026-10-04. Source: `git@bitbucket.org:overdrive/scoringapp.git`, `master` at `04b1a38`.

Provenance labels: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

No business-rules document was in the repository. Every rule below is taken from executable code. Rules marked **Inferred** are a reading of that code, not a signed-off law of the game. Where two code paths disagree, both are recorded.

Event display names other than the hardcoded strings in this file are downloaded (`Events.CommName`). This document uses the numeric ids the client branches on.

## Access

| Rule | Provenance |
|---|---|
| Login is online only. The password is not checked against the local `Users` table | **Implemented** (`Loginn.CheckOnline`) |
| Both the username field and the password field must be non-empty or the click does nothing | **Implemented** (`Login.Button_Click`) |
| A response with `UserID` greater than 0 is treated as success | **Implemented** |
| The password returned path stores the password in SQLite as the user typed it. `MD5Hash` is never called | **Implemented** |
| The next launch opens the fixture list when any `Users` row has `LoggedIn = 1` | **Implemented** |
| Logout sets `LoggedIn = 0` for those rows. It does not remove the password | **Implemented** |
| No screen checks `UnionID` or any other permission after login | **Implemented** absence of a check |
| Fixture download and commentary upload send no auth header | **Implemented** |

Who is allowed to receive fixtures is decided by the Footprint API. **Unknown** in this repository.

## Fixture data the client keeps

From the download (`Webservices.SetFixture` / `setTeam`):

- Identity and schedule: `FID`, date, start time, location, fixture number, stage, round, tournament id and name.
- Teams: home and away ids, names, abbreviations.
- Points: try, conversion, penalty, drop goal, penalty try, `Portry`.
- `Allow22MeterTry`, `Code`, half length `HtimeMin` stored as `HalfTime`.
- Timers: `YELLOWCARDTIMER`, `BLOODTIMER`, `HIATIMER`, and `YELLOWCARDCOUNTER` (the counter is stored; the sin-bin length used on screen is `YellowCardTimer`).
- Officials whose `PosDescription` is exactly `Referee`, `Assistant Referee 1`, or `TMO`.
- Players: user id, names, nickname, position, vice, captain, jersey, and `Front` when the team-side list contains the same user id. `CurrentPos` starts equal to `Pos`. `Location` is set to 0. `Time` is set to 0.

The event catalogue (`EVENTS` and `SESSIONS`) is written only while processing the first fixture in the JSON array.

Provenance: **Implemented**.

## On-field and bench

**Inferred**, from `GetPlayers` and `getLineUpTab`:

- `Fixture.Code` is the last on-field position number. The lineup tab creates that many rows.
- A player is offered as on-field when `CurrentPos <= Code` and `Location < 3`.
- A player is offered as a substitute when `CurrentPos > Code` and `Location < 3`.
- The general player list (not the sub-selection list) also hides players with `Returned > 0`.

`Location` values used by queries:

| Value | Use in code | Provenance |
|---|---|---|
| 0 | Set on download. Restored when a yellow or red commentary row is deleted | **Implemented** |
| 3 | Yellow / sin bin. `GetYellowCards` filters `Location = '3'`. Deleting event 12 sets location back to 3 | **Implemented** |
| 4 | Red card. `GetRedCards` filters `Location = '4'`. `Player_clicked` returns immediately | **Implemented** |

Other location numbers are not given names in code. **Unknown**.

## Match phase

**Implemented** status machine in `GetSession` / `GetSessionBTNS`:

| Status | Session stored on new commentary | Phase label from the button, not from the switch comment |
|---|---|---|
| 0 | 0 | Not started (`MATCH START`) |
| 1 | 0 | First half running |
| 2 | 0 | First half ended |
| 3 | 1 | Second half running |
| 4 | 1 | Second half ended |
| 5 | 2 | Extra time, first period running |
| 6 | 2 | Extra time, first period ended |
| 7 | 3 | Extra time, second period running |
| 8 | 3 | Extra time, second period ended |
| 9 | 3 | Comment in `GetSession` says END. No dedicated button |
| 10 | 3 | Completed. Clocks stop |

The comments inside `GetSession` call status 0 "1H Started". The button for status 0 is `MATCH START`, and status 1 is the running first half. The button behaviour is what the operator sees.

Scoring clicks are ignored unless status is greater than 0 and less than 10.

Ending a phase writes a commentary event:

| New status | Event id written |
|---|---|
| 2 (first half ended) | 28 |
| 4 (second half ended) | 39 |
| 6 (extra-time first period ended) | 32 |
| 8 (extra-time second period ended) | 33 |
| 10 (completed or abandoned) | 30 |

Starting a phase selects an event id but the kickoff handlers do not themselves insert it. They open that side's player list:

| New status | Event id selected |
|---|---|
| 1 | 27 |
| 3 | 29, and the playing clock is later forced to `HalfTime:00`, or `40:00` if `HalfTime` is empty, when `Process_Data` is called with `set_time == 1` |
| 5 | 31 |
| 7 | 33 |

**Conflict (Implemented in both places):** event 33 is both the extra-time second-half start selection and the extra-time second-half end insert. **Unknown / Requires Confirmation** which id the server catalogue actually uses for those two moments.

Choosing a team on the kickoff popup records `Time_Started` as the local `DateTime` and increments status. The product meaning of that team (side kicking off versus side to receive) is **Unknown / Requires Confirmation**.

## Clock

**Implemented:**

- While running and not paused, playing time advances and is written to `Fixture.Playing_Time`.
- Time OFF sets `Game_Paused = 1`, stops the playing stopwatch, and runs stoppage stopwatches. Playing time is drawn in red.
- Time ON sets `Game_Paused = 0` and writes commentary `Play resumed` with comment `Duration: {stoppage}`.
- Time-off reasons accepted by the popup: Weather or Other, Substitution, TMO, Strategic Break, Foul Play, Clothing, H.I.A, Talking to AR, Injury. The raw button name is stored when the `switch` does not rename it.
- During status 1 only, the break-time label is shown between 35 and 40 minutes (green, countdown toward 40) and between 40 and 45 minutes (red, `+` time). Outside that window the label is collapsed. These bounds are literals. They are not read from `HalfTime`.
- When the first half ends, if playing time is past 40:00 the commentary comment is `REF TIME +` that excess. The 40:00 subtract is a literal, not `HalfTime`.
- `Game_Duration` is a separate stopwatch (`dt_Tick_pT` / `sw_pt`) from playing time. Commentary `Time` is usually `Playing_Time`, while the sort key `No` is taken from `Game_Duration`.

**Inferred:** playing time is the match clock the operator sees as the event time, and game duration keeps moving in a different way (it is started on resume and on the pause path). The exact product names "playing time" versus "game duration" are the control names `Playing_Time` and `Game_Duration`.

## Scoring

**Implemented** in `Score.SetScore`. Counts come from commentary. Points per event come from the fixture, not from constants in `Score.cs`.

| Commentary `EventID` | Counted as | Points column |
|---|---|---|
| 2 | Try | `Tries` |
| 3 | Conversion | `Conversion` |
| 4 | Penalty | `Penalties` |
| 5 | Drop goal | `Dropgoal` |
| 6 | Penalty try | `PenaltyTry` |
| 53 | `Portry` | `Portry` |
| 11 | Yellow card (count only) | none |
| 14 | Red card (count only) | none |
| 40, 43, 44, 45, 46, 48, 49, 50, 51 | Substitution count | none |

Half score (`H_HS`, `A_HS`) includes only rows with `Session == 0`. The fields named full score (`H_FS`, `A_FS`) include every session. **Inferred:** extra-time events change the figure shown as the full score. They do not have a separate extra-time total.

Event 53 is omitted from the event button list unless `Allow22MeterTry` is 1. A comment on the click handler calls case 53 a drop goal. The score method does not. **Conflict.** **Unknown / Requires Confirmation** whether 53 is a 22-metre try, a power try, or a mis-labelled drop goal. The column name in schema and download is `Portry` / `Portry_Points`.

`GetBlueCards` counts event 55 and is not called. Blue cards do not change the scoreboard. The click handler for event 45 (comment: blue card) does not save a row.

Penalty try (event 6) asks the operator to confirm before save. The confirm copy uses the squad name.

A loop at the start of `SetScore` writes "Error: {player} - {event}" to the console when a home scoring player is still in `GetLineUP_IDs`. It does not block the save. `GetLineUP_IDs` returns every player on the side, not only those on the field (the position filter is commented out). **Inferred:** that console line is not a business rejection.

## Cards

**Implemented:**

- First yellow (no existing event-11 commentary for that player on the fixture): show a yellow card, set `Location` to 3, and arm a timer of `YellowCardTimer` minutes.
- If `Count_YellowCards_Player` is not 0: do not start that timer. Write commentary with comment `Second Yellow Card`, set the event name to `Red Card`, set `EventID` to 14, and set `Location` to 4.
- Red card sets `Location` to 4 and adds a red block. The player cannot be picked again while location stays 4.
- Deleting the yellow or red commentary row sets `Location` to 0 and `Started` to 0.
- Deleting event 12 sets `Location` to 3. **Inferred:** event 12 is the yellow-card return, so deleting the return puts the player back in the sin bin.

**Inferred:** two yellows for one player in one fixture become a sending-off. The code uses "any prior yellow row", not a check that the count is exactly one. A third yellow click follows the same red path.

The timer lengths for blood and HIA are fixture fields. The code does not document a law-of-the-game duration. **Unknown** what those minute values are supposed to be when the API sends them; the client multiplies by 60.

## Substitutions and injuries

**Implemented:**

- Group 3 events are labelled `Substitution` on the commentary event name, and the previous event name is kept in the comment.
- The replacement list is players with `CurrentPos > Code` and `Location < 3`.
- On save, `CurrentPos` of the player going off and the player coming on are swapped.
- Event 44 saves immediately inside `Subs_Clicked` before the injury-card branch.
- Events 49 and 50 take a different injury path (no third row on the card; return handlers exist).
- While the match is paused, push and injury actions can be stored and applied on Time ON (`HandlePushOnResume`, `HandleInjuryOnResume`, `HandleInjuryPermanentOnResume`, `HandleInjuryReverseOnResume`).
- Deleting a substitution-like event (ids 40, 43, 44, 45, 46, 48, 49, 50, 51, 55, 57) swaps `CurrentPos` back, using each player's current position at delete time.

The legal reason for each substitution id is the downloaded `CommName`. This client does not restate it. **Unknown / Requires Confirmation** which ids are tactical, blood, HIA, injury, or front-row.

Commit `cdd8d70` says the time-off substitution workflow is "as Heinrich described". That description is not in the repository.

## Commentary edit and delete

**Implemented:**

- Delete asks for confirmation, then removes the row by `Commentary.ID`.
- Edit loads time (the code indexes characters 0, 1, 3, and 4 of `Time`, so it expects `mm:ss`), event, squad, player, and reason.
- Session events 30 and 28 hide the player picker in the session-edit popup.
- Upload sends the commentary list as one JSON form field. It does not send the `Scores` row. Whether Footprint recomputes the score is **Unknown**.

## Offline fixture form

**Implemented, and it does not match the schema this app creates.** `SetOfflineFixture` inserts columns the `CREATE TABLE Fixture` statement does not have, omits a comma in the column list, and reads dictionary keys the form does not set. The exception handler does not write an alternate update. Treat "the operator can create a match without Footprint" as a screen that exists, not as a rule that succeeds. **Unknown / Requires Confirmation** whether an older database file shipped in `Database\Footprint.db` has the extra columns. That file was not opened for this document.

## Reset and abandon

| Action | Rule | Provenance |
|---|---|---|
| Reset (`Border_MouseDown` on the scoring page) | Delete all commentary for the fixture. Set `Game_status` to 0 and the clock strings to `00:00`. No confirm dialog in the method | **Implemented** |
| Abandon | Set `Game_status` to 10 and write event 30, same as END GAME | **Implemented** |
| Exit | `Environment.Exit(1)` | **Implemented** |

## Rules that must not be invented from names

- Standard rugby values (5 for a try, 2 for a conversion, 10 minutes for a yellow card, 15 players) are not constants in the scorer. They arrive per fixture, except the hardcoded 35/40/45 minute display and the 40:00 fallback.
- `Allow22MeterTry` only controls whether event 53 is listed. It does not change point arithmetic.
- There is no local check for maximum substitutions, maximum sin bins, or whether a conversion follows a try.
