# Business rules — footprint-app-api

Evidence date: 2026-10-14.

Source: `git@bitbucket.org:overdrive/footprint-app-api.git`, `master` at `fb96734` (2020-06-25).

There is no business-rules document in the source repository. Rules below are what the code does. Each rule is **Inferred** from implementation unless a comment or the README states it. The code does not cite a policy, a union rulebook, or a signed requirement.

Mock identity values are omitted.

## Access

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

**Inferred** from `authorizer: aws_iam` on every HTTP event in `serverless.yml`.

The template does not declare a Cognito authorizer, an API key, or a usage plan.

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

**Inferred.** `create.js` comments say the user id is the Cognito Identity Pool identity id. `logging.js` stores that same field as `userID`. No Cognito user pool or identity pool is defined in this repository.

A caller can pass IAM and still send a different `personId` in the JSON body. Permission writes trust `personId` (BR-018). They do not compare it to the identity on the request.

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

**Inferred** from `create.js` and `get.js`. The DynamoDB key uses `cognitoIdentityId` as `userId` and the path id as `noteId`. A get for another identity's note id does not return that note, because the key includes the caller identity. A missing item throws `Item not found.`

### BR-004 — Member reads and writes are not limited to the caller

**Inferred.** Member id, email, and Cognito uuid come from the path or the body. No handler adds `CognitoUuid = <caller identity>` to a member query except `getuseruuid`, which uses the path id, not the request identity.

Any IAM caller who can invoke the route can read or update the member id they supply.

### BR-005 — Permission flags are stored, not enforced

**Inferred.** `PRIVATE`, `VIEW`, `EDIT`, `CREATE`, and `DELETE` are columns on permission rows. No member, lookup, or note handler reads those columns before it runs.

Whether the caller is supposed to enforce them is **Unknown / Requires Confirmation**.

### BR-006 — CORS is open

**Inferred.** Lambda responses set `Access-Control-Allow-Origin` to `*` and `Access-Control-Allow-Credentials` to true (`libs/handler-lib.js`, `libs/response-lib.js`, `create.js`). Gateway error responses allow any origin and any header (`resources/api-gateway-errors.yml`).

## Members

### BR-007 — A member is a row in `tbl_member`

**Inferred** from every member handler. The identifier used for updates is `MemberID`. There is no insert into `tbl_member` and no delete from `tbl_member` in this repository.

### BR-008 — Search text must be longer than one character

**Inferred** from `getfilteredusers.js`. `data.search.length > 1` adds a `LIKE` group on full name, email, role description, mobile number, and ethnicity description. A search of length 0 or 1 does not add that group.

`group`, `union`, and `role` apply only when the value is greater than 0. `union` filters `OrganisationID`. `group` filters `GroupID`. `role` filters `RoleID`.

If none of those four conditions hold, the query has no `WHERE` and `LIMIT 0,1000`. If any hold, there is no `LIMIT`.

### BR-009 — A full profile is one member id

**Inferred** from `getuser.js`. Path `id` is `MemberID`. Descriptions are joined rather than returning only foreign keys. See the PRD workflow for the join list.

Age is the year difference `To_Days(Curdate()) - To_Days(DateOfBirth)` formatted as `%Y`, then forced numeric. It is not a birthday-adjusted age. The same expression is used in `getuseruuid.js` (alias `Age`) and `getfilteredusers.js` (alias `age`).

### BR-010 — Disability flags display as Yes or No

**Inferred** from `getuser.js`. If `PhysicallyDisabled` or `SpecialNeeds` equals 1, the select returns `"Yes"`, otherwise `"No"`.

`updateusergeneral.js` writes `data.disabled` and `data.additionalNeeds` back into those columns as quoted strings. The code does not convert Yes/No to 1/0. Whether the column type accepts that is **Unknown / Requires Confirmation**.

### BR-011 — Social and extra profile values are named rows

**Inferred** from `getuser.js`.

| Alias | Source | Match |
|---|---|---|
| `Facebook`, `Twitter`, `Instagram` | `tbl_socialmedia.Value` | `Description` equals that network name, same `MemberID` |
| `FavouriteFood`, `FavouriteMusic`, `FavouriteFilm`, `MatricYear`, `StudentNumber` | `tbl_additional_member_info.Value` | `Description` equals that label |
| `PlayYear` | same table | `Description` equals `Play_Year` |
| `DietaryRequirements` | same table | `Description` equals `DiettryOther` |

No update route writes these tables.

### BR-012 — Profile images are URL strings

**Inferred** from `getuser.js`.

- Thumb: `tbl_media_hub` where `TypeID` is `"1"`.
- Large: `tbl_media_hub` where `TypeID` is `"2"`.
- Both URLs: `https://footprint-reboot-qa-zaf.s3-eu-west-1.amazonaws.com/sportscode-1/members/{MemberID}/media_thumbs/{Filename}`.

The S3 client that would have loaded the thumb is commented out. The API does not check that the object exists.

### BR-013 — Medical aid is read with the profile

**Inferred** from `getuser.js`. Left join `tbl_medical_aid` on `MemberID` returns `MedicalAidName`, `MedicalAidMainMember`, `MedicalAidNumber`, `MedicalNotes`. No handler updates that table.

### BR-014 — Email lookup uses a different column spelling

**Inferred** from `getbyemail.js`: `WHERE email = path id`, and `SELECT *`. Other handlers quote `Email`. This repository does not show the real column definition.

### BR-015 — Cognito uuid is both a lookup and a writable field

**Inferred.**

- `GET /footprint/useruuid/{id}` finds the member whose `CognitoUuid` equals the path id.
- `POST /footprint/updateuuid` sets `CognitoUuid` from `data.uuid` for `data.MemberID`.
- Permission modifications look up `MemberID` with `CognitoUuid = data.personId`.

Nothing in the code checks uniqueness of `CognitoUuid` before the update.

### BR-016 — Listing members returns at most 100 full rows

**Inferred** from `getallusers.js`: `SELECT * FROM tbl_member LIMIT 0,100`. No order is specified, so which 100 rows the engine returns is not fixed by this SQL.

### BR-017 — Profile updates replace a fixed column set

**Inferred** from the five section handlers. Each sets every column in its list from the body, including when a field is missing (the SQL then contains the text `undefined` inside quotes, because the value is concatenated). There is no "update only provided fields" branch.

Sections and columns are listed in the PRD (WF-8). General writes surname into `LastName` and `title` into `Title`. The profile read also returns role `Description` aliased as `Title`, and the member's own `Title` aliased as `PersonalTitle`. Those are different fields.

### BR-018 — The address update string is not valid SQL

**Inferred** from `updateuseraddress.js`. A comma follows the `MobileNumber` assignment and the next token is `WHERE`. This is what the file contains. It is not a separate intended rule.

### BR-019 — Status is an id, not a name

**Inferred** from `updatememberactive.js`: `StatusID = data.status` for `data.memberId`. The profile and uuid reads show `tbl_status.Description` as `Status`. No file lists which id means active, inactive, or deceased.

`DateDeceased` is writable on the identity update. The status update does not set `DateDeceased`.

### BR-020 — Email update writes `Email` by `MemberID`

**Inferred** from `updateemail.js`. Body fields are `Email` and `MemberID`. No format check.

## Permissions

### BR-021 — A group permission row has five flags

**Inferred** from `updatepermissions.js` and the reads of `tbl_permissions_group`. Flags are `PRIVATE`, `VIEW`, `EDIT`, `CREATE`, `DELETE`. The update sets all five, sets `ModifiedDateTime` to `NOW()`, and sets `ModifiedUserID` to the `MemberID` whose `CognitoUuid` equals `data.personId`. The row is chosen by `PermissionGroupID = data.id`.

Flag values are concatenated as numbers, not quoted. The code does not restrict them to 0 and 1. Mocks send 0 and 1.

### BR-022 — Modules and cards are strings on the group permission row

**Inferred.**

- `getpermissionmodules.js` returns distinct `ModulePermission` for a `GroupID`.
- `getpermissions.js` returns rows for `GroupID` plus `ModulePermission`.
- `getmoduledetails.js` returns rows for body `group` plus body `module` (same table, different JSON names).
- `getallcardpermissions.js` returns `PermissionGroupID` and `CardPermission`, grouped by `CardPermission` only. Under MySQL `ONLY_FULL_GROUP_BY`, that select is not valid. Whether the deployed server has that mode is **Unknown**.

`Member_Manage.txt` lists names that line up with profile sections (`Member_Profile_PersonalInformation_General`, `_Identity`, `_Other`, `Member_Profile_Contact_Address`, `Member_Profile_Contact_NextOfKin`) and other names with no route. **Inferred** that the file may be a module list. The code never loads it. **Requires confirmation** before treating those strings as `ModulePermission` values.

### BR-023 — Deleting a permission is a GET

**Inferred** from `serverless.yml` and `deletepermission.js`. `GET /footprint/deletepermission/{id}` deletes the `tbl_permissions_group` row whose `PermissionGroupID` is the path id. There is no HTTP DELETE route. There is no check that the row exists. The handler still returns the MySQL result through `success` when the driver returns a truthy object.

### BR-024 — A member override updates or inserts

**Inferred** from `updatecustompermissions.js`.

If `tbl_permissions_member` already has a row for `PermissionGroupID = data.id` and `MemberID = data.memberId`, the five flags and the modifier columns are updated the same way as BR-021.

If it does not, the file builds an insert. The values list puts a `ModifiedUserID` assignment where a value belongs, so the insert statement is not valid SQL. `MemberGroupID` is filled with `data.id`.

### BR-025 — The member-permission read uses different columns from the upsert

**Inferred** from `getuserpermissions.js` versus `updatecustompermissions.js`.

The read filters `GroupID`, `ModulePermission`, and `MemberID` (`data.personId`). The upsert filters `PermissionGroupID` and `MemberID` (`data.memberId`). The upsert's insert column list does not include `GroupID` or `ModulePermission`.

This repository does not prove that both queries can succeed against the same table.

### BR-026 — Unrouted copy assigns every member their group's rows

**Inferred** from `grouptomembercustom.js`, which is not a deployed function (the YAML block is commented out).

For each member with `GroupID > 0`, every `tbl_permissions_group` row for that group is inserted into `tbl_permissions_member`, including `ModulePermission`, `CardPermission`, `MemberPermission`, and `PermissionGroupID`. `CreatedUserID` is `0`. Existing rows are not detected. The function returns no payload.

### BR-027 — Unrouted save of a group permission

**Inferred** from `savepermissions.js`, which is not in `serverless.yml`. The intended columns are group id, module, card, five flags, creating user, and `CreatedDateTime`. The values string is not valid SQL (the delete flag is concatenated onto the text `id`, then a subquery).

## Lookups

### BR-028 — "Types" are organisation groups in a country

**Inferred** from `getalltypes.js`. Path id is `CountryID`. Columns returned are `GroupID` and `Description` from `tbl_organisation_group`.

### BR-029 — "Groups" on the deployed route are member groups

**Inferred** from `getallgroups.js`. All rows of `tbl_member_group`: `MemberGroupID`, `Description`. No country filter.

`getgroups.js` is not deployed. It reads `tbl_organisation_group` where `type_id` equals the path id. That is a different rule from BR-028 and from this one.

### BR-030 — Roles belong to a member group

**Inferred** from `getrole.js`. Path id is `MemberGroupID`. Columns are `MemberRoleID` and `Description`.

The profile read joins `tbl_member_role` on `MemberRoleID = tbl_member.RoleID`. The search filter uses `RoleID` as well. No handler updates `RoleID`.

## Audit

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

**Inferred** from `logging.js`. The item stores the whole API Gateway event, the query text, and the result. `getallusers.js` does not call `log`. Note create and note get do not call `log`.

The write is fire-and-forget. A failed audit put does not change the HTTP result.

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

**Inferred** from `serverless.yml` `tableName: footprint-log` and from the attribute names in `create.js` (`userId`, `noteId`) versus `logging.js` (`userID`, `logID`). One table accepting both shapes is not established by this repository.

## Errors and input

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

**Inferred.** Handlers that read a body call `JSON.parse(event.body)` before any SQL. A throw is HTTP 500 and `{"error":"<message>"}` via `handler-lib.js`. `create.js` uses a callback and will fail inside the Lambda if `JSON.parse` throws, rather than through that wrapper.

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

**Inferred** from `get.js` and `handler-lib.js`. Status 500, body `{"error":"Item not found."}`.

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

**Inferred** from `response-lib.js` plus `handler-lib.js`. `failure()` sets an inner `statusCode` of 500. The wrapper returns HTTP 200 and a JSON encoding of that inner response. Empty MySQL arrays are truthy, so they take the success path.

### BR-036 — No field validation

**Inferred.** No handler checks email format, date format, required fields, string length, or that a member id exists before an update. A MySQL error becomes the wrapper's HTTP 500 message, which will contain the driver message.

### BR-037 — SQL is the request text

**Inferred.** Path ids and JSON fields are concatenated into the statement. There is no placeholder binding in these files. That is the implemented data access, not a separate product rule about who may send which characters.

## Unwired starter behaviour

### BR-038 — `hello` is not a product rule

**Inferred.** `handler.js` returns a delayed "Go Serverless" message. It is not in `serverless.yml`. The unit test expects a callback that this function does not call.
