Skip to content
PharmaFlow Knowledgebase
Esc
↑↓navigate↵open⌘Jpreview
On this page

Medicines and Batches

Search medicines with per-branch stock, manage the medicine record, read movements, and query the controlled registry.

You manage every sellable item through the medicine record in apps/server/src/features/medicines/route.ts. Stock itself lives in batches, and the medicine row carries the pricing and safety flags you need at the counter.

Search medicines with per-branch stock

Call GET /medicines with search, category, form, low_stock, is_active, and branch_id. Pass branch_id when you need sellable stock for one branch: the query sums the stock_movements ledger for that branch, as implemented by sumStockByMedicine in apps/server/src/features/medicines/stock.ts.

curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8000/api/v1/medicines?search=amox&branch_id=$BRANCH_ID&page=1&page_size=20"

Filter inactive records with is_active=all or is_active=false. Combine low_stock=true with branch_id to scope the threshold check to that branch. Without branch_id, the sum covers the whole organisation because there is no running stock column on the medicine row.

Read one medicine and its movements

Fetch GET /medicines/:medicineId for the full record, then GET /medicines/:medicineId/movements for its append-only ledger with the acting user joined in. Use the movements list when you need to explain a quantity change to a pharmacist.

curl -H "Authorization: Bearer $TOKEN" "http://localhost:8000/api/v1/medicines/$MEDICINE_ID"
curl -H "Authorization: Bearer $TOKEN" "http://localhost:8000/api/v1/medicines/$MEDICINE_ID/movements?page=1&page_size=50"

Create and update medicines

You need admin, inventory_manager, or branch_manager to write. The body schema lives in apps/server/src/features/medicines/schemas.ts and accepts name, generic_name, barcode, category, form, strength, unit, selling_price, low_stock_threshold, requires_prescription, is_controlled, vat_category, and requires_cold_chain.

curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Amoxicillin 500mg","generic_name":"Amoxicillin","category":"Antibiotic","form":"capsule","strength":"500mg","unit":"capsule","selling_price":"12.500","low_stock_threshold":20,"requires_prescription":true,"vat_category":"zero_rated"}' \
  "http://localhost:8000/api/v1/medicines"

Updates accept the same fields as partial input; only keys listed in updatableColumns are written. Deleting a medicine is a soft delete that sets is_active = 0 and emits a sync DELETE event. Product photos are never set here; they go through the validated image endpoint only.

Handle controlled medicines

Flag a record with is_controlled: true at creation. Every controlled dispense is written to controlled_dispense_log at sale time, and you read it back through GET /medicines/controlled/registry, restricted to admin, auditor, branch_manager, and pharmacist. Filter with medicine_id and patient_id when you audit one drug or one patient.

curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8000/api/v1/medicines/controlled/registry?medicine_id=$MEDICINE_ID&page=1&page_size=50"

Avoid common errors

You get 404 Medicine not found when the id is wrong or the record is soft-deleted, and 400 No fields to update when your PUT body carries nothing writable. Send money as a decimal string such as "12.500" because formatMedicine in apps/server/src/features/medicines/catalog/queries.ts returns prices that way. Keep barcode unique per product so counter search resolves to one row.

curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"selling_price":"13.000","low_stock_threshold":25}' \
  "http://localhost:8000/api/v1/medicines/$MEDICINE_ID"

Check is_active on the response after every write so you never dispense a deactivated record.

Next steps

Continue with /inventory/purchasing-and-purchase-orders to receive stock into batches, then check /inventory/alerts-and-expiry to watch thresholds and expiry for the medicines you just created.

Last updated on October 20, 2026