---
title: Coupons and Discounts
description: Validate coupon codes at the register and manage them as an admin or branch manager.
---

You apply coupons by code at sale time, and you manage the coupon table from `apps/server/src/features/coupons/route.ts`.

### Log in first

Validate calls are public, but management calls need a token. Log in once for the admin examples.

```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'])")
```

### Validate a code before tendering

You call `GET /coupons/validate/:code` from the register before the customer pays, so you can show the discount or the reason it fails. The code lookup is case insensitive.

```bash
curl http://localhost:8000/api/v1/coupons/validate/DEMO10
```

The response carries `is_valid` plus `reason_invalid`, which is one of deactivated, not yet valid, expired, or max uses reached. `DEMO10` is the seeded demo coupon and it applies 10 percent off.

### Apply a coupon on the sale

You pass `coupon_code` on `POST /sales` and the sale core in `apps/server/src/features/sales/creation/service.ts` rechecks everything inside the transaction. A percentage coupon discounts the subtotal, a fixed coupon subtracts its value, and the discount never exceeds the subtotal.

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

Each successful use writes a `coupon_usage` row and increments `usage_count` on the coupon, so a code with `max_uses` stops working exactly at its cap.

### List coupons as an admin

You list every coupon with `GET /coupons`, and only the `admin` role may call it. Pharmacists and cashiers validate codes but never see this list.

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

### Create a coupon

You create coupons as `admin` or `branch_manager` with `POST /coupons`. The fields are `code`, `discount_type` (`percentage` or `fixed`), `discount_value`, plus optional `valid_from`, `valid_until`, and `max_uses`.

```bash
curl -X POST http://localhost:8000/api/v1/coupons \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "RAMADAN15",
    "discount_type": "percentage",
    "discount_value": 15,
    "description": "Seasonal counter promotion",
    "valid_from": "2026-01-01",
    "valid_until": "2026-12-31",
    "max_uses": 500
  }'
```

Codes are trimmed and uppercased, duplicates return 409, percentage values above 100 return 400, and non-positive values return 400.

### Update or deactivate a coupon

You update with `PUT /coupons/:couponId` as `admin` or `branch_manager`. You can change `is_active`, `valid_until`, `max_uses`, and `description`, and setting `is_active` to false emits a sync `DELETE` for the coupon.

```bash
curl -X PUT http://localhost:8000/api/v1/coupons/COUPON_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_active": false}'
```

### Next steps

Ring the discounted sale in /sales/selling-at-the-counter, handle post-sale refunds in /sales/returns-and-credit-notes, and review shift totals in /sales/sessions-and-parked.
