---
title: Purchasing and Purchase Orders
description: Receive batches directly, run the draft to received purchase order flow, and read the stock movement ledger.
---

You bring stock into a branch in two ways, both defined in `apps/server/src/features/purchases/route.ts` and `apps/server/src/features/purchase-orders/route.ts`: a direct batch receipt, or a formal purchase order that ends in batch receipts.

### Receive a batch directly

Post to `POST /purchases` when stock arrives without a purchase order. You must send `supplier_id`, `medicine_id`, `branch_id`, `batch_number`, `expiry_date`, `quantity`, and `unit_cost`. The handler inserts one `active` batch, then writes an `IN` row into the stock movements ledger with `reference_type: purchase`, and the medicine stock is kept accurate as a ledger sum, not a stored column.

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"supplier_id":"'"$SUPPLIER_ID"'","medicine_id":"'"$MEDICINE_ID"'","branch_id":"'"$BRANCH_ID"'","batch_number":"B-2026-041","expiry_date":"2027-06-30","quantity":100,"unit_cost":"8.250"}' \
  "http://localhost:8000/api/v1/purchases"
```

You need `admin`, `pharmacist`, `inventory_manager`, or `branch_manager` for this call. List receipts with `GET /purchases` using `status`, `search`, `supplier_id`, `branch_id`, `medicine_id`, `from_date`, and `to_date`. Written-off batches are hidden unless you pass `status=written_off`.

### Run the purchase order flow

Create a `DRAFT` order with `POST /purchase-orders` using the schema in `apps/server/src/features/purchase-orders/schemas.ts`: `supplier_id`, `branch_id`, optional `expected_date` and `notes`, plus an `items` array of `medicine_id`, `ordered_qty`, and `agreed_unit_cost`.

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"supplier_id":"'"$SUPPLIER_ID"'","branch_id":"'"$BRANCH_ID"'","items":[{"medicine_id":"'"$MEDICINE_ID"'","ordered_qty":200,"agreed_unit_cost":8.1}]}' \
  "http://localhost:8000/api/v1/purchase-orders"
```

Move the order with `PUT /purchase-orders/:id/status` to `SENT` or `CANCELLED`. A `RECEIVED` order is terminal and a cancelled one cannot change. Receive it with `POST /purchase-orders/:id/receive`, sending per-line `batch_number`, `expiry_date`, `qty_received`, and `unit_cost`; each line becomes a batch plus an `IN` movement with `reference_type: purchase_order`, and the order flips to `RECEIVED`.

### Plan ordering from low-stock suggestions

Call `GET /purchase-orders/suggested?branch_id=$BRANCH_ID` before you draft an order. It returns medicines at or below threshold with `suggested_quantity = max((threshold - stock) * 2, 10)` plus the most reliable active supplier. Paste those lines straight into your `POST /purchase-orders` body.

```bash
curl -H "Authorization: Bearer $TOKEN" "http://localhost:8000/api/v1/purchase-orders/suggested?branch_id=$BRANCH_ID"
```

### Read the movement ledger and write off stock

Query `GET /purchases/movements/all` with `medicine_id`, `branch_id`, `movement_type`, and `search` to audit every `IN`, `OUT`, `ADJUST`, `RETURN`, and `WRITE_OFF` entry. When a batch is unsellable, call `PUT /purchases/:batchId/write-off`: it zeroes `qty_remaining`, marks the batch `written_off`, and writes a negative `WRITE_OFF` movement, and the medicine stock total falls out of the ledger automatically.

```bash
curl -X PUT -H "Authorization: Bearer $TOKEN" "http://localhost:8000/api/v1/purchases/$BATCH_ID/write-off"
```

### Inspect one receipt

Fetch `GET /purchases/:batchId` when a pharmacist questions a delivery. The response joins the medicine name and supplier name to the batch row so you can confirm `batch_number`, `expiry_date`, quantities, and `unit_cost` in one call.

```bash
curl -H "Authorization: Bearer $TOKEN" "http://localhost:8000/api/v1/purchases/$BATCH_ID"
```

Compare `qty_received` against `qty_remaining` to see how much of that batch has already sold. Use the batch id as the `reference_id` when you search the movement ledger for its full history.

### Next steps

Go to `/inventory/transfers-and-stockcount` to move received stock between branches, and to `/inventory/alerts-and-expiry` to catch batches approaching expiry before they need a write-off.
