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

VAT and Money

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.

curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@demo.pharmaflow","password":"Demo@1234"}'
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

Last updated on October 20, 2026