---
title: Selling at the Counter
description: 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.

```bash
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.

```bash
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.

```bash
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`.

```bash
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:

```bash
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.
