---
title: Stock and FEFO
description: Understand batches, the append-only movement ledger, and how each sale drains the earliest expiry first.
---

### The three layers of stock truth

Stock lives in three places and they have a strict order. The `stock_movements` table in `packages/database/schema.sql` is the ledger of record: append-only rows with `movement_type` of `IN`, `OUT`, `ADJUST`, `RETURN`, or `WRITE_OFF` (`packages/database/schema/catalogue.ts`). Never update or delete those rows. `batches.qty_remaining` and `medicines.stock_quantity` are caches you update alongside each movement so reads stay fast, but you can always rebuild them from the ledger.

### How batches qualify for sale

Each batch row holds `medicine_id`, `branch_id`, `qty_remaining`, `unit_cost`, `expiry_date`, `status`, and the regulatory `sfda_status`. The `selectFefoBatches` query in `apps/server/src/features/sales/creation/queries.ts` only considers batches with `qty_remaining > 0`, `status = 'active'`, a `sfda_status` of `active` or null, and an `expiry_date` on today or later. It sorts them by `expiry_date` ascending, so the earliest expiry is always the first candidate. Sales run it with all guards on, so expired, quarantined, or recalled batches can never leave the shelf.

### How FEFO deduction splits a line

`createSale` in `apps/server/src/features/sales/creation/service.ts` walks the ordered candidates until the requested quantity is covered. Map `consumedByBatch` holds what earlier cart lines already drew, so two lines selling the same medicine never take the same units twice. Each slice records `batchId`, `deduct`, `unitCost`, and its VAT fields.

`insertSaleItemsAndDeductStock` in `apps/server/src/features/sales/creation/mutations.ts` then writes the stock effects: one `sale_items` row per slice with its own `cost_at_sale` snapshot, a compare and swap decrement of `batches.qty_remaining`, one `stock_movements` row of type `OUT` with a negative `qty_delta` per slice, and a re-sync of `medicines.stock_quantity` through `syncSoldMedicines` in the same file. That per slice cost snapshot is what makes historical margin reports possible.

### What happens when stock runs short

If the available total is below the requested quantity, `createSale` in `apps/server/src/features/sales/creation/service.ts` throws an `HttpError`. When blocked batches exist, `findBlockedBatch` in `apps/server/src/features/sales/creation/queries.ts` names the batch number and its `QUARANTINED` or `RECALLED` status so you can tell the user why the sale stopped. The push replay in `apps/server/src/features/sync/service.ts` feeds those failures through `deriveRejectionCode` in `apps/server/src/features/sync/protocol/rejection.ts`: an insufficient or blocked message becomes `INSUFFICIENT_STOCK`, 403 becomes `BRANCH_MISMATCH`, 409 becomes `CONFLICT`, any other 4xx becomes `VALIDATION`.

### How other flows reuse the ledger

Purchases write `IN` movements when batches arrive (`apps/server/src/features/purchases/creation/mutations.ts`), purchase order receipts write `IN` as well (`apps/server/src/features/purchase-orders/lifecycle/mutations.ts`), returns write `RETURN` (`apps/server/src/features/returns/creation/mutations.ts`), stock counts write `ADJUST` (`apps/server/src/features/stockcount/service.ts`), and expiry write-offs write `WRITE_OFF` (`apps/server/src/features/purchases/creation/service.ts`, `writeOffBatch`). Transfers run `selectTransferFefoBatches` in `apps/server/src/features/transfers/queries.ts` on the source branch, deduct a `OUT` movement there and create a new destination batch with a matching `IN` movement in `apps/server/src/features/transfers/service.ts`. Returns are separate documents in `sale_returns`, never negative sales, and only restockable items add quantity back.

### How to read a movement row

Each `stock_movements` row tells you what, where, how much, and why. `medicine_id` plus `batch_id` names the physical stock, `branch_id` scopes it, and the signed `qty_delta` shows direction. `reference_id` and `reference_type` link back to the sale, purchase, or count that caused it, and `created_by` plus `created_at` give you the audit trail. The optional `client_mutation_id` and `device_id` (`packages/database/schema/catalogue.ts`) survive offline replays so you can trace a movement to the exact device that queued it.

### Common stock mistakes

Stock bugs usually come from touching the wrong layer.
Watch for these when you review a pull request.

- Updating `batches.qty_remaining` without writing the matching movement row
- Editing or deleting a `stock_movements` row instead of writing a reversal
- Deducting without the expiry and status guards that sales use
- Recording a return as a negative sale instead of a `sale_returns` document

### Try it with curl

Log in, then fetch a medicine to inspect its cached quantity before selling.

```bash
curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"pharm1@demo.pharmaflow","password":"Demo@1234"}'
```

```bash
TOKEN="<paste access_token here>"
curl -s "http://localhost:8000/api/v1/medicines?search=amox" \
  -H "Authorization: Bearer $TOKEN"
```

### Next steps

- Read /architecture/vat-and-money for how each FEFO slice gets its VAT line
- Read /architecture/offline-sync for how offline sales replay the same deduction
- Read /inventory/transfers-and-stockcount for the count flow that writes ADJUST movements
