# Business rules — im_angular_app

Evidence date: 2026-10-12. Source revision: `master` `060172d`.

Rules are taken from screens, services, and the two README files. A rule that the code applies is marked **Implemented**. A rule that is only copy on a screen, with the calculation elsewhere, is marked **Documented**. A conclusion drawn from the code without a written rule is marked **Inferred**. Nothing here is an approved change to those rules.

## Product and customer

BR-001 — The app is for an I&M customer using the I&M Wealth Capital Fund. **Documented** on the terms screen ("I&M Wealth Capital Fund Terms & Conditions") and **Inferred** from Sanlam member calls plus I&M account calls. The README title is ImBank. The approved legal name is **Unknown / Requires Confirmation**.

BR-002 — Only a whitelisted customer continues. `POST checkwhitelist` must return a `member_no` other than `''`. Otherwise the person sees Coming Soon. **Implemented** (`LandingComponent.getIMCustomer`).

BR-003 — A whitelisted person who is not already a user of this app must accept the terms and the privacy policy before the dashboard. **Implemented** (`TermsAndConditionsComponent`). The links are the I&M Capital Information Memorandum PDF and the I&M privacy notice in `environment.ts`.

BR-004 — There is no separate staff or admin role in this app. **Implemented.** Absence of a role model.

## Currency and amounts

BR-005 — Amounts are in Kenyan shillings. Payment and Sanlam bodies send `tranCCY` / `currencyCode` `KES`. Messages say KES or Kes. **Implemented.**

BR-006 — The amount must be present and greater than or equal to `minPayment`. `minPayment` is the numeric `value` of setting `min_payment` from `GET getsettings`. The message uses `Kes.` and the absolute rounded value. **Implemented** (`AccountService.handleTransactionSubmit`). The setting's business value is not in this repo. **Unknown / Requires Confirmation.**

BR-007 — If settings fail to load, `minPayment` stays `0`, so any positive amount passes the minimum check. **Implemented.**

BR-008 — The amount must be less than or equal to the balance string passed into submit. The message says the amount must be less than that balance, while the comparison is `amt > Number(balance)`, so an amount equal to the balance is allowed. **Implemented.**

BR-009 — For a purchase, that balance is the selected I&M account's `availableBalance`. For a withdrawal, submit passes `actual_balance` of the selected member. The withdrawal screen also displays `actual_balance + pending_withdrawals` as the deductible balance. The cap and the displayed figure are not the same expression. **Implemented.**

## Who and what must be selected

BR-010 — A purchase requires an I&M account. A withdrawal does not. The missing-account message is only raised when `trans.type === 'Purchase'`. **Implemented.**

BR-011 — A withdrawal requires a member number. **Implemented** (the non-purchase branch).

BR-012 — The purchase member check is written as "if the members collection has length and no member number". `TransactionBlockComponent.submitTransaction` passes the selected member object in the collection argument, not the members array. An object has no `length`, so once a member object is present the check behaves like "member number required". While the field is still the initial `''`, `''.length === 0` and the member check is skipped. **Implemented.** Whether a purchase without a member is allowed is **Unknown / Requires Confirmation**.

BR-013 — Creating the pending row sends `type` as the first character of `Purchase` or `Withdraw` (`P` or `W`), `process: 'Pending'`, the CIF, the account number (empty string when no account), the member number, the amount, and `member_details`. **Implemented.**

## Charges

BR-014 — A purchase review shows charge `KES 0.00`. **Implemented** (`review-withdrawal-modal.component.html`). Matches the commit that removed the withdrawal charge from purchases.

BR-015 — A withdrawal review shows the `charge` returned by `POST sanlamwitdrawalcharge` for that member. **Implemented.**

BR-016 — Copy on the review modal: "More than one withdrawal within the same month will attract charges of KES 500.00." Hidden when the type is Purchase. **Documented** in the template. This client does not count withdrawals. The amount actually charged is whatever the Gateway returns.

BR-017 — Copy on `#confirm-modal`: the first withdrawal in the month is free and later ones cost KES 500.00. **Documented** in that template. That modal is not the one `handleTransactionSubmit` opens. See features.md.

## Purchase and withdrawal outcomes

BR-018 — The code field accepts digits only and stops at 6 characters. **Implemented** (`validate-modal`).

BR-019 — OTP is requested only when the preferred mode is `OTP`, for 180 seconds. **Implemented.** Resend requests a new OTP and restarts the timer. A `PIN` mode does not call `requestotp`. Both modes still call `validateotp` with `otpCode`.

BR-020 — Purchase proceeds to the bank call only when `validateotp` returns `code === 200`. **Implemented.**

BR-021 — The bank call is `POST initiatepayment` with a 12-digit zero-padded transaction id, CIF, sender account and name, `tranCCY: 'KES'`, narration `{memberNumber} - {first_name}`, and `tranAmount: '100.00'`. The amount on the transaction is in a comment, not in the JSON. **Implemented.** This conflicts with BR-006 and with the amount shown on the success modal. **Unknown / Requires Confirmation** whether the 100.00 is deliberate.

BR-022 — Sanlam purchase (`POST sanlampurchaseflow`) runs only when `initiatepayment` returns `responseCode === 'SUCCESS'`. The Sanlam body uses `currentTransaction.amount`, not `100.00`, plus username, transaction id, `parent_no: '09576'`, member number, `KES`, KRA pin, `middle_name` (stored from `imgetcustomer.lastName`), and CIF. **Implemented.**

BR-023 — Withdrawal calls `POST sanlamwithdrawalflow` after OTP `code === 200`, with the transaction amount, the same `parent_no` `09576`, member number, and `KES`. It does not call `initiatepayment`. **Implemented.**

BR-024 — Withdrawal shows the failed modal only when the response `success` is `false`. Other responses show success. **Implemented.**

BR-025 — Success copy states value dates for a purchase (by 12:00 value next working day; after 12:00 within 2 working days) and crediting within 3 working days for a withdrawal. **Documented** in `success-modal.component.html`. The client does not schedule those dates. The live paragraph also says the email confirms "your purchase" when the transaction is a withdrawal.

BR-026 — A failed transaction tells the person to try again or contact `customercare@imbank.co.ke` or `+254 719 088 000`. Try again reopens the validate modal and requests an OTP. **Implemented.**

## Statements and yield

BR-027 — A statement needs a member, a start date, and an end date. The end date must not be before the start date. **Implemented.**

BR-028 — Fewer than two transactions in the response is treated as no results. **Implemented.**

BR-029 — PDF pages hold 20 transactions. The on-screen pager uses 10 (`STATEMENTS_PER_PAGE`). **Implemented.**

BR-030 — The PDF file name masks every character of the member number except the last two, as `X`. **Implemented.**

BR-031 — Portfolio value is the sum of `actual_balance` across members returned by `getsanlammembers`. **Implemented** (`balance-block`).

BR-032 — Daily yield shows the previous rate and the latest rate from `dailyyield` (`secondLast` and `last`). Equal rates use a neutral marker. A lower latest rate uses the red asset. A higher latest rate uses the green asset. **Implemented.**

BR-033 — Transaction filters: Pending matches status `pending` case-insensitively. Purchase, Withdrawal, and Interest match `trans_type` the same way. Choosing the active filter again, or All, clears the filter. **Implemented.**

## Session and identity

BR-034 — Dashboard treats the access token as expired when `exp * 1000` is before now, clears storage, and returns to I&M login. **Implemented.** The method is named `checkIfTokenValid` and returns true in the expired case.

BR-035 — Sanlam token is fetched again when missing or when that same expiry check is true. **Implemented.**

BR-036 — A refresh call exists: every 300000 ms, `client_id=im-wealth`, grant `refresh_token`, `client_secret` read from `session_state`, against the realm token URL. Idle logout is written as 600 seconds without `lastActiveTime` being updated. No component starts either interval, and no component writes `refreshToken` or `lastActiveTime`. **Implemented** as unused code. The login redirect uses client `bb-web-client`, not `im-wealth`. **Unknown / Requires Confirmation** which client id is correct.

BR-037 — Leaving via the header calls `POST updatesession` and clears all local storage. **Implemented.**

BR-038 — `createsession` sends an empty `mac`. **Implemented.**

BR-039 — `middle_name` sent on a Sanlam purchase is the customer's `lastName` from `imgetcustomer`. **Implemented.** Whether Sanlam expects a middle name is **Unknown / Requires Confirmation.**

## Host and environment

BR-040 — Development and production builds use the same Gateway `/dev` stage and the same `test-dxp.imbank.com` URLs. **Implemented** (`environment.ts`, `environment.prod.ts`). A distinct production rule was not found.
