# Business rules — lambda (IM Capital backend)

Evidence date: 2026-09-28. Revision: `master` `dfb4dca`.

No business-rules document exists in the source repository. Every rule below is **Inferred** from implementation unless the line says **Documented**. Rules are what the code does, not a confirmed product policy.

Secrets, passwords, tokens, and cookies are not copied here.

## Identity and access

**BR-001 — I&M token uses the password grant.**  
Node `handler.js` and `functions/PostAccessToken.java` post `grant_type=password`, `client_id=erc-client`, `scope=profile email` to realm `inm-retail`. The username, password, and client secret are constants in source. **Inferred.**

**BR-002 — The first service agreement is the one that is selected.**  
Node takes `result[0].id` from the agreements list (`from=0&size=30`). No rule chooses among later rows. **Inferred.**

**BR-003 — A missing access token stops the SAM token check.**  
`TokenCheck` returns the text `Access token is missing` when the query parameter is null or empty. It does not call I&M. **Inferred** from the active code. The commented block shows an earlier behaviour that did call I&M.

**BR-004 — An empty Sanlam auth token skips purchase.**  
`SanlamPurchaseFlow` returns an empty body when `auth_token` is empty, before customer creation. **Inferred.**

**BR-005 — API Gateway is declared with AWS_IAM as the default authorizer.**  
The authorizer id is the Amplify-trusted role `MyApiGatewayRole`. Handlers do not check a user role. **Inferred** that there is no application-level role matrix. Whether IAM signing is enforced is **Unknown / Requires Confirmation**.

**BR-006 — CORS allows any origin.**  
Template CORS and handler headers use `*`. **Inferred.**

## Users

**BR-007 — A user is unique by `user_name`.**  
`CheckUser` selects `user.id` where `user_name` equals the request value. Existing users are updated. New users are inserted. **Inferred.**

**BR-008 — Email match is case-insensitive.**  
`CheckUserEmail` compares `LOWER(email)` to `LOWER(<request email>)` and returns `id`, or `0` when absent. **Inferred.**

**BR-009 — `cif` replaces `nonce` as the I&M customer identifier.**  
When the create-user body has `cif`, that value is stored in `user.im_customer_identifier` instead of `nonce`. **Inferred.**

**BR-010 — A new user is linked to Sanlam parent `09576`.**  
`CreateUser` calls `AddSanlamMebmber` with parent `"09576"`. The purchase flow uses `parent_no` from the request instead. **Inferred.** The two flows do not use the same parent source.

**BR-011 — Create-user returns the terms timestamp, not the new id.**  
The response is `accepted_terms_timestamp` for `user_name`. This repository never writes that column. **Inferred.**

**BR-012 — Whitelist lookup is by CIF and returns `Member No`.**  
Table `white_listed`, column `CIF`, column `Member No`. No match returns an empty string inside `{"member_no":""}`. **Inferred.** The handler does not block later calls when the member number is empty.

## Member numbers

**BR-013 — USD member numbers are prefixed.**  
If `currencyCode` is `USD`, the new member number is `USD` plus a 5-digit number. Otherwise it is a 5-digit number taken from `last_kes_number`. **Inferred.**

**BR-014 — Counters live on the first `sanlam_member` row.**  
`SELECT * FROM sanlam_member` uses JSON array index 0. **Inferred.**

**BR-015 — Do not create a Sanlam customer when a member number is already on the user.**  
Code comment: "don't create new account if user has member number (could change in future)". The flag is `user.has("member_no")` on the joined select. **Documented** comment, **Inferred** rule.

**BR-016 — Sanlam customer create succeeds only on HTTP 201.**  
Any other status becomes the string `Not Added`, and purchase flow stops with `Error creating Sanlam user`. **Inferred.**

**BR-017 — Tax number sent to Sanlam is the request `kraPin`.**  
Form field `tax_no`. **Inferred.**

## Transactions

**BR-018 — The caller supplies the process value, and it is stored twice.**  
Insert writes the request `process` into both `transaction.status` and `transaction.process`. **Inferred.**

**BR-019 — Audit columns on insert are user id 1.**  
`created_by` and `updated_by` are the integer `1`. **Inferred.**

**BR-020 — Purchase and withdrawal set process to `complete`.**  
They do this after the Sanlam call returns without throwing. They do not read the Sanlam body to decide. **Inferred.**

**BR-021 — A thrown Sanlam error does not mark the transaction complete.**  
The catch path returns the error text. **Inferred.**

**BR-022 — Transaction list is every row for one `im_customer_identifier`.**  
No status filter. **Inferred.**

## Payments

**BR-023 — Payments are within-bank retail transfers at I&M.**  
Constants: `paymentType=WITHIN_BANK_TRANSFER`, `transferType=within-retail`, `customerType=RETAIL`, `instructionPriority=NORM`, `paymentMode=SINGLE`, `chargeBearer=OUR`, `approved=true`. **Inferred.**

**BR-024 — The creditor bank is I&M Bank Ltd.**  
Branch code `57`, name `I & M Bank Ltd`, BIC `IMBLKEN0XXX`. **Inferred.**

**BR-025 — Purpose code is `C-117`.**  
**Inferred.** What `C-117` means is **Unknown / Requires Confirmation**.

**BR-026 — Payment reference is customer id, underscore, member number.**  
**Inferred.**

**BR-027 — The originator identifier uses scheme `ID` and the account uses scheme `BBAN`.**  
Originator id is body `userID`. Account id is body `accountNumber`. The counterparty name is body `userName`. **Inferred.**

**BR-028 — The payment body is RSA-encrypted before it is sent.**  
Public key parameter `IM_public_key`. Algorithm `RSA/ECB/PKCS1Padding` in blocks of 117 bytes. The request also sends `MFACode`. **Inferred.**

**BR-029 — Payment execution date is "today" from `GetCurrentDate`.**  
**Inferred.** The timezone of that helper was not separately verified.

## Sanlam token type

**BR-030 — Sanlam token always uses the taxable consumer key.**  
`taxable` is `true`. Non-taxable parameters are unused. Comment says this will change by user type. **Inferred** current rule. **Documented** future change, not implemented.

## Settings and logs

**BR-031 — A setting is a single name lookup.**  
`select value from settings where setting_name = '<query setting>'`. Multiple rows would all be returned as a JSON array. No default setting list is in code. **Inferred.**

**BR-032 — Login success is an info log.**  
Title `Login`, classification `info`, message `Successful Login, <timestamp>, <ip>`, reference `session_state`. Written from the service-agreement handler after agreement context, including when later product summary fails. **Inferred.**

**BR-033 — Critical logs use a fixed reference when the code has no customer id.**  
Several catch blocks call `LogToDatabase` with reference `"customer's CIF"`. **Inferred.** That string is not the customer's CIF.

**BR-034 — Log lines exported to S3 use `#` between fields.**  
Order: `created_at`, `log_title`, `classification`, `message`, `reference`. Only rows with `created_at` greater than the last export are included. The first run, when `logs_exported_time` is empty, uses one year before now. **Inferred.**

**BR-035 — The export bucket does not change with stage.**  
Bucket `imcapital-dev-app-logs`. **Inferred.**

## Fixed identifiers in callers

These are constants, not request input. **Inferred.**

| Constant | Where | Used as |
|---|---|---|
| `00006` | Node `sanlam_statement_data.js` | `groupmemberid` |
| `2022-05-06` | same file | statement `tdate` |
| `08000` | Node `sanlam_subgroup_members.js` | `main_member_no` |
| `00638` | `UserToken.java` | statement member |
| `00001-001-23192202-001` | `AccountBalance.java` | Sanlam UAT balance path. Comment in code: "Replace with actual account number" |
| `09576` | `CreateUser.java` | Sanlam parent number |

## Data rules that contradict a careful reading of the SQL

**BR-036 — Member-counter update compares `last_usd_number` to the new USD value.**  
`UPDATE sanlam_member SET last_kes_number = ? where last_usd_number = ?` binds the incremented KES and USD numbers. **Inferred** from the statement as written. Whether a row matches is **Unknown / Requires Confirmation**.

**BR-037 — Generated user id may not be requested.**  
`PostCreateUser` calls `getGeneratedKeys()` without `Statement.RETURN_GENERATED_KEYS` on the insert. `PostCreateTransaction` does pass `RETURN_GENERATED_KEYS`. **Inferred.** Effect on the id returned to `AddSanlamMebmber` was not executed.

## Rules that were not found

- No minimum or maximum payment or investment amount.
- No currency allow-list beyond the USD versus other split.
- No check that a withdrawal amount is covered by a balance.
- No check that the whitelist passed before purchase, payment, or user create.
- No session expiry handling.
- No idempotency key.
- No rule that GET must not change data, even though GET is routed to the same handlers as POST.
