# Business rules — lambda_java

Evidence date: 2026-10-10. Source: `main` at `7055717`.

No business-rules document exists in the source repository. Every rule below is **Inferred** from implementation unless marked otherwise. Rules are what the code does, not a decision that it should keep doing that.

## Access

| ID | Rule | Evidence |
|---|---|---|
| BR-001 | A call is allowed only when the I&M service-agreement response is a non-empty array and at least one element has `isMaster` true. The returned agreement `id` is not stored on the policy. | `Authorizer`, `ServiceAgreement.doGetServiceAgreement` |
| BR-002 | The authorizer principal is the literal `user` for every caller. | `Authorizer.generateAuthResponse("user", ...)` |
| BR-003 | The authorizer does not verify a JWT. The Auth0 verify block is commented out. | `Authorizer.java` imports |
| BR-004 | `session_state` sent to the service-agreement helper is always an empty string. The `id_token` argument is not added as a request header. | `Authorizer` passes `""`; `ServiceAgreement.getJsonNode` sets only `Authorization` |
| BR-005 | Missing or empty body on most handlers yields HTTP 200 and an empty body. That is not a validation error status. | Handlers such as `UserCreation`, `SanlamPurchaseFlow`, `WhiteListChecker` |
| BR-006 | `/tokencheck` treats any non-empty query `access_token` as present. It does not check expiry, signature, or the I&M user. The token is written into the response body. | `TokenCheck.getTokenCheck` |

## Users and terms

| ID | Rule | Evidence |
|---|---|---|
| BR-007 | A user is unique by `user_name`. Existence is `SELECT id FROM user where user_name = ...`. Zero means insert. Any other id means update. | `UserChecker`, `UserCreation` |
| BR-008 | On insert, `sanlam_link.parent_no` is `09576`. The request does not supply the parent on this path. | `UserCreation` calls `addSanlamMember(..., "09576", ...)` |
| BR-009 | If the body has `cif`, that value is stored as `im_customer_identifier` instead of `nonce`. | `UserCreation.postCreateUser` |
| BR-010 | If the body has `new_member`, that string is stored as `sanlam_link.member_no`. Otherwise the member number inserted at user-create time is empty. | `UserCreation` |
| BR-011 | Update changes `user_name`, `email`, `name`, `given_name`, `family_name`, and `im_customer_identifier` for the existing id. It does not write `sanlam_link`. | `UserUpdate` |
| BR-012 | The create-user response is `accepted_terms_timestamp` for that username. This handler never sets the timestamp. | `TermsChecker` |
| BR-013 | Email uniqueness is implemented in `UserEmailChecker` (`LOWER(email)`). `UserCreation` does not call it. | `UserEmailChecker` vs `handlers/UserCreation` |

## Whitelist

| ID | Rule | Evidence |
|---|---|---|
| BR-014 | A CIF is whitelisted when `white_listed.CIF` matches. The response member number is column `Member No`. No row yields an empty member number inside JSON. | `WhiteListUserChecker` |

## Sessions

| ID | Rule | Evidence |
|---|---|---|
| BR-015 | Creating a session requires a body `user_name` that `UserChecker` can resolve. The looked-up id is stored as `sessions.created_by`. | `handlers/SessionCreation`, `function/SessionCreation` |
| BR-016 | Session rows store `ip_address`, `session_id`, and `mac`. No expiry column is written. | Insert SQL |
| BR-017 | Ending a session sets `logout_time` to the database `NOW()` for that `session_id` only. The handler looks up `user_name` and does not compare it to the session row. | `function/SessionUpdate` |
| BR-018 | A successful session update returns an empty body. | `handlers/SessionUpdate` leaves `body` as `""` on success |

## Transactions

| ID | Rule | Evidence |
|---|---|---|
| BR-019 | A new transaction stores `account`, `member_number`, `value`, `type`, `status`, `process`, `im_customer_identifier`. `created_by` and `updated_by` are always `1`. | `TransactionCreation` insert |
| BR-020 | The request field `process` is written to both `status` and `process`. | Same insert, parameters 5 and 6 |
| BR-021 | `member_number` may be omitted and is then stored as an empty string. | `handlers/Transaction` |
| BR-022 | Listing transactions filters only on `im_customer_identifier`. | `function/Transaction` |
| BR-023 | Updating a transaction changes `process` for `id`. The helper's Java return value stays `0`, so the HTTP body is `{ id:0 }` even when the SQL runs. | `TransactionProcessUpdate`, `TransactionUpdate` |
| BR-024 | After a Sanlam purchase or withdrawal call returns without throwing, `process` is set to `complete`. Customer creation is the exception: HTTP status other than 201 returns `Not Added` and the purchase is not sent. | `SanlamCustomerCreation`, `SanlamPurchaseFlow`, `SanlamWithdrawalFlow` |
| BR-025 | A purchase with an empty `auth_token` does not call Sanlam and does not mark the transaction complete. | `SanlamPurchaseFlow` |
| BR-026 | A withdrawal with a null or empty token does not call Sanlam. The function returns the text `Access token cannot be null or empty`. | `SanlamWithdrawalCreation` |

## Sanlam member numbers

| ID | Rule | Evidence |
|---|---|---|
| BR-027 | Member numbers are taken from the first row of `sanlam_member`. | `NewMemberNumber` uses `jsonArray.get(0)` |
| BR-028 | Currency `USD` produces `USD` plus a zero-padded 5-digit number, incrementing `last_usd_number`. Any other currency increments `last_kes_number` and does not add a `KES` prefix. | `NewMemberNumber.getNewMember` |
| BR-029 | The update statement is `UPDATE sanlam_member SET last_kes_number = ? where last_usd_number = ?`. It does not set `last_usd_number`. The WHERE value is the number after the in-memory increment. | `NewMemberNumber.updateLastNumber` |
| BR-030 | If the selected user JSON already has `member_no`, purchase does not allocate a number and does not create a Sanlam customer. The purchase `member` field is then the request's `member_no` (or empty if that field was absent), not the value loaded from `sanlam_link`. | `SanlamPurchaseFlow` sets `member_no` from the body, then only replaces it inside `if (!hasMemberNo)` |
| BR-031 | Sanlam customer create sends `member_no`, `email`, `surname` (from `family_name`), `middle_name`, `first_name` (from `given_name`), `mobile_no` (from `phone_number` if present), `postal_address` if present, and `tax_no` (from `kraPin`). | `function/SanlamCustomerCreation` |
| BR-032 | Sanlam purchase form fields are `trans_type=PURCHASE`, `member`, `amount`. Withdrawal fields are `trans_type=WITHDRAWAL`, `member`, `amount`. | `SanlamPurchaseCreation`, `SanlamWithdrawalCreation` |
| BR-033 | Sanlam token retrieval always uses the taxable consumer key and secret. `taxable` is the constant `true`. | `SanlamTokenRetrieval` |

## Payments

| ID | Rule | Evidence |
|---|---|---|
| BR-034 | Payment service name is `UtilityPayment`. Channel is `KEIMC`. Customer type is `RETAIL`. Event type is `GCHRG`. Event id and biller name are `WEALTH_FUND_DEPOSIT_` plus `tranCCY`. | `PaymentInitiation` |
| BR-035 | The biller account number is a literal in source. The comment says it must be replaced with the account I&M provides. | `PaymentInitiation` |
| BR-036 | The checksum string is the concatenation, with no separator, of service name, channel, request ref, sender account, amount, and currency. It is RSA-encrypted with SSM `Initiate_Payment_Public_Key` (`RSA/ECB/NoPadding`), Base64-encoded, then SHA-256 hex. Failure becomes the checksum text `NA`. | `CheckSum` |
| BR-037 | Payment HTTP calls do not verify TLS certificates or hostnames. | `PaymentInitiation.sendRequest` |

## I&M customer and OTP

| ID | Rule | Evidence |
|---|---|---|
| BR-038 | OTP request sends the body `access_token` as a bearer token and returns the upstream body. Any exception returns `false`. | `RequestOTP` |
| BR-039 | OTP validation posts `{"otpCode":"<otpCode>"}` with bearer `access_token`. Any exception returns `false`. There is no local check of OTP length or expiry. | `OTPValidation` |
| BR-040 | `/imaccess` reads `auth_token`, loads cookies, calls access control, then calls customer details with CIF replaced by an empty string. The value returned to the caller is the access-control body. | `IMAccessControl.getAccess` |
| BR-041 | `/usertoken` loads statement data for member `00638` after a Sanlam client login. The event body is not used to choose the member. | `UserToken.getToken` |
| BR-042 | `/accountbalance` does not read the event. It requests one compiled account number on the Sanlam UAT host. | `AccountBalance.getAccountBalance` |

## Settings and logs

| ID | Rule | Evidence |
|---|---|---|
| BR-043 | A setting is `settings.value` where `setting_name` equals query parameter `setting`. Missing query string returns an empty body. | `SettingRetrieval` |
| BR-044 | SSM parameters are read only when their type is `SecureString`. Other types produce an empty string. An SSM exception terminates the process (`System.exit(1)`). | `Parameter.getParameter` |
| BR-045 | SQS log lines use `#` between timestamp, title, classification, message, and reference. User-create and transaction-create failures use title `Get access token` and classification `Critical`. | `SQSLogCreation`, catch blocks in `UserCreation` and `Transaction` |
| BR-046 | The hourly job reads every message on `log_queue`, deletes each message, then writes one S3 object under `logs/` in bucket `imcapital-dev-app-logs`. The bucket name does not change when `Environment` is `prod`. | `SQSLog` |

## Data rules that are not enforced here

The database is not created in this repository. Uniqueness, foreign keys, not-null columns, and check constraints are **Unknown**. The Java code does not reject duplicate emails, negative amounts, unknown currencies, or a second open session.
