# PRD — 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 prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `serverless.yml`, the handler files, `libs/`, `logging.js`, `mocks/`, `README.md`, and the 22 commits on that revision. Provenance is labelled. Mock identity values are omitted.

`package.json` description and `LICENSE` are the upstream Serverless Node.js Starter. The current `README.md` is a short deploy note for this API. Where the starter description disagrees with the routes, the routes are the implemented behaviour.

## Product purpose

**Documented.** README title: "Evolv Foorprint Reboot API". Service name: `footprint-reboot-api`.

**Inferred.** An AWS API that lets an IAM-authenticated caller read and update Footprint members and permission flags in MySQL, and write an audit row to DynamoDB.

**Implemented.** Twenty-six HTTP functions in `serverless.yml`. The starter note create and note get are two of those functions. They share the DynamoDB table name `footprint-log` with the audit log.

**Unknown / Requires Confirmation:** the product owner, the calling application, and whether this API is still deployed.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| HTTP caller holding AWS IAM credentials | Every route sets `authorizer: aws_iam` | **Implemented**. The caller application is not in this repo |
| Cognito identity on the request | `create.js`, `get.js`, and `logging.js` read `event.requestContext.identity.cognitoIdentityId`. Comments in `create.js` say identities are federated through a Cognito Identity Pool | **Implemented** as a field read and as a comment. No Cognito resource is in the template. The YAML authorizer is `aws_iam`, not a Cognito authorizer |
| Person identified by `personId` in a JSON body | Permission updates resolve `ModifiedUserID` with `SELECT MemberID FROM tbl_member WHERE CognitoUuid = personId` | **Implemented**. The code does not check that `personId` equals the IAM/Cognito identity on the request |
| Member (`tbl_member`) | Read and updated by the member routes | **Implemented** as SQL. No screen is in this repo |
| Person running Serverless locally | README: `serverless invoke local --function updateemail --path mocks/updateemail.json`. Deploy: `serverless deploy -v --aws-profile evolvqa` | **Documented**. `mocks/updateemail.json` is not in the tree |

No role check and no permission-flag check is applied before a member read or write. The permission routes store and return flags. They do not gate the other routes.

## Systems the product talks to

1. AWS API Gateway and Lambda. Service `footprint-reboot-api`, runtime `nodejs12.x`, stage `prod`, region `eu-west-1`, memory 512 MB, profile `evolvqa`. Each function is packaged alone (`package.individually: true`).
2. A VPC. Security group `SECURITYGROUPIDS` and subnets `SUBNETIDS1` and `SUBNETIDS2` are required at deploy time. Unlike a commented VPC, this block is active in the template.
3. DynamoDB in `eu-west-1`. Table name is the literal `footprint-log`. IAM allows describe, query, scan, get, put, update, and delete on `arn:aws:dynamodb:eu-west-1:*:*`. The table itself is not created by this template.
4. MySQL through `serverless-mysql`. Host, user, password, and database come from `DBHOST`, `DBUSERNAME`, `DBPASSWORD`, `DBNAME` via `serverless-dotenv-plugin`. The plugin loads `.env`. `env.example` does not list those names.
5. S3 only as URL strings inside `getuser.js`. Bucket `footprint-reboot-qa-zaf`, region host `s3-eu-west-1`. The S3 client is commented out. Not a deployed integration.

No queue, mailer, cache, or monitoring vendor is configured. There is no Bitbucket Pipelines file.

## Major workflows

Routes below are the path in `serverless.yml`. Serverless Framework REST APIs also prefix the stage, so the deployed base path is `/prod` unless a custom domain exists. No custom domain is in the template. **Requires confirmation** of the public URL.

Every routed function sets `cors: true` and `authorizer: aws_iam`.

### WF-1 — Create a DynamoDB note

**Implemented** in `create.js`, `POST /footprint`.

1. Parse `event.body` as JSON. Fields used: `content`, `attachment`.
2. Put an item: `userId` = Cognito identity id, `noteId` = `uuid.v1()`, `content`, `attachment`, `createdAt` = `Date.now()`.
3. Table is `process.env.tableName`, which the template sets to `footprint-log`.
4. On DynamoDB error, callback status 500 and body `{"status":false}`. On success, status 200 and the item JSON.
5. This handler does not use `libs/handler-lib.js`. The response is a single HTTP response.
6. This handler does not call `logging.js`.

Origin: starter shape from `c0c86b4`, path `/footprint` in the current template.

### WF-2 — Read a DynamoDB note

**Implemented** in `get.js`, `GET /footprint/{id}`.

1. Get item where `userId` is the Cognito identity id and `noteId` is `event.pathParameters.id`.
2. If `result.Item` is missing, throw `Error("Item not found.")`. `handler-lib.js` turns that into status 500 and body `{"error":"Item not found."}`. It is not a 404.
3. Otherwise return the item. The wrapper sets status 200. This path is not double-wrapped, because `get.js` does not call `success()`.

The same method also has literal paths (`/footprint/users`, `/footprint/groups`, `/footprint/getallcardpermissions`). This repo has no test that shows whether API Gateway prefers those literals over `{id}`. **Requires confirmation** if `GET /footprint/users` was ever handled by `get.js`.

### WF-3 — Search members

**Implemented** in `getfilteredusers.js`, `POST /footprint/members`.

1. Parse JSON body. Mock `getfilteredusers.json` uses `union`, `group`, `role`, `search`.
2. Select member id, full name, email, role description as `Title`, mobile, gender description, an age expression aliased `age`, ethnicity description, organisation `TradingName` as `Organisation`.
3. Joins: `tbl_member_role`, `tbl_gender`, `tbl_ethnicity`, `tbl_organisation`.
4. If `search.length > 1` or `group > 0` or `union > 0` or `role > 0`, add `WHERE` and the matching clauses. Search is `LIKE` on full name, email, role description, mobile, and ethnicity. `group` filters `GroupID`. `union` filters `OrganisationID`. `role` filters `RoleID`.
5. If none of those conditions hold, append `LIMIT 0,1000` and no `WHERE`.
6. If any filter condition holds, the query has no `LIMIT`.
7. Call `log(...)`. If `results` is truthy, return `success(results)`, else `failure(results)`. See WF-16.

A missing `search` property throws when `.length` is read. The wrapper returns 500.

### WF-4 — Read one member profile

**Implemented** in `getuser.js`, `GET /footprint/user/{id}`.

1. Path `id` is compared to `tbl_member.MemberID`.
2. The select lists identity, contact, sport, next of kin, social, school, medical, and status fields. Lookup descriptions come from left joins (role, gender, ethnicity, organisation, status, countries, city, identity type, relationship, sports code, positions, suburb, province).
3. Social rows: `tbl_socialmedia` where `Description` is `Facebook`, `Twitter`, or `Instagram`.
4. Extra rows: `tbl_additional_member_info` where `Description` is `FavouriteFood`, `FavouriteMusic`, `FavouriteFilm`, `MatricYear`, `StudentNumber`, `Play_Year`, or `DiettryOther` (spelled that way in the SQL).
5. Images: `tbl_media_hub` `TypeID` `"1"` as thumb filename and `"2"` as the large filename. Both URLs use the prefix `sportscode-1/members/{MemberID}/media_thumbs/` on `https://footprint-reboot-qa-zaf.s3-eu-west-1.amazonaws.com/`.
6. `PhysicallyDisabled` and `SpecialNeeds` are returned as `"Yes"` when the column equals 1, otherwise `"No"`.
7. Age is `Date_format(From_Days(To_Days(Curdate()) - To_Days(DateOfBirth)), "%Y") + 0`, aliased `Age`.
8. The secondary position join is written as `LEFT JOIN tbl_position secpos ON (pripos.PositionID = m.PlayerSecondaryPositionID)`. The `ON` clause uses `pripos`, not `secpos`.
9. An S3 `getObject` block is commented out. `console.log(results)` remains.
10. Call `log(...)`, then `success` or `failure` as in WF-16.

### WF-5 — Read a member by Cognito uuid

**Implemented** in `getuseruuid.js`, `GET /footprint/useruuid/{id}`.

Selects a short profile: member id, full name, email, role description as `Title`, mobile, gender, `Age` (same date expression as WF-4), ethnicity, organisation trading name, status description, `GroupID`, and `CognitoUuid`. Filter: `CognitoUuid = path id`. The uuid column is not table-qualified.

### WF-6 — Read a member by email

**Implemented** in `getbyemail.js`, `GET /footprint/email/{id}`.

`SELECT * FROM tbl_member WHERE email = path id`. The column is written `email`. Other handlers use `Email`. Whether the server treats those as the same column is **Unknown / Requires Confirmation**.

### WF-7 — List members

**Implemented** in `getallusers.js`, `GET /footprint/users`.

`SELECT * FROM tbl_member LIMIT 0,100`. Does not call `logging.js`. Still returns `success` or `failure` inside the wrapper (WF-16).

### WF-8 — Update a profile section

**Implemented** as five POST routes. Each parses JSON, updates `tbl_member` where `MemberID = data.id`, calls `log(...)`, and returns `success` or `failure`.

| Route | File | Columns written | Body fields |
|---|---|---|---|
| `POST /footprint/updateusergeneral` | `updateusergeneral.js` | `PreferredName`, `FullName`, `LastName`, `Title`, `CountryOfNationalityID`, `CountryOfBirthID`, `CityOfBirthID`, `EthnicityID`, `GenderID`, `PhysicallyDisabled`, `SpecialNeeds`, `Height`, `Weight`, `SportSafetyNumber` | `preferredName`, `fullName`, `surname`, `title`, `countryOfNationality`, `countryOfBirth`, `cityOfBirth`, `ethnicity`, `gender`, `disabled`, `additionalNeeds`, `height`, `weight`, `sportSafetyNumber`, `id` |
| `POST /footprint/updateuseridentity` | `updateuseridentity.js` | `IdentityCardTypeID`, `IdentityCardNumber`, `IdentityCardCountryID`, `IdentityCardIssueDate`, `IdentityCardExpiryDate`, `DateOfBirth`, `DateDeceased`, `PassportCountryID`, `PassportNumber`, `ResidencyStatusID`, `PassportIssueDate`, `PassportExpiryDate` | `idCardType`, `idCardNumber`, `countryOfIssue`, `issueDate`, `expiryDate`, `dateOfBirth`, `dateOfDeath`, `passportCountryOfIssue`, `passportNumber`, `residencyStatus`, `passportIssueDate`, `passportExpiryDate`, `id` |
| `POST /footprint/updateuserother` | `updateuserother.js` | `PrimarySchool`, `SecondarySchool`, `TertiaryInstitution`, `Club`, `NationalPlayerNo`, `RelationshipID`, `SportsCodeID`, `PlayerPrimaryPositionID`, `PlayerSecondaryPositionID` | `primarySchoolName`, `secondarySchoolName`, `tertiaryInstitutionName`, `clubName`, `nationalPlayerNumber`, `relationshipStatus`, `sportsCodes`, `primaryPlayingPosition`, `secondaryPlayingPosition`, `id` |
| `POST /footprint/updateuseraddress` | `updateuseraddress.js` | `CountryID`, `Address`, `SuburbID`, `CityID`, `ProvinceID`, `PostalCode`, `MobileNumber` | `country`, `address`, `suburb`, `city`, `province`, `postalCode`, `mobileNumber`, `id` |
| `POST /footprint/updateusernextofkin` | `updateusernextofkin.js` | `NextOfKinInitials`, `NextOfKinFirstname`, `NextOfKinLastname`, `NextOfKinEmail`, `NextOfKinMobileNumber`, `NextOfKinRelationshipID` | `initials`, `firstName`, `lastName`, `emailAddress`, `mobileNumber`, `relationship`, `id` |

`updateuseraddress.js` places a comma after the `MobileNumber` assignment and then `WHERE`. That string is not valid SQL as written.

None of these handlers write `tbl_socialmedia`, `tbl_additional_member_info`, `tbl_media_hub`, or `tbl_medical_aid`, even though `getuser.js` reads them.

No mock file exists for these five routes.

### WF-9 — Update email, Cognito uuid, or status

**Implemented.**

| Route | File | SQL |
|---|---|---|
| `POST /footprint/updateemail` | `updateemail.js` | `UPDATE tbl_member SET Email = data.Email WHERE MemberID = data.MemberID` |
| `POST /footprint/updateuuid` | `updateuuid.js` | `UPDATE tbl_member SET CognitoUuid = data.uuid WHERE MemberID = data.MemberID` |
| `POST /footprint/updatememberactive` | `updatememberactive.js` | `UPDATE tbl_member SET StatusID = data.status WHERE MemberID = data.memberId` |

`updatememberactive` is the last commit (`fb96734`, "Update the active member"). The body field is `status`, stored in `StatusID`. The handler does not interpret "active" as a particular status id. Mock `updateuuid.json` supplies `MemberID` and `uuid`. There is no `updateemail` mock in git, despite the README command.

### WF-10 — Group permission flags

**Implemented.**

- `GET /footprint/getpermissionmodules/{id}` (`getpermissionmodules.js`): distinct `ModulePermission` from `tbl_permissions_group` where `GroupID` is the path id.
- `POST /footprint/permissions` (`getpermissions.js`): `SELECT *` from `tbl_permissions_group` where `GroupID = data.GroupID` and `ModulePermission = data.ModulePermission`. Mock body keys: `GroupID`, `ModulePermission`.
- `POST /footprint/getmoduledetails` (`getmoduledetails.js`): `SELECT *` from the same table where `GroupID = data.group` and `ModulePermission = data.module`. Mock body keys: `group`, `module`.
- `POST /footprint/updatepermissions` (`updatepermissions.js`): set `PRIVATE`, `VIEW`, `EDIT`, `CREATE`, `DELETE`, `ModifiedDateTime = NOW()`, and `ModifiedUserID` from the member whose `CognitoUuid` is `data.personId`, where `PermissionGroupID = data.id`. Mock body keys: those five flags, `id`, `personId`.
- `GET /footprint/getallcardpermissions` (`getallcardpermissions.js`): `PermissionGroupID` and `CardPermission` from `tbl_permissions_group`, grouped by `CardPermission` only.
- `GET /footprint/deletepermission/{id}` (`deletepermission.js`): `DELETE FROM tbl_permissions_group WHERE PermissionGroupID = path id`. The HTTP method is GET.

### WF-11 — Member permission flags

**Implemented** in `updatecustompermissions.js`, `POST /footprint/updatecustompermissions`, and `getuserpermissions.js`, `POST /footprint/userpermissions`.

Update:

1. `SELECT * FROM tbl_permissions_member WHERE PermissionGroupID = data.id AND MemberID = data.memberId`.
2. If at least one row, `UPDATE` the five flags, `ModifiedDateTime`, and `ModifiedUserID` (member id looked up by `CognitoUuid = data.personId`) for that permission-group id and member id.
3. If no row, build an `INSERT` into `tbl_permissions_member` listing `PRIVATE`, `VIEW`, `EDIT`, `CREATE`, `DELETE`, `CreatedDateTime`, `MemberGroupID`, `CreatedUserID`, `MemberID`. The `VALUES` clause places `` `ModifiedUserID`=(SELECT ...) `` where a value should be. That insert string is not valid SQL as written. `MemberGroupID` is set from `data.id` (the same value used as `PermissionGroupID` in the select).
4. Mock body keys: the five flags, `id`, `personId`, `memberId`.

Read (`getuserpermissions.js`): `SELECT * FROM tbl_permissions_member WHERE GroupID = data.GroupID AND ModulePermission = data.ModulePermission AND MemberID = data.personId`. Mock body keys: `GroupID`, `ModulePermission`, `personId`.

The read filters `GroupID` and `ModulePermission`. The upsert filters and writes `PermissionGroupID` and does not set `GroupID` or `ModulePermission`. **Requires confirmation** whether those columns exist on `tbl_permissions_member` and whether the read and the write were meant to use the same key.

### WF-12 — Lookup lists

**Implemented.**

| Route | File | Query |
|---|---|---|
| `GET /footprint/types/{id}` | `getalltypes.js` | `GroupID`, `Description` from `tbl_organisation_group` where `CountryID` is the path id |
| `GET /footprint/groups` | `getallgroups.js` | `MemberGroupID`, `Description` from `tbl_member_group` (no filter) |
| `GET /footprint/roles/{id}` | `getrole.js` | `MemberRoleID`, `Description` from `tbl_member_role` where `MemberGroupID` is the path id |

`getalltypes` is named "types" and selects organisation groups by country. `getallgroups` selects member groups, not organisation groups.

### WF-13 — Copy group permissions onto members

**Implemented** in `grouptomembercustom.js` only. The function block in `serverless.yml` is commented out, so it is not a route.

The function selects every `MemberID` and `GroupID` from `tbl_member`. For each group id greater than 0 it selects `tbl_permissions_group` for that group and inserts a `tbl_permissions_member` row copying the five flags plus `PermissionGroupID`, `ModulePermission`, `CardPermission`, and `MemberPermission`. `CreatedUserID` is the literal `0`. It does not check for an existing row. It does not return a body. `success` and `failure` are imported in a comment only.

### WF-14 — Insert a group permission

**Implemented** in `savepermissions.js` only. Not declared in `serverless.yml`.

The insert lists `GroupID`, `ModulePermission`, `CardPermission`, the five flags, `CreatedUserID`, `CreatedDateTime`. The values string concatenates `data.DELETE` directly onto the text `id` and then a member-id subquery. That string is not valid SQL as written.

### WF-15 — Audit log

**Implemented** in `logging.js`. Called at the end of every MySQL handler except `getallusers.js`. Not called by `create.js` or `get.js`.

Puts an item on `process.env.tableName` (`footprint-log`):

- `userID`: Cognito identity id (capital `ID`)
- `logID`: `uuid.v1()`
- `event`: `JSON.stringify(event)`
- `query`: `JSON.stringify(query)`
- `result`: `JSON.stringify(result)`
- `createdAt`: `Date.now()`

The put callback ignores errors. The HTTP response does not wait on a successful put. Note items in WF-1 use `userId` and `noteId` on the same table name. Whether one DynamoDB key schema accepts both shapes is **Unknown / Requires Confirmation**.

### WF-16 — Response wrapping on MySQL handlers

**Implemented.** `libs/handler-lib.js` always answers HTTP 200 with `JSON.stringify` of whatever the function returns, unless the function throws, in which case HTTP 500 and `{"error":"<message>"}`.

MySQL handlers return `success(results)` or `failure(results)` from `libs/response-lib.js`. Those helpers already build `{ statusCode, headers, body }` with `body` a JSON string. The wrapper then stringifies that object and forces status 200.

So a MySQL call that returns `failure(...)` is still HTTP 200. The outer body is a JSON object whose `statusCode` is 500 and whose `body` is a string. A thrown error (invalid JSON, missing property, MySQL exception) is HTTP 500 from the wrapper.

An empty array is truthy in JavaScript, so an empty result uses `success`, not `failure`.

`get.js` returns the item itself, so it is wrapped once. `create.js` uses a callback and is not wrapped.

### WF-17 — Starter hello

**Implemented** in `handler.js` as `hello`. Not a function in `serverless.yml`. `tests/handler.test.js` calls it. The function returns a promise and does not invoke the callback the test passes. The expectations sit inside that callback.

## Functional requirements recovered from the implementation

### FR-001 — IAM on every route

**Implemented.** Every `http` event sets `authorizer: aws_iam`.

Evidence: `serverless.yml`. Confidence: high.

### FR-002 — Stage, region, runtime, and VPC

**Implemented.** Stage `prod`, region `eu-west-1`, runtime `nodejs12.x`, memory 512, AWS profile `evolvqa`, VPC from three environment names.

Evidence: `serverless.yml`, README deploy line. Confidence: high for the template. Whether that profile still deploys is unknown.

### FR-003 — Note create and get are per Cognito identity

**Implemented.** `create.js` and `get.js` use `cognitoIdentityId` as `userId` on table `footprint-log`.

Evidence: `create.js`, `get.js`, `serverless.yml` `tableName`. Confidence: high.

### FR-004 — Member search

**Implemented.** `POST /footprint/members` filters by search text, group, organisation (`union`), and role, and caps the unfiltered query at 1000 rows.

Evidence: `getfilteredusers.js`, `mocks/getfilteredusers.json`. Confidence: high.

### FR-005 — Member profile by id

**Implemented.** `GET /footprint/user/{id}` returns the joined profile described in WF-4.

Evidence: `getuser.js`. Confidence: high.

### FR-006 — Member by Cognito uuid or email, and a short list

**Implemented.** `GET /footprint/useruuid/{id}`, `GET /footprint/email/{id}`, `GET /footprint/users` (100 rows, no audit log).

Evidence: `getuseruuid.js`, `getbyemail.js`, `getallusers.js`. Confidence: high.

### FR-007 — Profile section updates

**Implemented** as the five POST routes in WF-8. Address SQL includes a trailing comma before `WHERE`.

Evidence: the five `updateuser*.js` files and their `serverless.yml` entries. Confidence: high.

### FR-008 — Email, uuid, and status writes

**Implemented** as WF-9. Status is whatever numeric `data.status` the body sends. No allow-list is in the code.

Evidence: `updateemail.js`, `updateuuid.js`, `updatememberactive.js`. Confidence: high.

### FR-009 — Permission flags are five columns

**Implemented.** `PRIVATE`, `VIEW`, `EDIT`, `CREATE`, `DELETE` on `tbl_permissions_group` and `tbl_permissions_member`. Group rows can be updated and deleted. Member rows are updated or inserted. The insert text in `updatecustompermissions.js` is not valid SQL.

Evidence: `updatepermissions.js`, `updatecustompermissions.js`, `deletepermission.js`. Confidence: high.

### FR-010 — Permission and lookup reads

**Implemented** as WF-10 reads and WF-12.

Evidence: the get* permission files, `getalltypes.js`, `getallgroups.js`, `getrole.js`. Confidence: high.

### FR-011 — MySQL handlers audit to DynamoDB

**Implemented** for every MySQL handler except `getallusers.js`. `create.js` and `get.js` do not audit.

Evidence: `logging.js` and imports in the handlers. Confidence: high.

### FR-012 — MySQL HTTP status is 200 unless the function throws

**Implemented.** See WF-16.

Evidence: `libs/handler-lib.js`, `libs/response-lib.js`. Confidence: high.

### FR-013 — Gateway errors allow any origin

**Implemented.** `resources/api-gateway-errors.yml` sets `Access-Control-Allow-Origin` and `Access-Control-Allow-Headers` to `*` for `DEFAULT_4XX` and `DEFAULT_5XX`.

Evidence: that file, included from `serverless.yml`. Confidence: high.

### FR-014 — Environment names for MySQL and VPC

**Implemented** as template interpolation. Not listed in `env.example`.

Evidence: `serverless.yml`, `env.example`. Confidence: high.

### FR-015 — Permission flags are not an access check

**Implemented** absence. No handler reads the flag columns and then allows or denies the member routes.

Evidence: member handlers contain no reference to `tbl_permissions_group` or `tbl_permissions_member`. Confidence: high for the code. Whether a caller enforces flags before calling is **Unknown**.

## Constraints

- Runtime `nodejs12.x` is what the template deploys. It is not a statement that AWS still accepts that runtime.
- SQL is built by concatenating path parameters and JSON fields. There is no parameter binding and no schema validation.
- CORS on the Lambda responses allows origin `*` with credentials.
- DynamoDB IAM is the whole region (`eu-west-1:*:*`), not one table ARN.
- Stage is fixed to `prod`. No other stage is declared.
- The API does not create members. It updates existing `MemberID` values and reads rows.
- `Member_Manage.txt` lists module-like names. No handler reads that file.

## Discrepancies

| Topic | What the repo shows |
|---|---|
| README vs package | README says Evolv Footprint Reboot (spelled "Foorprint"). `package.json` still describes the Serverless starter and points `repository.url` at the Anomaly Innovations GitHub project |
| README test command | `serverless invoke local --function updateemail --path mocks/updateemail.json`. That mock file is not in `mocks/` |
| Note table vs audit table | Both use `footprint-log`. Notes use `userId` / `noteId`. Logs use `userID` / `logID` |
| `GET /footprint/{id}` vs literal GETs | Both are declared. No test shows which function API Gateway invokes |
| `getuser` secondary position | Join alias `secpos` is not used in the `ON` clause; `pripos.PositionID` is |
| Address update | Trailing comma before `WHERE` in `updateuseraddress.js` |
| Custom permission insert | `VALUES` contains `` `ModifiedUserID`=(SELECT ...) `` |
| `savepermissions.js` | Not routed, and the `VALUES` clause is not valid SQL |
| Member permission read vs write | Read filters `GroupID` and `ModulePermission`. Upsert uses `PermissionGroupID` and does not write those two columns |
| `getbyemail` column | SQL says `email`. Other SQL says `Email` |
| `getalltypes` name | Path and file say types. SQL reads `tbl_organisation_group` by `CountryID` |
| `getallgroups` vs `getgroups.js` | Routed `getallgroups` reads `tbl_member_group`. Unrouted `getgroups.js` reads `tbl_organisation_group` by `type_id` |
| Delete method | `deletepermission` is HTTP GET |
| `failure()` status | Declared 500 inside a body that the wrapper returns as HTTP 200 |
| Diet label | SQL matches `Description = "DiettryOther"` |
| Play year label | SQL matches `"Play_Year"` and aliases the column `PlayYear` |
| Image path | Large image uses media type `"2"` but the URL path still contains `media_thumbs` |
| `grouptomembercustom` | File exists. Template function is commented out |
| Test | Asserts on a callback `hello` never calls |
| `libs/images-lib.js` | Empty. Not imported |
| `Untitled-2.txt` | Two product JSON objects (barcode, product code, price). No handler reads the file |
| `Member_Manage.txt` | Sixteen names, including the five profile sections that have update routes, plus reports, career, organisation, contract, and apparel. No handler reads the file |

## What requires human confirmation

1. Is `footprint-reboot-api` on profile `evolvqa`, stage `prod`, region `eu-west-1`, still the live API, and what is its public URL?
2. Which application is the IAM caller, and does that application enforce the permission flags this API only stores?
3. What is the key schema of DynamoDB table `footprint-log`, and were note items and audit items both written to it?
4. Are `tbl_permissions_member.GroupID` and `ModulePermission` real columns, and was the member-permission read supposed to match the upsert?
5. Were the address-update comma and the custom-permission insert ever executed successfully?
6. Is the email column `email` or `Email` on the deployed database?
7. What do `StatusID` values mean, and which value is "active"?
8. Are the names in `Member_Manage.txt` the `ModulePermission` values the permission routes expect?
9. How does this repository relate to `FootprintApp.net`, `FootPrintLive`, `unity-lambda-api`, and the other Footprint directories in Silicon Ark?
10. Should `GET /footprint/{id}` remain deployed beside the literal GET routes?
