# PRD — magalies-pharma-heisenberg-frontend

Evidence date: 2026-10-09.

Source: `git@bitbucket.org:overdrive/magalies-pharma-heisenberg-frontend.git`, `master` at `f153f4f` (2026-02-02). Full hash `f153f4f5d4807c85e49c01a5e5a77c594eff0cc3`.

There is no prior PRD in the source repository or in Silicon Ark. `README.md` is the Angular CLI template. Requirements below are recovered from routes, components, services, guards, environment files, and Cypress specs. Provenance is labelled.

Secrets committed in `src/environments/environment.ts` and `environment.prod.ts` (what3words key, Cognito identifiers) are not copied into this document.

## Product purpose

**Inferred.** A browser application Magalies Pharmaceuticals staff and customer users use to take a prescription from submission through review, batch manufacture, pack scanning, and courier paperwork.

**Documented** only as the names in `README.md` ("MagaliesPharma"), `index.html` ("Magaliesberg Pharmaceuticals"), and the dashboard heading ("Magalies Pharmaceuticals").

**Implemented.** Angular SPA, package `magalies-pharma`. Default URL `/` redirects to `/portal`.

**Unknown / Requires Confirmation:** the product owner, the public URL, and whether `f153f4f` is the build in production.

## Users and interactions

Roles are Cognito groups on the access token (`cognito:groups`), read in `src/core/services/auth.service.ts`. The create-user and edit-user screens offer the same five names (`user-create.component.ts`, `user-view.component.ts`).

| Actor | Where the name is enforced in this UI | Status |
|---|---|---|
| SuperAdmin | Side nav: dashboard, users, logs, and every other section. User list card is `*authGroup="['SuperAdmin']"`. Dashboard charts are the same | **Implemented** as menu and template visibility |
| Prescription | New prescription, prescription list, batches (list), individuals, companies, medicine, order medicine, stocktake | **Implemented** in `side-nav.component.ts` `access` arrays |
| Fulfilment | Prescription list, new batch, batch list, medicine, order medicine, stocktake | **Implemented** in the side nav. "New medicine" and "New order medicine" list the string `Fulfilmennt` (three n's). A group named `Fulfilment` does not match that string |
| Packaging | Same family as Fulfilment for prescriptions, batches, medicine, order medicine, and stocktake | **Implemented** in the side nav |
| Customer | New prescription. Stocktake section header includes `Customer`; the two stocktake links do not | **Implemented** |
| Person who is not signed in | `/portal` shows the Amplify sign-in slot (`hideSignUp` true, `loginMechanisms` email). `/tandcs` and `/waybill/review/:waybill` are outside that slot | **Implemented** |
| Person who forgot a password | Amplify forgot-password slots in `portal.component.ts` | **Implemented**. `handleForgotPassword` calls `Auth.forgotPassword(formData.toString())`. What Amplify passes as `formData` and whether `toString()` yields an email is **Unknown / Requires Confirmation** |

A user may hold more than one group. `RoleAuthorisationService.isAuthorised` returns true when any current group is in the allowed list. User edit stores an array of role names (`roles`). Commit `e8dd19a` message: "allow adding of multiple user roles".

The API may reject a call the menu still shows. This repository does not contain the Lambda authorizers. See `magalies-pharma-heisenberg-serverless`.

## Navigation and access

**FR-001 — Email sign-in, no self-signup**

Requirement: Opening `/portal` shows "Sign in to your account", the logo `/assets/logo.jpg`, and a link to `/tandcs`. The user signs in with email and password. There is no sign-up form. A successful sign-in reloads the page. If the path is `/portal`, the app then navigates to `/portal/dashboard`.

Evidence: `portal.component.html`, `portal.component.ts` `handleSignIn`, `auth.service.ts` Hub `signIn` → `window.location.reload()`, `app.component.ts`.

Implementation: **Implemented**.

Confidence: High.

**FR-002 — Menu entries follow Cognito groups**

Requirement: The side nav renders a link only when `authGroup` finds one of the user's groups in that link's `access` array. An empty role list shows nothing. Groups are compared by exact string.

Evidence: `auth-group.directive.ts`, `side-nav.component.ts`, `side-nav.component.html`.

Implementation: **Implemented**.

The outer `*authGroup="currentRoles"` passes the user's own groups in as the allow-list, so any user who already has a group passes that wrapper. **Implemented**.

**FR-003 — Route role map is not applied to child screens**

Requirement recorded in code: `RoleAuthorisationGuard` holds an allow-list for `dashboard`, `submit`, `prescriptions`, `batches`, `individuals`, `companies`, `medicine`, and `logs`.

What the router does: `canActivate: [RoleAuthorisationGuard]` is set on `portal` only. `canActivateChild` is commented out. Child `canActivate` lines are commented out. For path `portal` the allow-list is an empty array, and an empty array is treated as allowed. If `isAuthenticated()` is false, the guard returns true (comment in source: "I am giving you a second chance").

Evidence: `app-routing.module.ts`, `role-authorisation.guard.ts` lines 98–114, `role-authorisation.service.ts`.

Implementation: **Implemented**. The map does not run for child routes. The Amplify authenticator, not this guard, is what blocks anonymous use of `/portal` children. A signed-in user can open a URL the menu hides, and the guard will not stop them. The API may still reject the call.

`users`, `stocktake`, `shipping`, `fulfill`, `split`, `order-medicine`, and `prescriptions-form` are absent from the map.

## Workflows

### WF-1 — Submit a prescription (signed in)

Trigger: side nav "New Prescription" or the dashboard button "New prescription". Route `/portal/submit`. Component `ClientFormComponent`.

Steps visible in the component:

1. Choose entity type Individual or Company (`EntityType` enum).
2. Upload a PDF (accept `.pdf` only).
3. Capture individual or company details, prescriber, optional distributor, addresses, and medicine line items.
4. Save. On success the code navigates to `/portal/submit` again (it does not navigate to a thank-you page).

Leaving the page while `DirtinessService` is dirty asks "You have unsaved changes. Do you want to leave this page?"

Evidence: `client-form.component.ts`, route `submit` with `canDeactivate: [DirtinessGuard]`, Cypress `cypress/integration/client-submission.ts` visits `/portal/submit` and expects the URL to include `/thanks`.

The Cypress expectation and the component's navigate target disagree. **Requires Confirmation** which outcome is still wanted. No route named `thanks` exists. `PublicFormComponent` navigates to `/thanks`, and that component is not on any route. Cypress `public-submission.ts` visits `/submit`, which is also not a route (`submit` lives under `portal`).

### WF-2 — Review, view, and export an order

Trigger: "Prescriptions" → `/portal/prescriptions`. The list resolver loads orders. Opening a row goes to `/portal/prescriptions/review/:id`.

The review screen loads one order (`OrderResolver`), can change `order_status_id` from statuses returned by the API (`extras.order_status`), edit parties and lines, split (link to `/portal/split/:id`), and open fulfilment (`/portal/fulfill/:id`).

`POST /order/{id}` saves the order. A PDF download (`jspdf`) lists line items and, when OTP `status` is `verified`, a footer "Verified by OTP on {date} by {name}, practice {practice_number}". VAT on that PDF is hardcoded at 15 percent. The source comment says `order.vat` is not available.

`/portal/prescriptions/view/:id` is a separate view component. `/portal/prescriptions-form` is routed to `PrescriptionFormComponent` and has no side-nav entry found.

Evidence: `app-routing.module.ts`, `prescription-list.component.ts`, `prescription-review.component.ts` (PDF around the OTP block), `prescription.service.ts`, `otp.service.ts`.

Order status names are not hardcoded. They come from `POST /form-data` / order extras. The legal set of statuses is **Unknown** from this repository alone.

### WF-3 — Fulfil by scanning

Trigger: from review, `/portal/fulfill/:id`.

The screen lists scanned and unscanned fulfilment items and posts scans through `BatchService`:

- `GET /fulfilment/{orderItemId}`
- `POST /fulfilment/{orderItemId}/scan/{batchItemId}`
- `POST /fulfilment/{orderItemId}/scan`
- `GET /fulfilment/{orderItemId}/fulfill`

Leaving the page always asks "Confirm if you have finished scanning" (`FulfilmentGuard.canDeactivate`). `canActivate` on that guard returns true and does not check a role. The route option is `canDeactivate: [FulfilmentGuard]` (the class is named as if it were an activate guard).

On one path the component navigates to `/space-dust-not-on-earth`, which matches the wildcard and shows `NotFoundErrorComponent`. The source comment is "makes sense". When that branch runs is inside `order-fulfilment.component.ts`. **Implemented.** Whether it is a deliberate joke destination or a leftover is **Unknown / Requires Confirmation**.

### WF-4 — Batches and labels

Trigger: "New batch" `/portal/batches/create`, list `/portal/batches`, view `/portal/batches/view/:id`.

Create requires medicine, quantity 1–500, batch number matching `^([A-Z]{3,10}[0-9]{3,10})$`, status, dosage type, and package type. Create posts `POST /batch`. Requesting a batch by package posts `POST /batch/by-package` after a confirmation dialog that collects a batch number.

Printing posts JSON to the print server:

- `POST {printServerBaseUrl}/label/`
- `POST {printServerBaseUrl}/barcode/`

Dev `printServerBaseUrl` is `https://10.10.0.96` (no port). Prod is `http://printserver:3000`. `environment.printing.dispensed_by` is the string `J Lubbe` in both files.

Evidence: `batch-create.component.ts`, `batch.service.ts`, environment files.

### WF-5 — Waybill and shipment

`ShippingService` talks to the Heisenberg API, not directly to Dawn Wing, except that the payload and the configured base URL are Dawn Wing's:

- `POST /waybill/auth` with `userName` and `password` from the environment (committed values are the literals `username` and `password`)
- `POST /waybill/capture`, `POST /parcel/capture`, `POST /waybill/complete` with `Authorization: Bearer` from that auth call
- `GET /waybill/generate` for the next waybill number (since `374b53d`)
- `GET /order/{id}/shipped` after the user confirms the order has shipped
- `GET /waybillorder/{waybillNumber}` for the public review page

Completing a waybill shows: "Once a waybill has been completed it cannot be changed. No new parcels can be captured."

`trackOrder` does not call the API. It `GET`s `assets/tracking.json`. **Implemented.**

`/waybill/review/:waybill` is outside the authenticator. It shows order number, customer order number, company, product, quantity, and date shipped.

Address helpers call what3words convert-to-3wa and autosuggest with the key in the environment file.

Prod and dev both set `waybill.baseUrl` to `http://swatws.dawnwing.co.za/dwwebservices/v2/uat/api`, sender site `TEST01`, and what3words `test.test.test`. **Requires Confirmation** that production traffic should use UAT.

`WaybillAuthInterceptor` would attach a bearer token to every `HttpClient` call. It is not registered in `core.module.ts` providers. Only `LoaderInterceptor` and `BasicAuthInterceptor` are. **Implemented** as unused class source.

### WF-6 — Catalogue, people, and companies

| Screen | Routes | API (service) |
|---|---|---|
| Individuals | `/portal/individuals`, `create`, `view/:id` | `/individual` |
| Companies | `/portal/companies`, `create`, `view/:id` | `/company` |
| Medicine | `/portal/medicine`, `create`, `view/:id` | `/medicine`, `/dosage-unit`, `/dosage-type`, `/package-type`, and nested dosage/package routes |
| Order medicine | `/portal/order-medicine`, `create` | `/ordermedicine` |

Dirty forms on create/view use `DirtinessGuard`. Delete and save actions go through `ConfirmationService` in the services that were read (`user.service`, `batch.service`, `shipping.service`).

User edit disables email and phone (`disabled: true` and `readonly`). Save sends the original email and phone, plus the edited name, roles, and a company id. Phone pattern on create and on the disabled control is `^\+[0-9]{11,12}$`. Create does not collect a password (commit `ec24304`). Reset is `GET /user/{name}/reset-password`. Enable is `GET /user/{Username}/enable`.

### WF-7 — Stocktake

"New Stocktake" `/portal/stocktake` collects medicine, quantity 1–500, batch number (same pattern as batch create), barcode `^([A-Z]{3,10}[0-9]{3,10}-[0-9]{3,10})$`, dosage, package, expiry, and optional additional information.

Posts:

- `POST /stocktake/count/`
- `POST /stocktakebarcode/scan/`
- `POST /stocktake/check/`
- `POST /stocktake/update/`

"Reports" `/portal/stocktake/report` posts `/stocktake/report/difference` and `/stocktake/report/scanned`.

These paths are not in the Silicon Ark serverless document set (tip 2023-05-03). The UI that calls them arrived in `f153f4f` (2026-02-02). Whether the deployed API implements them is **Unknown / Requires Confirmation**.

### WF-8 — Dashboard and reports

`/portal/dashboard` always shows the welcome heading. Chart cards (orders, batches, medicine) render only for `SuperAdmin`.

Data: `POST /dashboard`, `POST /report/medicine`, `POST /report/order`, `POST /report/shipped`, `POST /report/batch`. Downloads use ExcelJS and `file-saver`. Date filters are required pairs of start and end. Criteria include `monthly`.

`app.component.ts` sends every signed-in user who lands on `/portal` to the dashboard, including users who are not SuperAdmin. They see the heading and not the chart card. That matches commit `c178dee` ("welcome message to dashboard for nonne SuperAdmin's") more closely than the route map, which lists dashboard as SuperAdmin only and is not wired (FR-003).

### WF-9 — Logs, terms, missing page

- `/portal/logs` lists `GET /log`. Side nav access is SuperAdmin only.
- `/tandcs` loads `assets/swahilli.txt`. The file content is `Watch this space - Terms & Conditions coming soon`. The sign-in copy says signing in means the user agreed to those terms.
- Any other path renders `NotFoundErrorComponent`.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure visible in this app |
|---|---|---|---|---|---|
| Amazon Cognito | Browser ↔ Cognito | Sign-in, forgot password, sign-out (`Auth.signOut({ global: true })`) | Email, password, tokens, groups | Amplify `Auth` config from `environment.auth`. `mandatorySignIn: true` | Hub `signIn_failure` hides the loader. Message text is Amplify's |
| Heisenberg HTTP API | Browser → API | Each service method | JSON bodies and query strings listed above | `BasicAuthInterceptor` sets headers `accesstoken` and `idtoken` from the Cognito session on every `HttpClient` request | `UtilsService.showError` or `AlertService.errorAlert`. Empty token string if `currentSession` throws |
| S3 via presigned URL | Browser → API, then S3 | Prescription file upload/download | `POST /presigned-url/generate-upload` and `generate-download`. Bucket and folder differ by environment: `heisenberg-prescriptions-dev` or `heisenberg-prescriptions-prod`, folder `prescriptions`, file name `prescription.pdf` | Same Cognito headers on the API call. The presigned URL's own signature is produced by the API | **Unknown** beyond the service error helper. The service file is `presigned-url.service.ts` |
| what3words | Browser → `https://api.what3words.com` | Address conversion on the shipping service | Coordinates or words | API key in the environment file | Errors surface through `UtilsService.showError` on the callers that catch |
| Print server | Browser → host in `environment.api.printServerBaseUrl` | User confirms a print | JSON label or barcode payload | No auth header added in `batch.service.ts` `post` (those calls omit `headers`) | Caught and shown as an error alert |
| Dawn Wing | Indirect. This app posts to `/waybill/*` on the Heisenberg API. The environment also stores a Dawn Wing UAT URL and account fields | Waybill capture | Sender block `magaliesConfig` (account `PRYPP430`, company "Magalies Pharmaceuticals", address in Kosmos Skeerpoort) | Bearer token from `/waybill/auth` | Confirm dialogs before capture, parcel, and complete. Errors via `showError` |
| Static tracking file | Browser → same origin `assets/tracking.json` | `trackOrder` | Local JSON | Cognito headers are still attached because the interceptor is global | Returns `[]` on error |

Dev API base URL is `https://f420rd8gih.execute-api.eu-west-1.amazonaws.com/dev`. Prod API base URL is `https://api.magaliespharma.com`. Region on both Cognito blocks is `eu-west-1`.

## Constraints

- Angular 12 and Node 14 in CodeBuild. `README.md` requires `NODE_OPTIONS=--openssl-legacy-provider` for the local dev server.
- Prescription upload accept list is `.pdf`.
- Batch numbers are 3–10 capitals then 3–10 digits. Stocktake barcodes add `-` and 3–10 digits.
- Batch and stocktake quantity minimum 1, maximum 500.
- User phone, when entered, must be `+` and 11 or 12 digits.
- Password strength validator (`src/core/validators/password.ts`) requires upper, lower, digit, and a special character. The create-user form does not include a password control, so this validator is not on that form. **Implemented** as a function that the create screen does not call.
- Production build budget: initial bundle warning and error at 10mb (`angular.json`).
- `environment.prod.ts` has `production: false`, so production builds do not enable Angular production mode.

## Error and edge behaviour

| Case | Behaviour | Evidence |
|---|---|---|
| Unknown URL | `NotFoundErrorComponent` | route `**` |
| Unsaved form | Confirm before leave | `DirtinessGuard` |
| Leave fulfilment | Confirm scanning is finished, even when the dirty flag is false | `FulfilmentGuard` |
| Not signed in, opening `/portal` | Sign-in form. Guard itself returns true | portal template, guard |
| Signed in, direct URL to a hidden screen | Router allows it | FR-003 |
| API error | Snack or error alert with `error.error.message` where the service reads it | user, shipping, batch services |
| Waybill complete | User must confirm. Copy says the waybill cannot be changed afterwards. Enforcement is on the server | `completeWaybill` |
| Duplicate `OrderMedicineListComponent` | `AppModule` declares `src/app/core/components/order-medicine-list` whose template is `order-medicine-list works!`. The route uses `src/core/components/order-medicine-list`. Same selector `app-order-medicine-list` | `app.module.ts`, both components |
| OTP lookup failure | PDF footer is skipped when status is not `verified`. User name falls back to "Unknown User" | `prescription-review.component.ts` |
| `cognito:groups` missing | `updateUserAndRoles` assigns the payload field with no default. A token without groups would throw or store a non-array. **Unknown** at runtime; no guard in that method | `auth.service.ts` |

## Requirements traceability (selected)

```text
FR-001 — Email sign-in
Evidence: portal.component.html, auth.service.ts, app.component.ts
Implementation: Implemented
Confidence: High

FR-005 — Prescription submit
Evidence: client-form.component.ts, route submit, client-submission Cypress
Implementation: Implemented. Thank-you URL in Cypress is not a route
Confidence: High for the screen, Low for the post-save destination testers expect

FR-008 — Fulfilment scan
Evidence: order-fulfilment.component.ts, batch.service.ts fulfilment methods, FulfilmentGuard
Implementation: Implemented
Confidence: High

FR-010 — Waybill
Evidence: shipping.service.ts, waybill-review-orders.component.ts
Implementation: Implemented against the Heisenberg waybill routes
Confidence: Medium until the live API and Dawn Wing environment are confirmed
```

## Assumptions not treated as requirements

- That this UI and `magalies-pharma-heisenberg-serverless` are still deployed as a pair. **Inferred** from path names and Cognito group names. The API document set is older and does not list OTP, stocktake, or ordermedicine.
- That 15 percent VAT on the PDF is the business rate. The code comment states it. No configuration key exists.
- That `J Lubbe` is the dispenser who should appear on labels. It is a constant in both environment files.
