# Business rules — magalies-pharma-heisenberg-serverless

Evidence date: 2026-10-09. Source: `master` at `7d003d5`.

Rules below are taken from seeds, migrations, authorizers, and handlers. Each rule is labelled. A rule inferred from code is not a signed-off business policy.

## Actors

**Implemented.** Cognito groups created in `serverless/formations/03 - cognito.yml`:

| Group | What the authorizer allows |
|---|---|
| SuperAdmin | Every Lambda authorizer except the Customer authorizer, which allows only `Customer` |
| Prescription | Prescription authorizer, and the internal authorizer |
| Fulfilment | Fulfilment authorizer, and the internal authorizer |
| Packaging | Packaging authorizer, and the internal authorizer |
| Customer | Customer authorizer only. No route uses it |

**Implemented.** Routes that use `CognitoAuthorizer` accept any signed-in user of the pool. They do not read the group.

**Inferred.** The group names match the workflow stages (prescription entry, fulfilment/manufacture, packaging). That mapping is the authorizer attachment, not a written policy.

## Lookup codes

**Documented** as seed data in `database/seeds/form-data.sql`. These are the only status and type codes inserted by the repository.

### Order status

| Code | Label |
|---|---|
| AWR | Awaiting Review |
| AWF | Awaiting Fulfillment |
| PKG | Packaging |
| SHP | Shipped |

### Order item status

| Code | Label |
|---|---|
| AWF | Awaiting Fulfillment |
| AWM | Awaiting Manufacture |
| ISK | In Stock |
| PRC | Procured |
| PKD | Packed |

### Batch status

| Code | Label |
|---|---|
| AWM | Awaiting Manufacture |
| AWP | Awaiting Packaging |
| ISK | In Stock |
| DPL | Depleted |

### Individual type

| Code | Label |
|---|---|
| PNT | Patient |
| DOC | Doctor |
| VET | Veterinarian |
| PHM | Pharmacist |

### Company type

| Code | Label |
|---|---|
| DOC | Doctor's Practice |
| VET | Veterinary Practice |
| PHM | Pharmacy |
| DIS | Distributor |

### Delivery type

| Code | Label |
|---|---|
| COL | Collection |
| DEL | Delivery |

**Inferred.** Nothing in the handlers enforces an order from AWR to SHP or a batch from AWM to DPL. `POST order/{order_id}` writes whatever `order_status_id` the body sends.

## Parties

**Implemented** in `src/prescription/submit/handler.js`.

- A new individual or prescriber receives `profile_number` = UUID. The column is unique.
- Individual type on the submitted individual decides the order link: `DOC` and `VET` set `doctor_id`, `PNT` sets `patient_id`, `PHM` sets `client_id` and requires a company. The thrown message is "A Pharmacist must be associated with a company".
- A company with no individual sets `client_id`.
- Prescriber is stored on `prescriber_id`. After migration `11_23_2022_1309` that foreign key references `individual`. The earlier migration `08_16_2022_0840` referenced `company`. The current handler inserts a prescriber as an individual.

**Implemented.** `is_animal` exists on `individual` (default 0). No handler branches on it.

**Implemented.** A company may point at another company through `distributor_id` (`08_08_2022_1010`). No handler in `src/` was found that sets or reads that column for a business decision.

## Prescription lines

**Implemented.**

- Submit sets every new line to item status `AWF` and `price_per_item` 0.
- Edit also writes `price_per_item` 0.
- Edit intends to allow quantity changes and deletion only while the line status code is `AWF`, `AWM`, or `ISK`. Other statuses are returned in `failed` and not changed. The `in` check is written against the array of ids (see PRD WF-2). Treat the status list as the author's intent and the `in` operator as the code that actually runs.
- New lines on edit use the body's `order_item_status_id` when present, otherwise the first id returned for those three codes. Query order of that id is not specified in the handler.

## Catalogue

**Implemented.**

- Medicine create validation (`src/medicine/create/validate.js`) requires `name`, `description`, `barcode_prefix`, `active_ingredient`, and `schedule_status`.
- `barcode_prefix` is unique on `medicine`. `medicine_number` is unique and NOT NULL on `medicine_dosage_type` after the August 2022 migration.
- `contained_quantity` is a float. The November 2022 commit message is "made contained quantity for package types a float".
- Package rows have `label_name`, `price`, and `is_active` (default 1). Dosage types have `is_active` (default 1).
- Deletes of medicine, dosage unit, dosage type, package type, dosage type link, and package link set `deleted_at`. They are hidden from list and view. SuperAdmin is the authorizer on those deletes.
- Dosage units seeded: grams `GRM`, millilitres `MIL`, milligrams `MGRM`, unit `UNT`.
- Dosage types seeded: Tablet, Capsule, Suspension, Injection, Gel, Drops, Powder.
- Package types seeded: Container, Bottle, Bag, Syringe, Dropper Bottle, Vial.
- The medicine seed includes compounded names such as Acetyl Cysteine with dosage strength text and a medicine number (`AC-250` in the first row). That seed is catalogue data, not a clinical rule.

**Unknown.** What `schedule_status` values mean beyond the required integer. The seed uses `1`.

## Manufacture and stock

**Implemented.**

- Creating a batch inserts `quantity` batch items, each with a null barcode, `in_stock` true, and no order line.
- `batch_number` is unique. `batch_item.barcode` is unique and nullable, so many unscanned items can coexist.
- Stock check counts batch items for a package type that are not yet tied to an order line.
- Package list `stock_on_hand` counts items with `in_stock` true whose batch status is `ISK`.
- Batch view sets `remaining_quantity` to `quantity_produced` minus the number of ordered items loaded for that batch.
- Batch report sets `stock_remaining` to the count of in-stock batch items on the batch.
- The three counts do not use the same filter. **Requires confirmation** which definition staff should trust.

## Packaging scan

**Implemented** in `src/fulfilment/scan/handler.js` and `fulfill/handler.js`.

- A barcode that already exists on any batch item is rejected: "Barcode already scanned".
- The batch number is the scanned barcode up to the first `-`.
- The medicine number checked against the catalogue is the scanned `medicine_number` up to the first `-`. The full scanned string is stored on `batch_item.medicine_number`.
- The free item must share the order line's `medicine_dosage_package_type_id`.
- Mismatch message: "Medicine number does not match".
- When linked items already equal `total_quantity`, the response is HTTP 422 with `success: true` and "Order Item already fulfilled".
- Pack (`GET .../fulfill`) requires every linked batch item to have a barcode, then sets `in_stock` 0 and item status `PKD`.
- Pack does not set the order to `PKG` or `SHP`.

**Inferred.** A barcode is expected to look like `{batch_number}-{suffix}` and a medicine number like `{medicine_number}-{suffix}`, because both parsers use `substring` before the first `-`. The repository does not document the barcode format beyond that code.

## Orders split

**Implemented.**

- Child orders copy status, shipping details id, and whichever of patient, doctor, and client are set. They do not copy `prescriber_id` in the insert object in `split-create/handler.js`.
- Child `parent_order_id` points at the order in the path.
- Move tries to allow an item only when its order's `parent_id` equals the target parent or the target id. The schema column is `parent_order_id`. **Requires confirmation** before treating move as a working rule.

## Shipping

**Implemented.**

- Delivery type is required on submit because `shipping_details.delivery_type_id` is NOT NULL and is taken from `address.delivery_type_id`.
- `waybill_number` starts null.
- `waybill_data` is a JSON column. Order edit stores `JSON.stringify(data.waybill_data)`.
- Tracking rows live in `tracking_details.tracking` (TEXT) keyed by `shipping_details_id`.

## Users

**Implemented** in `src/user/create/validate.js` and `register.js`.

- Email, name, and phone are required.
- Phone length is at least 12 and the first character is `+`. Example in the error string: `+12345678912`.
- Password length must be at least 8. The handler overwrites the password with a 16-character mix of lower, upper, symbol, and digit before validation.
- Cognito username is the email. Email is marked verified at create. Temporary password is emailed (`DesiredDeliveryMediums: EMAIL`).
- `role` is passed straight to `adminAddUserToGroup`. The code does not check it against the five group names.
- Delete disables the user. It does not remove them from groups or from `user_profile`.

**Implemented and discrepant.** Code reads `USER_POOL_ID`. The example env block is named `COGNITO_POOL_ID`. See the PRD.

## Audit

**Implemented.** Model insert and update hooks write `log` with entity id, entity name, old JSON, new JSON, and `user_id` from the query context. Callers that pass `log_type` override the default `INSERT` or `UPDATE`. Scan passes `SCAN`. Deletes pass `DELETE`.

`GET log` is limited to SuperAdmin.

## Data naming

**Documented** in `README.md`:

- Table names are singular and lowercase (`company`, not `companies`).
- Columns are snake_case, preferably no more than two words.
- A foreign key column is `{table}_id`.

The migrations follow that pattern (`order_status_id`, `medicine_dosage_package_type_id`).

## Rules that are not enforced

Recorded so they are not invented later.

- No handler checks that a medicine's `schedule_status` blocks a sale.
- No handler checks stock before `POST prescription` accepts a line.
- No handler refuses a batch whose `expiry_date` is in the past.
- No handler requires `is_active = 1` before a package is ordered.
- Order line price is not taken from `medicine_dosage_package_type.price`.
- Collection versus delivery does not change the submit path, other than storing `delivery_type_id`.
