# PRD — lambda_java

Evidence date: 2026-10-10.

Source: `main` at `7055717` in `git@bitbucket.org:overdrive/lambda_java.git`.

Provenance labels used below: **Documented** (stated in a project document), **Implemented** (present in code or config), **Inferred** (reasonable reading of the implementation), **Planned** (described as future work), **Unknown**.

The SAM README is a hello-world document. It is not treated as a requirement for this service.

## Product purpose

**Inferred.** The service is the HTTP backend for an I&M Capital / wealth channel. A caller sends API Gateway events. The service identifies an I&M retail user, records that user and their session in MySQL, and performs Sanlam fund purchases and withdrawals plus I&M account, OTP, statement, and payment calls.

**Unknown — requires confirmation:** product name shown to customers, the client application, and whether Kenya (`co.ke`) is the only market.

## Users and roles

| Actor | Evidence | What they do |
|---|---|---|
| API caller | **Inferred.** Every route is an API Gateway method. The caller is not named. | Sends JSON bodies or query strings. |
| Retail customer | **Implemented** constants `customerType` `RETAIL` and Keycloak realm `inm-retail` in `AccessToken` and `PaymentInitiation`. | **Inferred** as the person whose token, CIF, and accounts are passed in. |
| API Gateway authorizer | **Implemented** `Authorizer`. | Allows the call when I&M returns a master service agreement. Principal id is the literal `user`. No application roles. |
| Operator / deployer | **Documented** only as SAM CLI steps in the stock README, and **Implemented** as `samconfig.toml` profiles. | Builds and deploys the stack. |

No admin UI, no permission table, and no role check beyond the authorizer were found.

## Major workflows

### 1. Authorised request

**Implemented.** `template.yaml` sets `DefaultAuthorizer: MyAuthorizer`. `Authorizer.handleRequest` reads the `Authorization` header, calls `ServiceAgreement.doGetServiceAgreement` with that value and an empty `session_state`, and returns Allow when the JSON array contains an object with `isMaster` true. Any exception or a missing master id returns Deny. JWT parsing in `Authorizer` is commented out.

`Globals.Api.Auth.AddDefaultAuthorizerToCorsPreflight` is false. Many resources also declare their own `OPTIONS` method on the same Lambda. Whether those explicit OPTIONS calls still invoke `MyAuthorizer` is **Unknown / Requires Confirmation**.

### 2. Create or update a local user

**Implemented.** `POST /createuser` (`UserCreation`):

1. Body fields: `user_name`, `email`, `name`, `given_name`, `family_name`, `nonce`. Optional `cif` replaces `nonce` as the I&M customer identifier. Optional `new_member` is the Sanlam member number.
2. If `user.user_name` is absent, insert into `user` and insert `sanlam_link` with parent number `09576`.
3. If the user exists, update `user` (name, email, I&M identifier). The existing `sanlam_link` row is not updated on this path.
4. Response body is `accepted_terms_timestamp` for that `user_name`, as a JSON array string. HTTP status is 200 even when the body is an error string.

### 3. Record a transaction, then buy or withdraw

**Implemented.**

1. `POST /transaction` inserts `transaction` and returns `{"id": <generated id>}`. `status` and `process` are both set from the request field `process`. `created_by` and `updated_by` are the literal `1`.
2. `POST /sanlampurchaseflow` loads the user by `userName`. If that row has no `member_no`, it allocates a new member number, posts a Sanlam customer, and inserts `sanlam_link`. It then posts a Sanlam purchase (`trans_type` `PURCHASE`) and sets `transaction.process` to `complete` for `trans_id`.
3. `POST /sanlamwithdrawalflow` posts a Sanlam withdrawal (`trans_type` `WITHDRAWAL`) and sets `transaction.process` to `complete`. Body fields `userName`, `currencyCode`, and `parent_no` are read and not sent to Sanlam.

`POST /updatetransaction` sets `process` from the body (`id`, `process`). The helper always returns integer `0`, and the handler formats `{ id:0 }` (not valid JSON).

### 4. I&M payment initiation

**Implemented.** `POST /initiatepayment` builds a utility-payment body (`serviceName` `UtilityPayment`, `initChannelID` `KEIMC`, `eventID` `WEALTH_FUND_DEPOSIT_` plus the currency, `eventType` `GCHRG`) and POSTs it to the SSM URL `Initiate_Payment` with an I&M client-credentials token. A checksum is an RSA-encrypted SHA-256 of concatenated fields, using SSM `Initiate_Payment_Public_Key`. The biller account number is a source literal; the comment says it must be replaced with the account provided by I&M.

### 5. Session

**Implemented.** `POST /createsession` resolves `user_name` to a user id and inserts `sessions` (`ip_address`, `session_id`, `mac`, `created_by`). Success body is the text `Session added`. `POST /updatesession` sets `logout_time = NOW()` for `session_id`. On the success path the response body stays empty.

## Functional requirements

### FR-001 — API surface

Requirement: The system exposes the methods declared in `IM-CAPITAL-BACKEND-SERVICE/template.yaml`.

Who: API caller.

Trigger: HTTP call to API Gateway stage `dev` or `prod`.

Inputs: API Gateway proxy event. Most handlers read `body` as a JSON string. `TokenCheck` and `SettingRetrieval` read `queryStringParameters`.

Expected behaviour: The matching Lambda runs. Successful handler returns `GatewayResponse` with status 200, `Content-Type: application/json`, and `Access-Control-Allow-Origin: *`.

Outputs: Body string from the handler. Several handlers put an error message in that string and still return 200.

Validation: Mostly absent. Missing body returns an empty string and status 200. Missing JSON fields throw, and the Lambda fails.

Error conditions: Uncaught exceptions become Lambda failures. `Parameter.getParameter` calls `System.exit(1)` when SSM throws.

Evidence: `template.yaml`, `GatewayResponse`, handlers under `co.ke.imcapital.handlers`.

Implementation: Implemented, with the template/class mismatches in FR-002.

Confidence: High for routes that have both a template event and a handler class.

### FR-002 — Template and class must match

Requirement: Each `Handler` in the template must be a class that exists, and behaviour that is only in an unwired class is not an API.

**Implemented discrepancy:**

| Template function | Handler in template | In source? |
|---|---|---|
| `GetToken` | `co.ke.imcapital.handlers.GetToken` | No class with that name. `TokenRetrieval` exists and is not referenced by the template. |
| `WriteLogs` | `WriteS3Logs` | Class exists. Schedule event is commented out, so nothing invokes it. |
| `LogS3Error` | `ErrorLoggerS3` | Class exists. No event. |

Classes present and not named by any template `Handler`: `TokenRetrieval`, `SQSLogCreate`, `SanlamCustomerCreation` (handler). The purchase flow calls the *function* `SanlamCustomerCreation`, not that handler.

Implementation: Implemented as a mismatch.

Confidence: High.

### FR-003 — Request authorisation

Requirement: A request is allowed only when the I&M service-agreement call returns a master agreement id.

Who: API Gateway, via `MyAuthorizer`.

Inputs: `Authorization` header. `session_state` is passed as an empty string.

Expected behaviour: IAM policy `execute-api:Invoke` Allow or Deny for `methodArn`. `principalId` is `user`.

Validation: No local JWT signature check. The commented Auth0 JWT block is not active.

Error conditions: Any exception denies.

Evidence: `Authorizer.java`, `ServiceAgreement.java`, `template.yaml` `Auth.Authorizers.MyAuthorizer`.

Implementation: Implemented.

Confidence: High for the Lambda. Confidence is medium for which routes skip it (explicit OPTIONS).

### FR-004 — Local user record

Requirement: The system creates or updates a `user` row and, on create, a `sanlam_link` row.

Inputs: `user_name`, `email`, `name`, `given_name`, `family_name`, `nonce`, optional `cif`, optional `new_member`.

Expected behaviour: See workflow 2. Parent number on create is `09576`.

Outputs: JSON array of `accepted_terms_timestamp`. The timestamp is selected, never written, by this handler.

Evidence: `handlers/UserCreation.java`, `function/UserCreation.java`, `UserUpdate.java`, `SanlamMemberAddition.java`, `TermsChecker.java`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Whitelist lookup

Requirement: Given a CIF, return the whitelist member number.

Inputs: body `cif`.

Expected behaviour: `SELECT * FROM white_listed where CIF = '<cif>'`. Response body `{"member_no":"<Member No>"}`. Empty member number when no row.

Evidence: `WhiteListChecker`, `WhiteListUserChecker`.

Implementation: Implemented. SQL concatenates `cif` (**Inferred** injection risk; not a stated requirement).

Confidence: High.

### FR-006 — Settings

Requirement: Return `settings.value` for query parameter `setting`.

Evidence: `SettingRetrieval`, `SettingByName` (`select value from settings where setting_name = ...`).

Implementation: Implemented. GET only in the template (`/getsettings`).

Confidence: High.

### FR-007 — Sessions

Requirement: Create a session row and later stamp logout time.

Inputs: create — `user_name`, `ip_address`, `session_id`, `mac`. Update — `user_name`, `session_id`.

Expected behaviour: Insert uses the looked-up user id as `created_by`. Update sets `logout_time = NOW()` and does not check that the session belongs to that user. `user_name` on update is only logged via the user lookup.

Evidence: `SessionCreation`, `SessionUpdate` (handler and function).

Implementation: Implemented.

Confidence: High.

### FR-008 — Transactions

Requirement: Insert a transaction and update its `process` column.

Inputs: create — `account`, `value`, `type`, `process`, `im_customer_identifier`, optional `member_number`. Update — `id`, `process`. List — `im_customer_identifier` (`TransactionsRetrieval` / `function/Transaction`).

Expected behaviour: Insert returns the generated id. Both `status` and `process` receive the request `process`. Update sets `process`. Purchase and withdrawal flows set `process` to `complete` after the Sanlam call returns without throwing. They do not check the Sanlam HTTP status before marking complete, except customer creation, which treats non-201 as `Not Added` and stops before the purchase.

Evidence: `Transaction`, `TransactionCreation`, `TransactionUpdate`, `TransactionProcessUpdate`, `SanlamPurchaseFlow`, `SanlamWithdrawalFlow`.

Implementation: Implemented.

Confidence: High.

### FR-009 — Sanlam member number

Requirement: When a purchase has no existing `member_no`, allocate one from `sanlam_member`.

Expected behaviour: Read the first row. If `currencyCode` is `USD`, member number is `USD` plus a 5-digit increment of `last_usd_number`. Otherwise member number is a 5-digit increment of `last_kes_number` with no currency prefix. The following `UPDATE` sets `last_kes_number` and its `WHERE` compares `last_usd_number` to the value after the increment. The `last_usd_number` column is not assigned.

Evidence: `NewMemberNumber.java`.

Implementation: Implemented. Whether the UPDATE matches a row is **Unknown / Requires Confirmation**.

Confidence: High for the SQL text. Low for the intended numbering rule.

### FR-010 — Sanlam purchase

Requirement: Create a Sanlam customer when needed, then a purchase order.

Inputs: `auth_token`, `userName`, `currencyCode`, `amount`, `parent_no`, `trans_id`, `kraPin`, `middle_name`, optional `cif`, optional `member_no`.

Expected behaviour: Empty `auth_token` returns an empty body and does not call Sanlam. Customer create is multipart form to SSM `Sanlam_Create_Customer` and must return HTTP 201. Purchase is multipart `trans_type=PURCHASE`, `member`, `amount` to SSM `Sanlam_Create_Purchase_Order`. Then `process` becomes `complete`.

Evidence: `SanlamPurchaseFlow`, `SanlamCustomerCreation` (function), `SanlamPurchaseCreation`.

Implementation: Implemented.

Confidence: High.

### FR-011 — Sanlam withdrawal

Requirement: Post a withdrawal and mark the transaction complete.

Inputs: `auth_token`, `member_no`, `amount` (integer), `trans_id`. Other parsed fields are unused.

Expected behaviour: Empty token returns `Access token cannot be null or empty` from `SanlamWithdrawalCreation` before the HTTP call. Otherwise multipart `trans_type=WITHDRAWAL`.

Evidence: `SanlamWithdrawalFlow`, `SanlamWithdrawalCreation`.

Implementation: Implemented.

Confidence: High.

### FR-012 — Sanlam reads

Requirement: The API can fetch a Sanlam token, daily yield, member details plus balance, subgroup members, statement data, withdrawal charges, and account balance, using SSM URLs and a bearer token from the request where the function sends one.

Evidence: handlers `SanlamTokenRetrieval`, `SanlamDailyYield`, `SanlamMembers`, `SanlamSubGroupMembers`, `SanlamStatementData`, `SanlamWithdrawalCharges`, `SanlamAccountBalance`, and the matching `function` classes.

`SanlamTokenRetrieval.getSanlamToken` sets `taxable = true` with the comment "will change depending on user type", so the non-taxable consumer key is not used.

`/accountbalance` (`AccountBalance`) does not use SSM or the request. It GETs a fixed UAT Sanlam balance URL for an account number compiled into the method.

Implementation: Implemented.

Confidence: High.

### FR-013 — I&M identity, OTP, profile, accounts, statements

Requirement: The API proxies I&M calls whose URLs come from SSM, except the hardcoded hosts listed in technical documentation.

| Route | Behaviour |
|---|---|
| `/requestotp` | GET SSM `IM_Request_OTP` with `Bearer` body `access_token`. Failure body `false`. |
| `/validateotp` | POST SSM `IM_Validate_OTP` JSON `otpCode` with `Bearer` body `access_token`. Failure body `false`. |
| `/preferred2fa` | Uses SSM `IM_Preferred_2FA`. |
| `/profileimage` | Uses SSM `IM_Get_Profile_Image`. |
| `/imgetaccounts` | SSM `IM_Get_Accounts` and `IM_Get_Account_Balances`. |
| `/imgetcustomer` | `IMCustomerRetrieval` / `IMUserDetails`. Live code uses hardcoded I&M hosts. The SSM name `IM_Get_Customer_Details` is commented out. |
| `/imaccess` | SSM `IM_Access_Control`, then `IM_Customer_Details` with `<<CIF>>` replaced by an empty string (`cif` is initialised to `""` with comment "replace when resp works"). The method returns the access-control body, not the customer-details body. |
| `/imstatement` | SSM `Sanlam_Get_Customer_Statements` via `function/IMStatement` (name is Sanlam despite the IM route). |
| `/usertoken` | Logs into the Sanlam client host inside `StatementData.postGetToken`, then loads statement data for member number `00638`. The API event body is not the source of that member number. |
| `/serviceagreement` | GET SSM `IM_Service_Agreement` and context URL `IM_Service_Agreement_Context`. |

Implementation: Implemented.

Confidence: High for the routes. Medium for the exact JSON each upstream returns, because that contract is not in this repo.

### FR-014 — Payment

Requirement: Initiate an I&M utility payment for a wealth-fund deposit.

Inputs: `requestRefNum`, `custid`, `senderAccountNo`, `senderName`, `tranAmount`, `tranCCY`, `narration`.

Expected behaviour: See workflow 4. TLS verification is disabled in `PaymentInitiation.sendRequest` (trust-all manager and hostname verifier that returns true).

Evidence: `InitiatePayment`, `PaymentInitiation`, `CheckSum`, `IMToken`.

A second class, `IMPaymentInitiation`, calls SSM `IM_Initiate_payment`. It is not the handler used by `/initiatepayment`.

Implementation: Implemented.

Confidence: High.

### FR-015 — Logging

Requirement: Failures in user create and transaction create enqueue an SQS message. An hourly function drains `log_queue` to S3.

Message shape: `yyyy-MM-dd HH:mm:ss#title#classification#message#reference`. Queue name `log_queue`, delay 5 seconds, region `eu-west-1`. The sender also calls `CreateQueue`.

S3 bucket `imcapital-dev-app-logs`, key `logs/yyyyMMddHHmm.log`.

`/logerror` is an HTTP logger (`ErrorLogger`). `DatabaseLog` can insert into `logs`. `S3Logs` selects `logs` after the max `logs_exported_time`.

`WriteS3Logs` and `ErrorLoggerS3` are deployed as functions with no trigger in the active template. `SQSLogCreate` is a handler with no template resource.

Implementation: Implemented for the hourly drain and the enqueue calls that exist. HTTP and database log paths are partial.

Confidence: High.

### FR-016 — CORS

Requirement: Browser callers can send GET, POST, and OPTIONS.

**Implemented** in template CORS: methods `GET, POST, OPTIONS`, headers `Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token`, origin `*`. Handlers also set `Access-Control-Allow-Origin: *`.

Confidence: High.

## Integrations

| System | Purpose | Direction | Auth as implemented | Failure |
|---|---|---|---|---|
| API Gateway | Public HTTP | Inbound | REQUEST authorizer | Deny policy |
| MySQL `IMCapital` | Users, links, transactions, sessions, settings, whitelist, logs | Outbound | Username and password from Secrets Manager JSON (`host`, `username`, `password`, `port`) | Connection errors logged; several handlers return the exception text in a 200 body |
| AWS Secrets Manager | DB secret | Outbound | Lambda role (policy not in repo) | `Secret.getSecret` rethrows |
| AWS SSM | URLs and client credentials | Outbound | Lambda role, `withDecryption` true | Non-secure-string logs an error and returns empty. SSM exception calls `System.exit(1)` |
| I&M Keycloak `test-upg-dxp.imbank.com` realm `inm-retail` | Token in `AccessToken` | Outbound | Client id and secret compiled into `AccessToken` | Returns error text |
| I&M token via SSM `IM_Get_Token` | Client-credentials token, scope `Read` | Outbound | SSM `IM_Client_ID`, `IM_Client_Secret` | `IMToken` |
| I&M OTP, profile, accounts, agreements, payment | Proxied calls | Outbound | Bearer token from the caller or from `IMToken` | Often the string `false` or the exception message |
| Sanlam East Africa | Fund orders and reads | Outbound | Bearer `auth_token` from the body, or consumer key/secret from SSM | Purchase customer create stops on non-201. Other calls return the body or an error string |
| SQS `log_queue` | Error messages | Outbound then inbound | Lambda role | Errors printed; message may be dropped |
| S3 `imcapital-dev-app-logs` | Log files | Outbound | Lambda role | Logged and swallowed |

SSM parameter names referenced in code:

`IM_Service_Agreement`, `IM_Service_Agreement_Context`, `IM_Request_OTP`, `IM_Validate_OTP`, `IM_Preferred_2FA`, `IM_Get_Profile_Image`, `IM_Fetch_customer_accounts`, `IM_Get_Accounts`, `IM_Get_Account_Balances`, `IM_Access_Control`, `IM_Customer_Details`, `IM_Get_Token`, `IM_Client_ID`, `IM_Client_Secret`, `IM_public_key`, `Initiate_Payment`, `Initiate_Payment_Public_Key`, `Sanlam_Get_Token`, `Sanlam_Taxable_Consumer_Key`, `Sanlam_Taxable_Consumer_Secret`, `Sanlam_NonTaxable_Consumer_Key`, `Sanlam_NonTaxable_Consumer_Secret`, `Sanlam_Create_Customer`, `Sanlam_Create_Purchase_Order`, `Sanlam_Create_Withdrawal_Order`, `Sanlam_Withdrawal_charges`, `Sanlam_Transaction_list`, `Sanlam_Get_Member_Details`, `Sanlam_Yield`, `Sanlam_Get_Account_Balance`, `Sanlam_Get_Customer_Statements`.

`Parameter` returns a value only when the SSM type is `SecureString`. A `String` parameter yields an empty value.

## Constraints

- Region `eu-west-1` for SSM, SQS, S3, and the SAM stacks.
- Lambdas run in a fixed VPC (two subnets, one security group) regardless of `Environment`.
- `SECRET_NAME` is `imcapital-dev-mysql-aurora` for every function, including when `Environment=prod`.
- Java package and JDBC database name are fixed: `co.ke.imcapital`, database `IMCapital`.
- CORS origin is `*`.
- No request schema, no rate limit, and no idempotency key were found.
- Sample event files contain bearer tokens. Those values are not requirements and are not copied here.

## Requirements that could not be confirmed

- The business definition of a successful purchase or withdrawal (HTTP 200 from this API is not the same as Sanlam success).
- Allowed values for transaction `type`, `status`, and `process` beyond the literal `complete`.
- Whether parent numbers `09576` and statement member `00638` are production identifiers.
- Whether prod is supposed to use a different database secret, S3 bucket, and Sanlam host. The template does not switch them.
- The client that calls this API, and the I&M and Sanlam contracts for each JSON body.
