# PRD — magalies-pharma-heisenberg-print-server

Evidence date: 2026-10-09.

Source: `git@bitbucket.org:overdrive/magalies-pharma-heisenberg-print-server.git`, `master` at `07aa3df` (2022-10-19).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `server.js`, `print.js`, the three ZPL templates, `package.json`, and the commit history on that revision. Provenance is labelled.

## Product purpose

**Documented.** `README.md` is the line "Heisenberg Print Server". `GET /` responds with the string `This is the Heisenberg Print Server`. `package.json` `name` is `heisenberg-test-print` and `description` is empty.

**Implemented.** A Node.js process accepts JSON over HTTP and sends ZPL to a named local printer.

**Inferred.** It prints batch barcodes and medicine labels for the Magalies Pharma Heisenberg workflow. The sibling Silicon Ark set `magalies-pharma-heisenberg-serverless` describes packaging that scans a barcode of the form `{batch or medicine number}-{suffix}`. This repository never names that API.

**Unknown / Requires Confirmation:** the product owner, the calling application, and whether the service is still operated.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| HTTP client posting JSON | `app.post('/barcode')` and `app.post('/label')` in `server.js` | **Implemented**. The client is not named |
| Person reading `GET /` | `res.send('This is the Heisenberg Print Server')` | **Implemented** |
| Operating-system print spooler | `printer.printDirect({ printer: printer_name, type: "RAW" })` | **Implemented** as a library call. The spooler is not in git |
| Operator who starts the process | `app.listen(3000)` | **Inferred**. No `start` script, service file, or README command |

No login, role, session, or user record exists in this repository.

## Systems the product talks to

1. Any HTTP client that can reach port 3000. CORS is `cors()` with no origin list. **Implemented.**
2. Three printer queues, hardcoded in `server.js`: `Barcode-Printer`, `Small-Label-Printer`, `Big-Label-Printer`. **Implemented** as strings. The queues themselves are **Unknown / Requires Confirmation**.
3. `@thiagoelg/node-printer`, required from `@thiagoelg/node-printer/lib`. **Implemented.**

No database, file upload, email, queue broker, or outbound HTTP call is present.

## Major workflows

### WF-1 — Process start

**Implemented** in `server.js` and the top of `print.js`:

1. Load Express, `body-parser`, `cors`, and `{ printBarcode, printLabel }` from `./print.js`.
2. Loading `print.js` reads three files with `fs.readFileSync`: `templates/barcodes.zpl`, `templates/labels.zpl`, `templates/labels_small.zpl`. A missing file throws before `listen`.
3. `app.use(bodyParser.json())` and `app.use(cors())`.
4. `app.listen(3000)` with no host argument. The log line is `Server listening on port ${port}`.

There is no config file and no `process.env` read.

### WF-2 — Health text

**Implemented.** `GET /` sends `This is the Heisenberg Print Server`. No other GET route exists. No JSON health payload, version, or printer status is returned.

### WF-3 — Print batch barcodes

**Implemented** in `POST /barcode` and `print.js` `printBarcode`.

1. Read JSON fields `description`, `expiry_date`, `batch_number`, `quantity`, `starting_number`.
2. Call `printBarcode` with printer name `Barcode-Printer`. If `starting_number` is falsy, pass `1`.
3. Compile `templates/barcodes.zpl` on that call.
4. For `x` from 0 while `x < quantity`, build a four-digit suffix with `("0000" + (starting_number + x)).slice(-4)` and a code `` `${batch_number}-${padded_string}` ``.
5. Each copy is rendered with `quantity: 1` and appended, plus a newline, into one string.
6. One `printDirect` sends that string as `RAW` to `Barcode-Printer`.
7. Printer success resolves the promise. `server.js` then responds with JSON `{ "message": "Printing barcodes" }`.
8. Printer failure rejects with no argument and `console.error`s the library error. The route `catch` responds with JSON `{ "message": "Printing failed" }`.

Neither response sets a status code. Express `res.json` uses 200 for both the success and the failure message. **Implemented.**

The route has no `try` around the destructuring of `req.body`. If `req.body` is missing, that line throws before the `catch`. **Implemented.**

### WF-4 — Print medicine labels

**Implemented** in `POST /label` and `print.js` `printLabel`.

1. Read JSON fields `description`, `directions`, `client`, `medicine_number`, `order_number`, `quantity`, `dispensed_by`, `contained_quantity`, `prescriber`, `date`, `starting_number`, `size`.
2. If `size == 'small'`, the queue is `Small-Label-Printer`. Any other value, including a missing `size`, uses `Big-Label-Printer`.
3. Call `printLabel`. Falsy `starting_number` becomes `1`. Falsy `size` is passed as `"standard"`.
4. `size == 'small'` compiles `templates/labels_small.zpl`. Every other size compiles `templates/labels.zpl`.
5. The loop matches WF-3, except the code is `` `${medicine_number}-${padded_string}` ``.
6. The Handlebars context includes `description`, `directions`, `client`, `barcode`, `order_number`, `dispensed_by`, `contained_quantity`, `prescriber`, `date`, and `quantity: 1`.
7. One `printDirect` sends the joined ZPL.
8. On resolve, the route responds `{ "message": "Printing labels" }`.

`POST /label` has no `catch`. A rejected print promise does not send that JSON. **Implemented.** Express 4 does not attach a rejection handler to this `async` route. What the client observes after that rejection is **Unknown / Requires Confirmation** (no test covers it).

### WF-5 — What is drawn on each label

**Implemented** as the Handlebars placeholders that the ZPL files still contain at `07aa3df`.

| Template | Placeholders that appear in the file | Passed by `print.js` but absent from this file |
|---|---|---|
| `templates/barcodes.zpl` | `description`, `expiry`, `batch_number`, `barcode`, `quantity` | none of the label-only fields |
| `templates/labels.zpl` | `description`, `directions`, `client`, `order_number`, `prescriber`, `contained_quantity`, `date`, `barcode` | `dispensed_by` |
| `templates/labels_small.zpl` | `description`, `client`, `order_number`, `contained_quantity`, `prescriber`, `date`, `barcode` | `directions`, `dispensed_by` |

Visible copy at HEAD:

- Batch label: description, `Expiry:` plus `expiry`, and `batch_number`, plus a QR of `barcode`.
- Standard medicine label: description, `Directions: ` plus `directions`, client, `Rx No:` plus `order_number`, prescriber, `Qty:` plus `contained_quantity`, date, plus a QR.
- Small medicine label: description, client, `Rx No:`, `Qty:`, prescriber, date, plus a QR. No directions line.

`dispensed_by` was printed as `Dispensed By:` on the standard label until `a44f442` (2022-10-19). The route still accepts the field. The templates no longer print it. **Implemented.**

## Functional requirements

### FR-001 — Identify the process

Requirement: `GET /` returns the text `This is the Heisenberg Print Server`.

Evidence: `server.js`.

Implementation: Implemented.

Confidence: High.

### FR-002 — Print a run of batch barcodes

Requirement: `POST /barcode` with `description`, `expiry_date`, `batch_number`, and `quantity` prints that many ZPL labels on the queue named `Barcode-Printer`. Each QR value is `{batch_number}-{suffix}` where the suffix is the four-character tail of `0000` plus `starting_number + index`. A missing or other falsy `starting_number` starts at 1.

Evidence: `server.js` `POST /barcode`, `print.js` `printBarcode`, `templates/barcodes.zpl`.

Implementation: Implemented.

Confidence: High for the code path. The physical queue name **Requires Confirmation**.

### FR-003 — Print a run of medicine labels

Requirement: `POST /label` prints `quantity` labels. `size` of `small` uses `templates/labels_small.zpl` and `Small-Label-Printer`. Any other `size`, including omitted, uses `templates/labels.zpl` and `Big-Label-Printer`. Each QR value is `{medicine_number}-{suffix}` with the same four-character padding as FR-002.

Evidence: `server.js` `POST /label`, `print.js` `printLabel`.

Implementation: Implemented.

Confidence: High.

### FR-004 — Send one raw job per request

Requirement: All copies for one request are concatenated and sent in a single `printDirect` call. The template variable `quantity` on each copy is `1`.

Evidence: `print.js` after `16e84dc`. `templates/barcodes.zpl` contains `^PQ{{quantity}}`. The label templates do not contain `^PQ`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Report a barcode print failure in JSON

Requirement: If `printBarcode` rejects, the HTTP body is `{ "message": "Printing failed" }`.

Evidence: `server.js` `catch`.

Implementation: Implemented. The status remains the default 200. **Requires Confirmation** whether callers treat that body as failure or only look at the status code.

### FR-006 — Report label print success in JSON

Requirement: After `printLabel` resolves, the HTTP body is `{ "message": "Printing labels" }`.

Evidence: `server.js`.

Implementation: Implemented. There is no matching failure body on this route. See WF-4.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| Unnamed HTTP client | Inbound | `POST /barcode`, `POST /label`, `GET /` | JSON fields listed in WF-3 and WF-4. Responses are a plain string or `{ message }` | None in this repo | Barcode print errors return the failure message. Label print errors do not. Invalid JSON is left to `body-parser` |
| OS printer queues | Outbound | `printDirect` | ZPL string, `type: "RAW"`, queue name | None in this repo | Callback `error` logs and rejects |
| Heisenberg serverless API | None in this repo | — | — | — | — |

The QR shape lines up with the pack-scan rule in `magalies-pharma-heisenberg-serverless` (substring before the first `-`). That link is **Inferred** from the two codebases. This repository has no URL, client, or comment that names the API.

## Constraints

- Port is the number `3000`. **Implemented.**
- Printer names are constants, not request fields. A caller cannot choose a queue other than by `size` on `/label`. **Implemented.**
- Templates are read once at process start. A template edit is invisible until restart. **Inferred** from `readFileSync` at module scope.
- `starting_number` of `0` is treated as missing, because the route uses a truthiness check rather than a null check. **Implemented.**
- Suffixes longer than four digits keep only the last four characters (`slice(-4)`). **Implemented.**
- No maximum on `quantity`. **Implemented** as the absence of a check.
- CORS middleware is installed with no arguments. **Implemented.**
- `package.json` `main` points at `index.js`, which is not in the repository. **Implemented** as that mismatch.

## Edge cases present in the code

| Case | Behaviour | Evidence |
|---|---|---|
| `quantity` missing, `0`, or negative | The loop does not run. `printDirect` still receives an empty string | `for (let x = 0; x < quantity; x++)` |
| `quantity` is a numeric string | JavaScript compares and adds it as a number | the same loop and `starting_number + x` |
| Non-numeric `starting_number` that is truthy | The suffix is the last four characters of `"0000"` plus `NaN` | `("0000" + (starting_number + x)).slice(-4)` |
| `size` omitted | Large printer and standard template | `size == 'small'` else branch, and `size ? size : "standard"` |
| `size` any value other than `small` | Same as omitted | `== 'small'` only |
| Printer error on `/barcode` | HTTP 200 and `{ "message": "Printing failed" }` | `res.json` inside `catch` |
| Printer error on `/label` | No `catch`. No success JSON is sent | `server.js` |
| `req.body` undefined | Destructuring throws before either route can respond | both handlers |
| Template file missing at startup | `readFileSync` throws and the process does not listen | `print.js` |
| `dispensed_by` supplied | Accepted and passed to Handlebars. Neither current label template prints it | `print.js`, both label ZPL files |
| Unknown JSON fields | Ignored | only the destructured names are read |

No test in the repository exercises these cases.
