# PRD — im_angular_app

Evidence date: 2026-10-12. Source: `git@bitbucket.org:overdrive/im_angular_app.git`, `master` `060172d` (2024-06-25). Remote tips `dev` (`15eaf33`) and `statements-ui` (`bdbba09`) were not file-diffed.

Provenance labels: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

No formal PRD existed in the repository or in `silicon_ark/im_angular_app/` before this file. Requirements below are recovered from the README files, routes, services, and screens. They are not a new product decision.

Hostnames and client ids below are the values committed in source. Secrets that are not in the source are not invented here.

## Product purpose

A person who already banks with I&M can open a web app that buys and withdraws I&M Wealth Capital Fund units and shows Sanlam balances, yield, transactions, and statements.

**Documented:** `README.md` title ImBank; `README_OLD.md` "I&M Capital Poc project". **Implemented:** landing, dashboard, and the service calls named in this PRD. Whether "POC" is still the product status is **Unknown / Requires Confirmation**.

The fund's books and the bank's accounts are not in this repository. This app calls an AWS API Gateway stage and the I&M test login realm.

## Users and roles

One person uses the app: an I&M retail customer who can complete the test-realm login.

There is no role table, no admin screen, and no `canActivate` guard. **Implemented.**

Access is gated by data, not by a role:

| Check | Where | Result |
|---|---|---|
| Fragment or query contains an access token | `LandingComponent.saveTokenFromUrl` | Token stored, or the browser is sent to the I&M auth URL |
| `checkwhitelist` returns `member_no` that is not `''` | `LandingComponent.getIMCustomer` | `canProceed` becomes true |
| `getuser` response is not `0` | `LandingComponent.getCheckToken` | Person is treated as an existing app user |
| Existing user and `canProceed` | `redirectUser` | Navigate to `/dashboard` |
| `canProceed` and user not found | `redirectUser` | Show terms. Accept navigates to `/dashboard` |
| Not `canProceed` | `redirectUser` | Show "Coming Soon!" |
| Dashboard has no token, or `JwtService.checkIfTokenValid` is true | `DashboardComponent.checkIfTokenValid` | `localStorage` cleared and browser sent to the I&M auth URL |

`checkIfTokenValid` returns true when `exp * 1000 < Date.now()`. Callers treat true as expired. The method name says "valid" and the return value means expired. **Implemented.**

Preferred second factor is a string from `POST preferred2fa` (`response.mode`). The validate screen masks the field when that mode is `PIN`. Both modes are posted to `validateotp` as `otpCode`. **Implemented.** What the Gateway does with a PIN versus an OTP is **Unknown / Requires Confirmation**.

## Major workflows

### 1. Open the app and sign in

Trigger: browser loads `/` or `/landing`.

Behaviour:

- `/` redirects to `/landing`. **Implemented** (`app-routing.module.ts`).
- If the URL has `access_token` as a query param, it is stored.
- Otherwise the fragment-style URL is split on `&`. If there are more than two pieces, `access_token`, `session_state`, and `id_token` are stored when present.
- Otherwise the browser goes to `environment.IMAuthLink` with `client_id=bb-web-client`, `redirect_uri={origin}/landing`, `response_mode=fragment`, `response_type=code id_token token`, `scope=openid`, and a fixed nonce.

`IMAuthLink` is `https://test-dxp.imbank.com/auth/realms/inm-retail/protocol/openid-connect/auth`. **Implemented** in both environment files.

The public IP is then read from `https://api.ipify.org/?format=json` and stored as `ip`.

### 2. Pass the whitelist gate

Trigger: a token is stored and `cif` is missing or the string `'undefined'`.

Behaviour (`getIMCustomer`):

- `POST imgetcustomer` with the access token.
- Store `custID` as `cif`, `kraPin` as `kraPin`, and `lastName` as `middle_name`.
- `POST checkwhitelist` with that `cif`.
- If `member_no !== ''`, set `canProceed` and store `new_member`.
- `POST getuser`. The method argument is ignored. The body email is `UserService.userEmail`, which is set only by `getUserFromToken`. Landing does not call `getUserFromToken` before `checkUser`. **Implemented.** Whether the email is empty on this call was not executed in a browser.
- `POST createsession` with username, IP, `session_state`, and `mac: ''`.
- `redirectUser` as in the table above.

If `cif` is already a real stored value, landing skips `imgetcustomer` and uses `checkUserStatusIfLoggedIn`. That path sets `canProceed` when `localStorage` key `new_member` is not the empty string. `getItem` returns `null` when the key is absent, and `null !== ''` is true, so a missing key also sets `canProceed`. **Implemented.** Whether that is intended is **Unknown / Requires Confirmation**.

Terms copy: "We have updated our I&M Wealth Capital Fund Terms & Conditions." The person must select the radio or the screen shows "You have not accepted the Terms & Conditions." Accept calls `router.navigate(['/dashboard'])`. Links open `environment.termsLink` (I&M Capital Information Memorandum PDF) and `environment.privacyLink` (I&M privacy notice). **Implemented.**

Coming soon copy: "Our web app will launch very soon! Please stay tuned." The button navigates to `environment.IMLink` (`https://test-dxp.imbank.com/inm-retail/select-context#/`). **Implemented.**

### 3. Use the dashboard

Trigger: navigation to `/dashboard`.

Behaviour:

- If the access token is missing or treated as expired, clear storage and send the browser to the same I&M auth URL as landing.
- Fetch a Sanlam token (`GET sanlamgettoken`) when the stored Sanlam JWT is missing or treated as expired. Store `sanlam_token`.
- `POST getsanlammembers`. The first member is selected.
- `POST imgetaccounts` with `cif`. Each `customerAccts` row is added as an account (`accountNo`, `accountName`, `availableBalance`). The method that does this is named `getSrviceAgreement` and does not call `getServiceAgreement`. **Implemented.**
- `POST preferred2fa`. The mode is stored as the auth type.
- `GET getsettings?setting=min_payment`. The first row's `value` becomes `minPayment`.
- `POST dailyyield` for the balance block.
- Member selection loads `POST statementdata` into the transaction list.

There is no router guard. A person who opens `/dashboard` with no token is redirected by this component, not by the router. **Implemented.**

Header: Dashboard link to `/dashboard`. "Back to OTG" and the logout icons call `updateSession`, clear `localStorage`, and follow `IMLink`. Profile photo comes from `POST profileimage` (`profile-photo` as a BMP data URL), with `/assets/img/default-avatar.png` when absent. **Implemented.** The logout method does not itself assign `window.location`. The anchors' `href` does.

Help text: `customercare@imbank.co.ke` and `+254 719 088 000`. **Implemented** on the dashboard help block and the failed-transaction modal.

### 4. Purchase units

Trigger: Purchase on the greeting bar. Sets transaction type `Purchase`, description "debited to purchase units", and shows `app-transaction-block`.

Inputs: I&M account, member, amount in the currency input.

Submit calls `AccountService.handleTransactionSubmit`. Rules are in `business-rules.md`. On success the service `POST`s `transaction` with `process: 'Pending'` and `type` equal to the first character of the transaction type (`P`), then opens `#reviewWithdrawalModal`.

Review screen: charge line is the literal `KES 0.00` for a purchase. The withdrawal-charge note is hidden. Submit requests an OTP only when auth type is `OTP`, then opens `#validate-modal`.

Validate screen: digits only, length stopped at 6. `POST validateotp`. If `code === 200`, `POST initiatepayment`, then if `responseCode === 'SUCCESS'`, `POST sanlampurchaseflow` and the success modal. Otherwise the failed modal.

`initiatepayment` sends `tranAmount: '100.00'`. The comment beside it is the live transaction amount. **Implemented.** Whether the bank debit is always 100 shillings while Sanlam receives `currentTransaction.amount` is **Unknown / Requires Confirmation** and should be treated as a discrepancy until a person confirms it.

Success copy for a purchase: email confirmation, and "Funds received by 12:00pm, will have value the next working day. Funds received after 12:00pm, will have value within 2 working days." **Implemented** as UI copy. The app does not calculate those value dates.

### 5. Withdraw

Trigger: Withdraw on the greeting bar. Type `Withdraw`, description "credited".

Same submit and review modal. The review charge is the number from `POST sanlamwitdrawalcharge`, not a client-side 500. A note on that modal says "More than one withdrawal within the same month will attract charges of KES 500.00." **Documented** in the template. The client does not count withdrawals in the month. The charge the API returns is what is shown.

Submit opens the validate modal. If OTP validation returns `code === 200`, `POST sanlamwithdrawalflow`. If `success === false`, failed modal. Any other response, including a thrown error object that is returned to the caller, takes the success branch unless `success` is exactly `false`. **Implemented.**

Success copy includes "The funds will be credited within 3 working days." The sentence above that still says "confirmation of your purchase" for a withdrawal as well. **Implemented.** A commented block in the same file uses "confirmation of your withdrawal". Which sentence is approved is **Unknown / Requires Confirmation**.

`#confirm-modal` repeats the irreversible-withdrawal sentence and "First withdrawal within the month is free of charge, subsequent withdrawals within the month will attract a charge of KES 500.00." That modal is the target of a hidden button on `#transactionModal`. No template opens `#transactionModal`. The greeting path opens the review modal, not the confirm modal. **Implemented.**

### 6. Read transactions and a statement

Transactions: list from `statementdata` for the selected member. Filters All, Pending, Purchase, Withdrawal, Interest. Pending matches `status` lowercased to `pending`. The others match `trans_type` lowercased. Display labels check `PURCHASE`, `WITHDRAWAL`, and status `PENDING`. **Implemented.**

Statements: member, start date, end date. End before start is rejected. `POST imstatement`. If `transactions.length < 2`, the screen says no results. Otherwise totals are formatted and rows are paged. PDF via jsPDF, 20 rows per PDF page, member number masked to leave the last two characters, file name prefix `IMC S_`. On-screen page size is `STATEMENTS_PER_PAGE` (10). **Implemented.**

### 7. Leave

"Back to OTG" updates the session and clears local storage, then the link opens the I&M select-context URL. `TokenRefreshService.logout` also clears storage and does not navigate. Nothing in the app calls `startTokenRefresh` or `checkUserActivity`. **Implemented.**

## Functional requirements

FR-001 — Landing redirect. The empty path redirects to `/landing`. **Implemented.** Confidence: high.

FR-002 — I&M login. A visit without a stored fragment token sends the browser to the I&M test realm with client `bb-web-client` and redirect `{origin}/landing`. **Implemented.** Confidence: high.

FR-003 — Whitelist. A customer whose whitelist `member_no` is `''` sees Coming Soon and a link back to OTG. **Implemented.** Confidence: high.

FR-004 — First-time terms. A customer who can proceed and is not returned by `getuser` as an existing user must accept terms before `/dashboard`. **Implemented.** Confidence: high. The `getuser` email input is the gap noted above.

FR-005 — Dashboard session. `/dashboard` without a usable access token returns the person to the I&M auth URL. **Implemented.** Confidence: high.

FR-006 — Portfolio. The dashboard shows the sum of `actual_balance` across Sanlam members and the last two daily yield rates. **Implemented.** Confidence: high.

FR-007 — Purchase. A purchase creates a pending transaction, reviews it, validates an OTP, calls `initiatepayment`, then `sanlampurchaseflow`. **Implemented.** Confidence: high. The hardcoded `tranAmount` is part of this behaviour.

FR-008 — Withdrawal. A withdrawal creates a pending transaction, shows the API withdrawal charge, validates an OTP, then calls `sanlamwithdrawalflow`. **Implemented.** Confidence: high.

FR-009 — Statements. A statement requires a member and a start date on or before the end date, then downloads a PDF. **Implemented.** Confidence: high.

FR-010 — Support contacts. Failed transactions and the help block show `customercare@imbank.co.ke` and `+254 719 088 000`. **Implemented.** Confidence: high.

FR-011 — Minimum amount. Amounts below the `min_payment` setting are rejected with a KES message. **Implemented.** Confidence: high. The setting's numeric value is not in this repo.

## Integrations

| System | Direction | Trigger | Data |
|---|---|---|---|
| I&M test realm `inm-retail` | Browser redirect out; tokens return on the fragment | No token on landing or dashboard | Auth code, id token, access token, session state |
| AWS API Gateway stage `dev` | App to Gateway | Each service method | JSON bodies and `Authorization` set to the raw access token (Sanlam token fetch in one service builds a `Bearer` header and then does not send it) |
| api.ipify.org | App to ipify | After landing init | `{ ip }` |
| I&M select-context | Browser navigation | Logout, coming soon | No body |
| Terms PDF and privacy page | New tab | Terms links | No API |

Gateway paths the app calls are listed in `technical-documentation.md`. Failure behaviour is almost always a swallowed error, a `null` return, or the failed modal. There is no retry loop. `logerror` is only in comments inside `PaymentService`.

`parent_no` on purchase and withdrawal is the literal `09576`. **Implemented.** What that number means in Sanlam is **Unknown / Requires Confirmation**.

## Constraints

- Currency shown and sent is KES.
- Both Angular environment files point at the test I&M host and the Gateway `/dev` stage. A production build replaces the file and does not change the URLs.
- `npm start` serves on host `192.168.8.168`. The README still says `http://localhost:4200/`.
- No end-to-end package is installed. `ng e2e` is the CLI paragraph in the README only.

## Planned

No item in the repository is marked as a future requirement. Unused components (portfolio chart with fixed sample points from 2000–2001, purchase block, withdrawal block, token refresh interval) are leftover implementation, not a written roadmap. **Inferred.**
