---
title: VAT and Money
description: See how the org tax policy prices every line, applies coupons, and stores money safely.
---

### The org tax policy in one place

Tax lives in the singleton `org_settings` row keyed `id = 'org'` in `packages/database/schema.sql`. Two columns matter: `tax_enabled` (default false) and `tax_rate` (default `0.1800`). `loadTaxPolicy` in `apps/server/src/features/settings/tax.ts` reads them into `{ enabled, standardRate }`, and `loadSaleTaxPolicy` does the same from `apps/server/src/features/sales/creation/queries.ts`. When the row is missing the fallback policy is `{ enabled: false, standardRate: 0.18 }`. Flip the policy off and every line prices at zero VAT; leave it on and `standard` category medicines charge the configured rate.

### How a category maps to a rate

`vatRateForCategory` in `apps/server/src/features/settings/tax.ts` is the only function that turns a category into a number. Unknown categories throw always, a disabled policy returns 0, and `zero_rated` or `exempt` returns 0 while `standard` returns the policy rate. Medicines default to `zero_rated` (`packages/database/schema/catalogue.ts`), so most drugs carry no VAT while cosmetics and supplements sit in `standard` at 18 percent.

### How a sale prices each line

In `apps/server/src/features/sales/creation/service.ts` every cart line computes `lineSubtotal = unitPrice x quantity` and `lineVat = lineSubtotal x vatRate` through `priceLine` in `apps/server/src/features/sales/creation/pricing.ts`, each rounded to three decimals. Lines accumulate into `vatBucketMap` through `accumulateVatBucket` in `apps/server/src/stock.ts`, and `finalizeVatBreakdown` from the same file turns the buckets into `[{ rate, taxable_amount, vat_amount }]` stored as JSON on the sale. Each FEFO slice also stores its own `vat_rate` and `vat_amount` on `sale_items`, so the receipt can show per batch tax.

### How coupons and credit change the total

`computeTotals` in `apps/server/src/features/sales/creation/pricing.ts` applies discounts after VAT. An optional `global_discount_pct` first takes that share of the subtotal and caps at it, then a `percentage` coupon takes its share of the discounted subtotal and a `fixed` coupon subtracts its value, both capped again so the discount never exceeds them. The last expression is `total = subtotal + vatTotal - globalDiscount - couponDiscount`, floored at zero. Credit sales then check the customer's standing: `chargeCreditSale` in `apps/server/src/features/sales/creation/mutations.ts` requires a selected customer with credit allowed and enough headroom under `credit_limit`, then writes the new balance and a `CHARGE` row in `customer_ledger` emitting a `customer_ledger` sync change.

### How money is stored and returned

Money columns use `NUMERIC(12,3)` in `packages/database/schema.sql` and arrive as strings in JSON, which is why the sale response in `apps/server/src/features/sales/types.ts` carries `subtotal_amount`, `vat_amount`, and `total_amount` as strings. Parse them with a decimal library on the client, never binary floats, or rounding will drift. The `payload_hash` column stores a SHA 256 hash built in `createSale` over the canonical item list plus branch, totals, payment method, currency code, and rate, so reusing a `client_mutation_id` with different contents is detectable.

### How offline replays freeze the policy

Queued sales carry their own tax snapshot so a rate change mid outage cannot rewrite history. `parseQueuedSaleTaxPolicy` in `apps/server/src/features/settings/tax.ts` requires `enabled`, `standard_rate` (0 to 1), and a `medicine_categories` map, and `createSale` rejects the replay with `Queued sale tax policy is missing or stale for this medicine` when a queued category no longer matches the live medicine row (`apps/server/src/features/sales/creation/service.ts`). The same sale also freezes the branch currency and rate through `resolveEffectiveRate` in `apps/server/src/features/currency/service.ts` before any money is written.

### Common money mistakes

Money bugs are silent until the totals stop reconciling.
Watch for these when you review a pull request.

- Doing line math in binary floats instead of decimal-safe rounding
- Applying the coupon before VAT instead of after it
- Returning money columns as numbers and losing the third decimal
- Replaying a queued sale without checking the frozen tax policy first

### Try it with curl

Log in as admin, then read the org settings to see the live tax policy.

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

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

### Next steps

- Read /architecture/stock-and-fefo for the FEFO slices each VAT line attaches to
- Read /sales/selling-at-the-counter to apply the DEMO10 coupon at the register
- Browse /api for the settings and sales endpoint shapes
