# Business rules — unity-lambda-api

Evidence date: 2026-09-30.

Source: `git@bitbucket.org:overdrive/unity-lambda-api.git`, `master` at `fe4b095` (2021-03-17).

There is no business-rules document in the source repository. Every rule below is taken from code, SQL, or comments. Rules that the code enforces are **Implemented**. Rules that are a reasonable reading of that code, and are not written down as policy, are **Inferred**. Comments that the code does not follow are called out. Nothing here is an approved product policy until someone confirms it.

Unwired files are included only in the last section. They are not rules of the deployed API.

## Access

### BR-001 — HTTP calls must pass AWS IAM

**Implemented.** Each HTTP event sets `authorizer: aws_iam`. There is no API key, no JWT check, and no Cognito authorizer resource in `serverless.yml`.

### BR-002 — Comments require Cognito; the template requires IAM

**Discrepancy.** Comments on `getuseruuid`, `tableall`, and the four calendar functions say "cognito must be logged in". The authorizer is `aws_iam`. `getuseruuid.js` does not read the Cognito identity. It filters `sysUser` by the path id.

`create.js`, `get.js`, and `logging.js` do read `cognitoIdentityId`. If that field is missing, those reads throw or store an empty user id. There is no explicit "must be present" check.

**Unknown / Requires Confirmation:** whether API Gateway is wired to a Cognito identity pool outside this repository.

### BR-003 — A note belongs to the caller identity

**Implemented** in `create.js` and `get.js`. The item key is `userId` = `cognitoIdentityId` and `noteId`. Get will not return another identity's note id, because both key parts are required.

### BR-004 — System-user reads are not limited to the caller

**Implemented.** `getuser` and `getdetails` use the id the caller sends. `getuseruuid` uses the uuid the caller sends. `getusers` returns every row in `sysUser`. None of these queries compare the row to `cognitoIdentityId`.

**Inferred:** any caller who can invoke the route can read the `sysUser` rows the SQL returns, including every column `SELECT *` includes.

### BR-005 — CORS is open

**Implemented.** Handler responses set `Access-Control-Allow-Origin` to `*` and `Access-Control-Allow-Credentials` to `true`. Gateway default 4XX and 5XX do the same for origin and allow every header.

## System users

### BR-006 — A system user is a row in `sysUser`

**Implemented** as the table name in the routed queries. Columns named by those queries: `UserID`, `strCognitoUuid`. Other columns returned by `SELECT *` are **Unknown**.

### BR-007 — User id and Cognito uuid are different lookups

**Implemented.**

- Numeric `UserID`: `GET /unity/user/{id}` and `POST /unity/getdetails` (`body.id`).
- `strCognitoUuid`: `GET /unity/uuid/{id}`, value placed in double quotes in the SQL.

### BR-008 — Listing users ignores the path id

**Implemented** in `getusers.js`. The route is `POST /unity/getusers/{id}` and the SQL is `SELECT * FROM sysUser` with no `WHERE`.

**Inferred** from the unused parameter and the YAML comment ("path is /unity/users", method GET): the path and method were not aligned with the handler. That is a defect observation, not a rule that the id should be ignored.

## Generic data access

### BR-009 — Action codes are single letters

**Implemented** in `global.js`.

| `data.a` | Result |
|---|---|
| `s` | `SELECT {c} FROM {t}` |
| `u` | `UPDATE {t} SET {c} ` |
| `i` | `INSERT INTO ` |
| anything else | `failure("No Action")` |

The file comment names Select, Update, Insert, and Delete, and nests the fields under `data`. The function does not implement that comment. Delete is not a code.

### BR-010 — Join and where text come from `data.con`

**Implemented.** The lengths checked are `data.j` and `data.w` (called as functions). The text appended is `data.con` in both branches, not the join array and not `data.w`.

**Inferred:** this does not match the comment, which describes a join array `j` and a condition `w`.

### BR-011 — Any table can be read by name

**Implemented** in `tableall.js`: `SELECT * FROM `{table}` WHERE {where}` using the JSON body. No allow-list.

### BR-012 — An empty result list is still success

**Implemented.** The handlers test `if (results)`. A MySQL result array is truthy even when it has no rows, so the success helper runs. `failure()` runs only when `results` is falsy.

## Calendar

Rules in this section are the SQL in the calendar handlers. They apply only if `event.pathParameters` contains the fields. The HTTP routes do not define those parameters (see the PRD). **Requires confirmation** before treating them as live API rules.

### BR-013 — A calendar event is a `crmCalendar` row

**Implemented** as the table name. Columns written: `txtDescription`, `txtNotes`, `dtStartDate`, `dtEndDate`. Optional since `fe4b095`: `strFirstUser`, `refSysUserID`. Identity column used by update and delete: `CalendarID`.

### BR-014 — Dates are formatted in MySQL

**Implemented.** Start and end values are passed through `DATE_FORMAT(..., '%Y/%m/%d %H:%i:%s')` on insert and update. The list query compares the date part only (`%Y/%m/%d`).

### BR-015 — Optional user fields are omitted when the property is absent

**Implemented** in `addcalendarevent.js` and `updatecalendarevent.js`. `strFirstUser` and `refSysUserID` are added to the statement only when `pathParameters.hasOwnProperty` is true for that name. Description, notes, and both dates are always referenced. If those required properties are missing, the SQL still interpolates them (as the string `"undefined"` if the value is missing and the object itself exists).

### BR-016 — List events for a user id inside a date window

**Implemented** in `getcalendarevents.js`. The row must have `refSysUserID` equal to `userID` or the predicate `refSysUserID = NULL`, and both the start day and the end day must fall between `dtStartDate` and `dtEndDate` from the request.

**Inferred:** `= NULL` was meant to include events with no user. MySQL does not treat `= NULL` as `IS NULL`, so null `refSysUserID` rows do not match that predicate.

**Inferred:** an event that overlaps the window but starts before it or ends after it is excluded, because both dates must sit inside the window.

### BR-017 — Update and delete address the row by `CalendarID` only

**Implemented.** Neither statement filters on `refSysUserID` or the caller identity.

**Inferred:** the SQL will change or remove any calendar row whose id is supplied, not only rows owned by the caller.

## Import

### BR-018 — Import does not apply a business write

**Implemented.** `saveimport.js` logs `event.body` and returns. The commented insert is not a rule in force.

The fixture under `mocks/saveimport.json` is a client/account spreadsheet. No rule in code maps those columns to a table.

## Audit

### BR-019 — MySQL calls are copied to DynamoDB

**Implemented** for routed MySQL handlers that call `log()`. The item stores the full event, the query text, and the driver result under table name `loging`.

The write is not part of the request's success check. A failed put does not change the HTTP result.

### BR-020 — Notes and logs share one configured table name

**Implemented** as configuration. `provider.environment.tableName` is `loging`. Both `create.js` and `logging.js` use `process.env.tableName`. The attribute names differ (`userId`/`noteId` versus `userID`/`logID`).

**Unknown / Requires Confirmation:** whether one DynamoDB table was supposed to hold both shapes. No table definition is in git, so no key schema is known.

## Errors and input

### BR-021 — Invalid JSON fails the invocation

**Implemented** where the handler calls `JSON.parse` with no try/catch (`global`, `getdetails`, `tableall`, `create`). A parse error rejects the Lambda. For handlers inside `handler-lib.js`, the client gets status 500 and `{ "error": <message> }`. `create.js` has no wrapper, so the Lambda error is whatever the runtime reports.

### BR-022 — Missing DynamoDB note is an error message, not a 404

**Implemented** in `get.js`. Message: `Item not found.` Status from the wrapper: 500.

### BR-023 — Declared failure status is not the HTTP status

**Implemented.** `failure()` sets 500 on an object that `handler()` then places in the body of a 200 response. See PRD WF-9.

### BR-024 — No field validation

**Implemented** by absence. No schema, required-field check, type check, or length check exists on any handler. Calendar dates are not validated before `DATE_FORMAT`.

## Unwired rules (not deployed)

These exist only in files `serverless.yml` does not publish. They are **not** current API behaviour.

### BR-025 — New `sysUser` rows are security group 1

**Implemented** in `adduser.js` only. `INSERT` sets `refSecurityGroupID` to the literal `1` and `strUser` from `pathParameters.name`. Not routed.

### BR-026 — A system user's display name follows their Cognito uuid

**Implemented** in `updateuser.js` only. `strUser` is set where `strCognitoUuid` equals `pathParameters.userUuid`. Not routed.

### BR-027 — Group permissions are five flags

**Implemented** in `savepermissions.js` and `updatepermissions.js` only. Columns: `PRIVATE`, `VIEW`, `EDIT`, `CREATE`, `DELETE` on `tbl_permissions_group`. Update also sets `ModifiedDateTime` to `NOW()` and `ModifiedUserID` from `tbl_member` where `CognitoUuid` equals `personId`. The save statement as written is not valid SQL (missing comma). Not routed.

**Inferred** from the column names and the 0/1 values in `mocks/updatepermissions.json`: each flag is on or off. The code does not check the range.

### BR-028 — A member can override a group's flags

**Implemented** in `updatecustompermissions.js` only. If `tbl_permissions_member` already has `PermissionGroupID = data.id` and `MemberID = data.memberId`, those five flags are updated. Otherwise an insert is attempted. The insert string is not valid SQL. Not routed.

There is no rule in code that says the member override wins over the group row at read time. No read of `tbl_permissions_member` is routed.

### BR-029 — A member's Cognito uuid can be set by member id

**Implemented** in `updateuuid.js` only. `tbl_member.CognitoUuid` is set from `data.uuid` where `MemberID = data.MemberID`. Not routed.

### BR-030 — Member profile fields

**Implemented** only as the select list in `getuserdetails_example.js`. The query reads `tbl_member` and the lookup tables named in that file (role, gender, ethnicity, organisation, status, country, city, identity type, relationship, sports code, position, suburb, province, social media, additional info, media hub, medical aid). It does not write them.

Derived values in that select:

- `PhysicallyDisabled` and `SpecialNeeds`: `1` becomes `Yes`, otherwise `No`.
- Age: year difference from `DateOfBirth` to the current date, via `Date_format(From_Days(...))`.
- Profile image URLs: bucket `footprint-reboot-qa-zaf`, key prefix `sportscode-1/members/{MemberID}/media_thumbs/{filename}`. Thumb is `tbl_media_hub` type `"1"`. The large image URL uses type `"2"`.
- Social rows match `Description` in `Facebook`, `Twitter`, `Instagram`.
- Extra info matches `FavouriteFood`, `FavouriteMusic`, `FavouriteFilm`, `MatricYear`, `StudentNumber`, `Play_Year`, `DiettryOther`.

The secondary position join uses `pripos.PositionID` on both sides, so `PlayerSecondaryPositionID` is not joined on `secpos`. **Implemented** as written. **Inferred** to be a mistake. Not a deployed rule.

### BR-031 — Roles for a member group

**Implemented** in `getrole.js` only. Returns `MemberRoleID` and `Description` from `tbl_member_role` where `MemberGroupID` equals the path id. Not routed.
