# Business rules — orginal_code

Evidence date: 2026-10-08.

Source: `main` at `18caf8e`. Rules below are what the code enforces. They are **Inferred** as business rules unless a line is also written in `README.md`. The README does not state pace, midnight close, or permission rules. Do not treat a default number as an approved policy until someone confirms it.

## Access

### BR-001 — JWT proves identity, not a permission

**Inferred** from `middleware/auth.js`.

A request with a valid signature for `JWT_SECRET_KEY` proceeds. The token must carry the claims the signer put there (`userId`, `caddieId`, `playerId`, `name`). When `userId` is present, `User.lastSeen` is set to now. `role` and `permissions` on the user document are not checked in this middleware.

Failure response: `401` `{ valid: false }`.

### BR-002 — Tokens are issued only for known people

**Inferred** from `controllers/token.js`.

`POST /token` loads a user by `externalId`, otherwise a caddie, otherwise a player. If all three lookups miss, the response is `401` `{ valid: false }`. The body `name` is copied into the token and is not checked against the database name.

This route is also behind the server gate (BR-004).

### BR-003 — Refresh uses a fixed 7-day horizon

**Inferred** from `refreshToken`.

After the signature verifies, a new token is issued when `expiresAt` is missing or when `now + 7 days` is after `expiresAt`. Otherwise the same token string is returned. The 7 days are hardcoded. `JWT_EXPIRES_IN` still controls the new token's `expiresIn`.

### BR-004 — Central-server routes allow one IP, or everyone when local

**Inferred** from `middleware/server.js`.

`x-forwarded-for` or the socket address must equal `SERVER_IP`, or `ENV` must be `local`. Otherwise `401` `{ valid: false }`.

Used for `/token`, `/user-server`, `DELETE /device/devices`, and `DELETE /caddietag/delete/:email`.

When `ENV=local`, the IP is not checked. `.env.example` sets `ENV=local`.

### BR-005 — Two-way calls must name a known device

**Inferred** from `middleware/twoway.js`.

Header `DeviceIMEI` is required (`422` `{ message: "Device imei is required" }`). It must match `Device.deviceId` (`404` `{ message: "Device not found" }`). The device's Mongo id is then `req.deviceId`.

### BR-006 — Browser origin must be listed

**Inferred** from `app.js`.

`Origin` must be one of the comma-separated `ALLOWED_ORIGINS`. The example lists `http://localhost:3000` and `https://localhost:3000`. Other origins get `origin: false`.

### BR-007 — Course import and most device writes are not JWT-protected

**Inferred** from the mounts in `app.js` and `routes/device.js`.

`/course` has no auth middleware. Fix insert, device management, buzzer, and `sendAction` on `/device` have no auth middleware. Tracker routes on the same router add `auth` per route.

Whether that is intentional is **Requires Confirmation**.

### BR-008 — Roles and permissions are stored, then read from another service

**Inferred** from `models/user.js` and `controllers/modules/user.js`.

A user has `role` (string), `permissions[]` (`permissionId`, `active`, `name`, `role`), and `coursePermissions[]` (`courseId`, `active`, `name`). `GET /user/getAllRoles` returns whatever `GET {LOCALHOST}/api/roles` returns. This repository does not list the role names.

Notification fan-out uses permissions: `checkUserNotifications` loads users whose `permissions` contain `{ name: permission, active: true }`.

## Devices and rounds

### BR-009 — Marshal, superintendent, and drinks devices do not form player rounds

**Inferred** from `grinder.js`.

A fix is ignored for round building when `device.marshal`, `device.super`, or `device.drinks` is true. The comment says to remove this special case later.

### BR-010 — A fix joins a section only if it is inside or within one metre

**Inferred** from `grinder.js` and `models/section.js`.

The section field is a GeoJSON Polygon with a `2dsphere` index. The lookup is `$near` with `$maxDistance: 1`. On a 2dsphere index that distance is metres. A point inside the polygon has distance 0 and matches.

Section `properties.trigger` is commented as `start|stop`.

### BR-011 — One open round per device for the local day

**Inferred** from the grinder query.

The open round is the document with `_id` greater than today's start-of-day id, the same `device`, and `complete: false`. A new round starts when `isNextSection` returns `'new-round'`.

### BR-012 — Turn time defaults to 30 minutes

**Inferred** from `grinder.js` and `models/config.js`.

`turnTime` is `config.turnTime` or `1800` seconds. The schema default is also `1800`.

### BR-013 — Pace is slow only when the signed minute value is negative

**Inferred** from `modules/pace.js`.

`paceInMin` is `Math.floor(seconds / 60)`.

| Condition | Text | Colour helper |
|---|---|---|
| Held up | Delayed | Orange |
| Holding another round up | Delayer | Magenta |
| Pace minutes `< 0` | Slow | Red |
| Otherwise | On - Pace (object) or On Pace (`status()`) | Green |

`status()` returns `On Pace` without the hyphen. `paceInfo` returns `On - Pace`. Both are in the same module.

Related `Config` defaults, in seconds: `slowPlayMin` 600, `paceThreshold` -600, `positionalGap` 600, `gapTimeThreshold` 1080. The grinder does not re-read these on every label call inside `pace.js`. Confirm before treating the numbers as policy.

### BR-014 — Midnight closes unfinished rounds

**Inferred** from `schedule.js`. Not in the README.

At 00:00:00 in the timezone captured when the scheduler started:

- Devices with `assignment: 'temporary'` become `permanent`. The third argument `{ marshal: false }` is passed as Mongoose options, not as a field update, so `marshal` is unchanged.
- Rounds with `_id` after yesterday's local start-of-day and `complete: false` become `complete: true`, `isIncomplete: true`, `reason: 'Closed at midnight'`.
- Every device's `round` is set to null.

### BR-015 — Batch size for the grinder is the course config

**Inferred** from `grinder.js` and `models/config.js`.

`fixLimit` defaults to 750. A full batch immediately starts another grind. `.env.example` `FIX_LIMIT=1` is not read.

`fixHdopThreshold` defaults to 1.2, `fixSnrThreshold` to 17.5, `geoHdopThreshold` to 0.8, `geoSnrThreshold` to 21. They are selected for the two-way device payload. The grinder snippet that was read does not apply them.

### BR-016 — Temporary assignment and buzzer are device fields

**Inferred** from `models/device.js`.

`assignment` is a string. `isBuzzer` defaults to true. `marshal`, `drinks`, and `super` default to false. Staff routes `postAssignment`, `tagAssignment`, `postBuzzerToggle`, and `buzzerToggle` write these. The exact allowed assignment strings, other than `temporary` and `permanent` in the midnight job, are **Unknown**.

## Tee times

### BR-017 — Auto-assign defaults on

**Inferred** from `models/config.js`.

`canAutoAssign` defaults to true. `POST /teetime/autoAssignTagsToggle` changes it. The grinder copies the flag into `isAutoAssignActive`.

### BR-018 — A provider sync replaces the day's provider rows

**Inferred** from `teetimes/providers/totalunity.js`.

Reservations are saved with `providerId` and `providerName`. Local tee times for that date whose `providerId` and time are not in the response are deleted when `shotgun.goalTime` is null. Other providers were not compared line by line. Assume they follow the same `processTeetimes` export until checked.

### BR-019 — Default sheet is created only if today is empty

**Inferred** from `schedule.js`.

At 00:10, if no `Teetime` exists for today's `YYYY-MM-DD` and `Config.defaultTeetimes` is set, `loadDefaultTeetimes` runs.

### BR-020 — Scheduled pins win, otherwise pins are cleared

**Inferred** from `schedule.js`.

At 00:05, a `PinSchedule` with `startDate >= today`, `endDate <= today`, and a template has its pins written to holes. If none match, every hole's `pin` is set to null.

The date query requires the start to be on or after today and the end on or before today, so only a schedule that both starts and ends today matches. A multi-day range does not match this predicate. **Requires Confirmation** that this is the intended window.

### BR-021 — Shotgun tee times carry their own goal

**Inferred** from `models/teetime.js`.

`shotgun.goalTime` is seconds. `isNineHole`, `startSection`, `stopSection`, `isRetro`, and `group` describe the start. Creating a shotgun is `POST /shotgun/create` and `POST /shotgun/retroactive/create` (same controller).

### BR-022 — Geofence can be ignored per tee time

**Inferred** from `models/teetime.js` and `POST /teetime/ignoreGeofence`.

`isIgnoreGeofence` defaults to false.

`softGeofenceTimer` defaults to 10. The unit is not named in the schema.

## Food, messages, and waivers

### BR-023 — Kitchen hours are course-wide intervals

**Inferred** from `models/config.js`.

`kitchenOperatingHours[]` has `startTime`, `endTime`, and `days`. Staff replace them with `PATCH /foodandbev/updateKitchenHours` (body is stored as the array). The device can read `GET /two-ways/fnb/kitchen-hours`.

### BR-024 — Feature flags default as follows

**Inferred** from `models/config.js` defaults. Legacy import overwrites them (`modules/migration.js`).

| Flag | Default |
|---|---|
| `teetimesActive` | true |
| `returnDeviceActive` | true |
| `messagingActive` | false |
| `outOfSequenceActive` | false |
| `scoringActive` | false |
| `drinksActive` | false |
| `leaderboardActive` | false |
| `foodAndBeveragesActive` | false |
| `ratingActive` | false |
| `groupSafetyActive` | false |
| `playerAssignmentActive` | false |
| `playerDataEntryActive` | false |
| `teetimePinningActive` | false |
| `waiverEnabled` | false |
| `calibrationMode` | false |
| `maintenance` | false |

`playersDisplay` defaults to the string `Players`. `currency` defaults to `$`.

### BR-025 — Disclaimer fields are configurable

**Inferred** from `Config.disclaimer`.

`text`, `isActive` (default false), and `fields[]` (`label`, `key`, `required`, `keyboard` default `text`). The device accepts via `PATCH /two-ways/accept-disclaimer`. `Device.waverAccepted` stores a date. The field name is spelled `waver`.

### BR-026 — Notification buttons depend on the type string

**Inferred** from `modules/notifications.js` `getButtonsForType`.

| Type | Buttons |
|---|---|
| 2Way Message (In) | Reply, Dismiss |
| Request for Support, Request For Drinks Cart, 2Way Message Out, Geofence Breach Low, Geofence Breach Medium, Geofence Breach High, Interaction Recorded, Problem Group, Pace Notification, Round Rating | View Round, Dismiss |
| Food Order Placed | View Orders, Dismiss |
| System Notification | Dismiss |
| Any other string | No buttons |

A user's copy of a notification is created already dismissed when the matching user-notification row is inactive, or when it is active but not `alwaysOn` and the current time is outside `activeDays`.

### BR-027 — Support ticket required fields

**Inferred** from `controllers/course.js`.

`subject`, `queryType`, `description`, `deviceType`, and `deviceNumber` are required. Missing fields return `401` and the validator error object. Success text says the ticket "was successfully updated" even on create.

The Monday.com call does not run (it is after `return`).

### BR-028 — CSV uploads drop the first row and split on commas

**Inferred** from `middleware/csvprocessor.js`.

Used by `POST /teetime/csvManualUpload` and `POST /players/upload`. The file field must be `file`. An empty file is `400` with message `File not found`. Rows are split on commas only. Quoted commas are not handled.

## User settings defaults

### BR-029 — Report emails default on

**Inferred** from `models/user.js`.

`report.dailyProblemGroup` and `report.weeklyPaceReport` default to true. Display defaults: gap-time monitor collapsed, caddies shown, custom fields shown, `conditionsUnit` `imperial`, flags, superintendent, marshal, and drinks shown. Tee-time display defaults: Golf Genius tee times shown; additional players, caddies, data directory, ignore-geofence toggle, and player data entry hidden.

These are stored preferences. Nothing in the snippet proves a mailer sends the two reports.

## Rules that are not established

- Who may import a course or post a fix. The routes do not check a role.
- The list of roles and what each permission name allows.
- Whether midnight close, the one-metre section match, and the pace colours are approved.
- Whether a pin schedule is meant to cover more than one day (the query as written does not).
- Currency, tax, or pricing rules for food and for `rangeFinding.price` and `music.price`. The fields exist. No calculation was found in the files read for this document.
- Handicap, member number, and booking policy. `totalunity.js` has those player fields commented out.
- Data retention, audit, and who may restore a `mongoose-delete` soft-deleted document. The plugin is applied on the models that were opened (`Config`, `User`, `Device`, `Round`, `Teetime`, `Section`). `deletedBy` is on the plugin options. No route reviewed here exposes restore.
