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

Selling at the Counter

Post idempotent sales with items, split payments, coupons, VAT, and FEFO deduction.

You create every counter sale with POST /sales in apps/server/src/features/sales/route.ts. The pricing and stock logic lives in apps/server/src/features/sales/creation/service.ts, with the VAT math and coupon rules in apps/server/src/features/sales/creation/pricing.ts; apps/server/src/features/sales/service.ts only re-exports both.

Log in first

Every example below needs a bearer token. Log in once, then reuse the token.

TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@demo.pharmaflow","password":"Demo@1234"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

Post a sale with an idempotency key

You must send a fresh X-Idempotency-Key per new sale. If you retry with the same key, the server replays the stored response with status 201 instead of charging twice.

curl -X POST http://localhost:8000/api/v1/sales \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: sale-0001" \
  -d '{
    "items": [{"medicine_id":"MED-001","quantity":2,"unit_price":12.5}],
    "payment_method": "cash",
    "coupon_code": "DEMO10",
    "customer_name": "Walk-in"
  }'

You can pass coupon_code in any case, the server uppercases it before lookup. You can also pass customer_id, notes, branch_id, client_mutation_id, and device_id in the same body.

Split payments and the dominant method

You split a total with payment_lines, and the server picks the line with the largest amount as the stored payment_method. Send this when a customer pays part cash and part card.

curl -X POST http://localhost:8000/api/v1/sales \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: sale-0002" \
  -d '{
    "items": [{"medicine_id":"MED-001","quantity":1,"unit_price":20}],
    "payment_lines": [
      {"method":"cash","amount":12},
      {"method":"card","amount":8}
    ]
  }'

What the server does for you

You do not compute VAT, batches, or invoice numbers yourself. Inside one transaction the server loads the org tax policy, prices each line with vatRateForCategory from apps/server/src/features/settings/tax.ts, deducts batches with FEFO via selectFefoBatches in apps/server/src/features/sales/creation/queries.ts, writes OUT stock movements and the priced slices with insertSaleItemsAndDeductStock in apps/server/src/features/sales/creation/mutations.ts, and claims the invoice number plus ICV in the same transaction. If the payment method resolves to credit, it checks the customer credit limit and writes a CHARGE row to customer_ledger. Controlled medicines also write a row to controlled_dispense_log.

List and inspect sales

You list sales with pagination and filters, and pharmacists only ever see their own rows because the route pins the user filter to the caller for that role, applied by countSales and listSales in apps/server/src/features/sales/listing/queries.ts.

curl "http://localhost:8000/api/v1/sales?page=1&page_size=20&payment_method=cash&search=INV-001" \
  -H "Authorization: Bearer $TOKEN"

You can also filter with customer_id, from_date, and to_date. Fetch one sale with its priced lines, then its invoice stub:

curl http://localhost:8000/api/v1/sales/SALE_ID -H "Authorization: Bearer $TOKEN"
curl http://localhost:8000/api/v1/sales/SALE_ID/invoice -H "Authorization: Bearer $TOKEN"

Opening a sale you do not own returns 404 by design, see enforceSaleOwnership in apps/server/src/features/sales/listing/ownership.ts.

Next steps

Continue with coupons in /sales/coupons-and-discounts, returns in /sales/returns-and-credit-notes, and your shift in /sales/sessions-and-parked.

Last updated on October 20, 2026