# Technical documentation — ohlief-app-api

Evidence date: 2026-10-08. Source: `git@bitbucket.org:overdrive/ohlief-app-api.git`, `master` `08d6e03` (2020-07-31, Jaco Brits).

No passwords from the environment are stored in git. One handler assigns a static AWS access key id and secret in source. Those characters are not copied into this document. See Security.

## What this repository is

A Serverless Framework service named `ohLief-app-api`, version `1.1.1` in `package.json`. Handlers are Node JavaScript using `async/await` and the AWS SDK v2 DocumentClient. The EDI behaviour is FTP in, DynamoDB, FTP out, and SES. The packaging description and `repository.url` still point at the Anomaly Innovations starter.

`README.md` is the title `Oh Lief Nodejs Lambda API` plus an unclosed `**Functions` line. It does not list routes.

## Repository facts

| Item | Value |
|---|---|
| Remote | `git@bitbucket.org:overdrive/ohlief-app-api.git` |
| Default branch | `master` |
| HEAD | `08d6e03` (2020-07-31) |
| First commit | `74115e6` (2020-02-15, "Api commit") |
| Commit count | 13 |
| Authors | Jaco Brits only |
| Other branches | none on this clone after a full history fetch |
| Licence file | MIT, copyright 2017 Anomaly Innovations |

The onboarding scan listed `sonar-project.properties`. It is not in the git tree at HEAD. The scan-host path `~/projects/sonar_scan/repos/ohlief-app-api` was not on the machine that produced this document.

## Application structure

```text
serverless.yml                 service, functions, provider, resource includes
resources/api-gateway-errors.yml
resources/dynamodb-table.yml
resources/s3-bucket.yml
resources/cognito-user-pool.yml
resources/cognito-identity-pool.yml
create.js                      POST /ohlief
get.js                         GET /ohlief/{id}
list.js                        GET /ohlief
update.js                      PUT /ohlief/{id}
delete.js                      DELETE /ohlief/{id}
listOrders.js                  GET /listOrders
listinvoices.js                GET /listinvoices
invoiceupdate.js               GET /invoiceupdate/{id}
invoicetoftp.js                GET /invoicetoftp/{id}
invoiceToFtp_old.js            not a deployed function
ftptos3.js                     GET /ftptos3
filetoDDb.js                   not a deployed function
createProduct.js               POST /createProduct
getproduct.js                  GET /getproduct/{id}
listProducts.js                GET /listProducts
updateProduct.js               PUT /updateProduct/{id}
billing.js                     POST /billing
handler.js                     hello, not deployed
libs/dynamodb-lib.js           DocumentClient.call(action, params)
libs/response-lib.js           HTTP 200 / 500
libs/billing-lib.js            calculateCost
tests/handler.test.js          hello only
mocks/                         sample API Gateway events
package.json                   dependencies
```

There is no `Dockerfile`, no `bitbucket-pipelines.yml`, no `app.js` server, and no infrastructure other than `serverless.yml`.

## Frameworks and runtime

| Piece | Value |
|---|---|
| Language | JavaScript, no TypeScript |
| Lambda runtime | `nodejs10.x` (`provider.runtime`) |
| Stage | `prod` unless overridden. `custom.stage` is `${opt:stage, self:provider.stage}` |
| Region | `eu-west-1` |
| AWS CLI profile | `ohlief` |
| Packaging | `package.individually: true`, plugin `serverless-bundle` `^1.2.5` (Webpack) |
| Local HTTP | `serverless-offline` `^5.3.3` |
| Env loading | `serverless-dotenv-plugin` `^2.1.1` |
| AWS SDK | `aws-sdk` `^2.639.0` (devDependency) |
| FTP | `ftp` `^0.3.10` |
| Stripe | `stripe` `^8.18.0` |
| Ids | `uuid` `^3.4.0` (`uuid.v1`) |
| Declared, not imported | `unzipper` `^0.10.10`, `xlsx` `^0.15.5` |

The template does not set `memorySize` or `timeout`. A comment inside `ftptos3` says the defaults are 1024 MB and 6 seconds. Those lines are comments. The live settings are **Unknown**.

Whether AWS still accepts `nodejs10.x` for a new deploy is **Unknown / Requires Confirmation**. This inspection did not deploy.

## Routes

CORS is `true` on every event. Authorizer is `aws_iam` except where noted.

| Function | Method and path | Handler | Authorizer |
|---|---|---|---|
| `create` | `POST /ohlief` | `create.main` | `aws_iam` |
| `get` | `GET /ohlief/{id}` | `get.main` | `aws_iam` |
| `list` | `GET /ohlief` | `list.main` | `aws_iam` |
| `update` | `PUT /ohlief/{id}` | `update.main` | `aws_iam` |
| `delete` | `DELETE /ohlief/{id}` | `delete.main` | `aws_iam` |
| `listOrders` | `GET /listOrders` | `listOrders.main` | `aws_iam` |
| `listinvoices` | `GET /listinvoices` | `listinvoices.main` | `aws_iam` |
| `invoiceupdate` | `GET /invoiceupdate/{id}` | `invoiceupdate.main` | `aws_iam` |
| `invoicetoftp` | `GET /invoicetoftp/{id}` | `invoicetoftp.main` | commented out |
| `ftptos3` | `GET /ftptos3` | `ftptos3.main` | commented out |
| `createProduct` | `POST /createProduct` | `createProduct.main` | `aws_iam` |
| `getproduct` | `GET /getproduct/{id}` | `getproduct.main` | `aws_iam` |
| `listProducts` | `GET /listProducts` | `listProducts.main` | `aws_iam` |
| `updateProduct` | `PUT /updateProduct/{id}` | `updateProduct.main` | `aws_iam` |
| `billing` | `POST /billing` | `billing.main` | `aws_iam` |

`schedule: rate(1 minute)` is commented out on `ftptos3` and `invoicetoftp`.

The client (`silicon_ark/ohlief-app-client`) calls this host, which is not named in this repo:

```text
https://0tsz4u0k70.execute-api.eu-west-1.amazonaws.com/prod
```

Stage `prod` and region `eu-west-1` match the template. The API id is **Unknown / Requires Confirmation** as this stack.

## Auth

HTTP authorizer, where set, is `aws_iam`. There is no Cognito authorizer on the methods, and no handler reads `requestContext.authorizer`.

`create.js` and `delete.js` read `event.requestContext.identity.cognitoIdentityId`. The other handlers do not.

CloudFormation, if deployed, creates:

| Resource | Settings in the template |
|---|---|
| User pool | Name `${stage}-user-pool` (`prod-user-pool` at the default stage). Username attribute `email`. `AutoVerifiedAttributes: email` |
| User pool client | Name `${stage}-user-pool-client`. `GenerateSecret: false`. `ExplicitAuthFlows: ADMIN_NO_SRP_AUTH` |
| Identity pool | Name `${stage}IdentityPool`. `AllowUnauthenticatedIdentities: false`. Linked to that user pool client |
| Auth role | `sts:AssumeRoleWithWebIdentity` for authenticated identities of that pool |

The auth role allows:

- `mobileanalytics:PutEvents`, `cognito-sync:*`, `cognito-identity:*` on `*`
- `execute-api:Invoke` on this API's `/*`
- `s3:*` on the template bucket's `/private/${cognito-identity.amazonaws.com:sub}/*`

The client is pinned to user pool `eu-west-1_rXTL9gyYP`, app client `5r1hmv1885cv8tc1hdnrr46388`, identity pool `eu-west-1:aeacda5a-adf0-4561-8696-7cede4d5d6f7`. Those ids are not in this repository. Whether this template created them is **Unknown / Requires Confirmation**.

The Lambda role itself (`provider.iamRoleStatements`) allows DynamoDB `DescribeTable`, `Query`, `Scan`, `GetItem`, `PutItem`, `UpdateItem`, `DeleteItem` on `arn:aws:dynamodb:eu-west-1:*:*`. That ARN is every table in the region, in every account. The role statement does not grant S3 or SES. The FTP handlers pass their own credentials into the SDK instead.

## Data

One table shape is declared.

| Key | Type | Role |
|---|---|---|
| `userId` | String | Hash |
| `transactionId` | String | Range |

Billing mode `PAY_PER_REQUEST`. No GSIs. No TTL. No streams.

Table name on the resource: `${self:custom.tableName}`, and `custom.tableName` is `${self:custom.stage}-ohlief`. At stage `prod` that is `prod-ohlief`.

Table name in the function environment:

```yaml
environment:
  tableName: order
  stripeSecretKey: ${env:STRIPE_SECRET_KEY}
```

Handlers call `process.env.tableName`. They do not read `custom.tableName`. A stack deployed as written creates `prod-ohlief` and then reads and writes `order`.

### Partitions the code uses

| `userId` | Who writes it | What else is set |
|---|---|---|
| `OrderRow` | `ftptos3.js` | `type` `order`, `process` `"0"`, file and product fields (business rule BR-007) |
| `OrderRow` | `invoiceupdate.js` | sets `type` to `invoice` only |
| `OrderRow` | `invoicetoftp.js` | sets `process` to `"1"` |
| `OrderRow` | `update.js` | sets `content`, `attachment`, `invoiceQty` |
| `ProductRow` | `createProduct.js` | `type` `product` and the product fields |
| `ProductRow` | `updateProduct.js` | the nine product fields |
| Cognito identity id | `create.js` | `content`, `attachment`, no `type` |
| Cognito identity id | `delete.js` | removes the item |

`noteId` is a second UUID on create paths. Gets and updates address `transactionId`, not `noteId`.

Scans filter `type` or `orderNumber` or `packageBarcode` with no index. Queries use the hash key only (`listOrders`). No handler paginates.

`libs/dynamodb-lib.js` constructs a new DocumentClient per call and returns `.promise()`. `invoiceupdate.js` and `ftptos3.js` also construct their own `AWS.DynamoDB.DocumentClient` and use callbacks. `invoicetoftp.js` calls `dynamoDbLib.call` without `await`.

### Invoice arithmetic

Implemented in `invoicetoftp.js`, and only there. Factor `0.21`. `parseFloat(x).toFixed(3)` returns a string; the later `*` coerces it. Details and the output column order are business rules BR-010 and BR-014.

`content`, `orderNum`, `totalexclVat`, `invoicedVat`, and `totalVatIncl` are module-level `var`s. `main` does not reset them. A reused Lambda process would append to the previous buffer and totals. **Inferred** from the scope of those variables. Not executed.

## External systems

### Order FTP

`ftptos3.js` connects with `FTP_HOST_ORDER`, `FTP_USERNAME_ORDER`, `FTP_PASSWORD_ORDER`. It lists the login directory (no remote path is set), reads each file, writes `processed/{name}`, and deletes the original name. The `processed` mkdir is commented out.

### Invoice FTP

`invoicetoftp.js` `WritetoFTP` connects with `FTP_HOST_INVOICE`, `FTP_USERNAME_INVOICE`, `FTP_PASSWORD_INVOICE` and `put`s the buffer as `I{orderNumber}.csv` in the login directory.

### S3

Import uploads to bucket `ohlief-app-uploads`, key = FTP file name, body = `JSON.stringify(content, null, 2)`. The client config names the same bucket for Amplify `Storage.vault`.

The template bucket `AttachmentsBucket` sets CORS (`*` origins, GET/PUT/POST/DELETE/HEAD, `MaxAge` 3000) and does not set `BucketName`. CloudFormation would assign a name. The output `AttachmentsBucketName` exports that name. Nothing in the handlers reads the output. **Unknown / Requires Confirmation** whether `ohlief-app-uploads` is that bucket.

`invoiceToFtp_old.js` mentions `ohlief-transaction-files` only inside a comment.

### SES

API version `2010-12-01`. Configuration set `info`. Source `accounts@ohlief.com`. Recipients and subjects are business rule BR-019. `invoicetoftp.js` calls `AWS.config.update` with a region of `eu-west-1` before creating the SES client. `ftptos3.js` does the same with environment keys.

### Stripe

`stripe(process.env.stripeSecretKey)` then `charges.create`. Currency `usd`. Used only by `billing.js`.

## Environment

| Name | Where it is read | Set in `serverless.yml` |
|---|---|---|
| `tableName` | Every DynamoDB handler | Literal `order` |
| `stripeSecretKey` | `billing.js` via `STRIPE_SECRET_KEY` | `${env:STRIPE_SECRET_KEY}` |
| `FTP_HOST_ORDER`, `FTP_USERNAME_ORDER`, `FTP_PASSWORD_ORDER` | `ftptos3.js` | Not in the template. Expected from `.env` through the dotenv plugin, or from the shell |
| `FTP_HOST_INVOICE`, `FTP_USERNAME_INVOICE`, `FTP_PASSWORD_INVOICE` | `invoicetoftp.js` | Same |
| `AWS_ACCESS_KEY_VALUE`, `AWS_SECRET_ACCESS_KEY_VALUE` | `ftptos3.js`, and `invoiceToFtp_old.js` | Same |
| `STRIPE_SECRET_KEY` | Mapped to `stripeSecretKey` | From the environment at deploy time |

`.gitignore` ignores `.env` and `env.yml`. No `.env` is committed. `env.example` was removed in `7e071a9`. Its only variable was `SAMPLE_ENV_VAR`.

## Response shape

`libs/response-lib.js`:

| Helper | Status | Headers |
|---|---|---|
| `success(body)` | 200 | `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Credentials: true` |
| `failure(body)` | 500 | same |

Body is `JSON.stringify`. Get-by-id uses 500 for a missing item, with `error: "Item not found."`.

`ftptos3.js` returns `undefined`. `handler.hello` returns 200 and does not use `response-lib`.

Gateway error responses: `resources/api-gateway-errors.yml`.

## Security notes visible in this repository

- `invoicetoftp.js` assigns a static AWS access key id and secret in `AWS.config.update`. The values are in git at HEAD and in the history of that file. They are omitted here on purpose. Treat them as exposed. Rotation is a human action; this document does not change the source repository.
- `ftptos3.js` reads access keys from the environment and still places them on the SDK client. Those values are not in git.
- `GET /ftptos3` and `GET /invoicetoftp/{id}` have no authorizer in the template. Both change data (DynamoDB writes, FTP delete, email).
- The Lambda DynamoDB statement allows item APIs on every table in `eu-west-1`.
- Order and product data is not partitioned by caller. Anyone who can invoke the route can read and update the shared `OrderRow` and `ProductRow` items.
- `DELETE` does not remove those shared items. It removes an item under the caller's identity id.
- Notification HTML interpolates file contents into HTML with no encoding in the handler.
- Success CORS allows credentials and uses origin `*`.
- `filetoDDb.js` is not deployed. If someone ran it, it targets region `ap-southeast-2` and table `Device`.
- Stripe secret and FTP passwords are not in git. The static SES credentials in `invoicetoftp.js` are.

## Development and tests

Script: `npm test` runs `serverless-bundle test`.

`tests/handler.test.js` imports `handler.js` and awaits `hello`. The assertions sit inside a callback argument. `hello` returns a promise and does not call that callback. **Inferred** from reading the test: the `expect` lines do not run. This inspection did not execute `npm test`.

No EDI handler has a test. `mocks/` holds API Gateway-shaped JSON for create, get, list, update, delete, billing, listOrders, and invoice update. They are local fixtures (`USER-SUB-1234`, `tok_visa`, a numeric path id). Handlers are not wired to load them.

No ESLint config, Prettier config, or husky hooks. No CI file. No coverage setting beyond what `serverless-bundle` does by itself. That default was not inspected.

`serverless-offline` is a plugin and is not given a custom port in `serverless.yml`.

## File-name notes

`listinvoices.js`, `getproduct.js`, `invoicetoftp.js`, and `ftptos3.js` do not match the camel-case of their Serverless function keys. The `handler:` strings match the file names, so deploy uses those files.

`invoiceToFtp_old.js` uses a capital T and is not referenced.

`mocks/update-event copy.json` is a second update fixture. The space is in the file name. Nothing imports it.
