iVendNextDevelopers Request a sandbox

S39 — Pharmacy counter rules (a check before payment and document checks)

A pharmacy sells medicine under a licence. A prescription line is paid only against a valid prescription for this customer. A controlled or prescription-only line is sold only by a pharmacist whose licence is in date. A controlled sale needs the patient's identity. Quantity caps hold, and dispensed medicine is not taken back. The same rules hold when a sale is made on the Desk.

This app checks those rules at Pay on the till, at a return's confirm, and when a sales order, delivery or sales invoice is submitted on the Desk. Each rule is a record the shop fills: its drug schedules, its pharmacists, the prescriptions its pharmacist enters and its patients' identity fields. Nothing is written in per country. The recipe: recipes/pharmacy_counter.md.

What the cashier and the clerk read

When What they read
Pay, a counter assistant, a prescription line "This sale can't be paid" and "Line 1: AMOX-250: Only a pharmacist may sell this."
Pay, a pharmacist whose licence has lapsed "… Only a pharmacist may sell this: your licence ran out on 2026-10-21."
Pay, no prescription number in Sale details "… needs a prescription. Add the prescription number in Sale details."
Pay, an expired or post-dated prescription "… Prescription RX-00012 was valid until 2026-10-01." (the class's validity, the written day counting as day 1) · "… is dated 2026-10-25, after today."
Pay, every fill used, or this sale over what is left "… Prescription RX-00012 has nothing left to dispense." · "… has only 9 of this left."
Pay, another patient's prescription, or the walk-in customer "… Prescription RX-00012 is for another patient." · "… needs a named customer, not a walk-in sale."
Pay, a controlled line, the customer has no ID on record "… needs the customer's national or residence ID. Add it on their Desk record."
Pay, over the schedule's cap "… No more than 20 may be sold at once." · "The patient has reached the limit for CD Schedule 2 until 2026-11-11."
a till return, or a Desk credit note, of dispensed medicine "… Dispensed medicine cannot be taken back."
Pay on the front of the shop, prescription medicine "… Sell this at the dispensary."
Submit on the Desk, no prescription number "… needs a prescription. Add a prescription number field."

Everything else is allowed.

How a fill is reserved, spent and given back

Every sale that dispenses against a prescription, or sells a schedule with a frequency cap, is one record in the shop: PP RXC Dispensing, named by an opaque key for the patient (never their name) and their next number (3f9c0a71d2e4b8c6~0007).

What the shop does

  1. Install the app from the App library, or paste its manifest into Retail Extension Package in Desk. Read what it will do, and consent.

    • The install adds the record types PP RXC Settings, Drug Schedule, Pharmacist, Premises Licence, Prescription and Dispensing.
    • It adds fields on Item, Item Group, Customer, Sales Order, Delivery Note and Sales Invoice, and four sale details (PP RXC Rx Number, Prescriber, Prescriber Licence, Directions).
    • It adds two licence reminders and the roles PP RXC Integration, PP RXC Pharmacist and PP RXC Pharmacist in Charge.
    • It adds five services, all switched off. Nothing changes at the till yet.
  2. Make the app's integration user (integration user recipe). Give it the role PP RXC Integration alone, generate its API key, and choose it on the feed registration PP RXC Counter. Then give that role a plain read on Item, Item Group, Customer, POS Profile, POS Invoice, POS Opening Entry, Sales Order, Delivery Note and Sales Invoice (Role Permissions), and press Connect again on the app's record. ⚠ Give that user no User Permissions: a booked sale the app cannot see cannot be counted.

  3. Give your people their roles. Pharmacists get PP RXC Pharmacist. The pharmacist-in-charge gets PP RXC Pharmacist in Charge, adds each pharmacist under PP RXC Pharmacist (user, licence number, valid until) and enters the premises licence. Only that role writes the list, so nobody adds themselves. A row ticked delegate does not sell pharmacist-only medicine.

  4. Make your drug schedules (PP RXC Drug Schedule), one per class per classification axis, each with its rules. Set them on your medicine items (Drug schedules: one row per axis, with its effective from) or on an item group (Drug schedule), which covers the groups below it. For items with variants, add Drug schedules in Item Variant Settings so variants copy it. Make the settings record Default: your hours from UTC, and any walk-in customers besides each POS Profile's own.

  5. Narrow the till services before switching either on.

    • On Pharmacy counter rules (dispensary), choose the dispensary's POS Profile.
    • On (front), choose the shop's other profiles.

    A check switched on with no POS Profile answers every till.

  6. Switch the services on. Read what each is sent, tick the consent and switch it on, the three Desk services too.

    • A POS Profile holds at most three checks, 10 seconds together. This app takes one on each profile (5 s), leaving room for the age check (S37) and one more.
    • A pharmacy sells offline nothing this app must check: keep the dispensary's POS Profile on Offline selling: Disabled.

Where it shows.

What leaves the shop.

Limits, said plainly.

What is in the folder

File What it is
manifest.json the record types, fields, sale details, roles, two licence reminders, the feed registration, and five services: two checks before payment (dispensary refuse, front allow_and_flag) and three document checks (Sales Order, Delivery Note, Sales Invoice; refuse), each installed switched off
rxrules.py the pure core: the signature check (verify), the shapes (sale_context, desk_context), an item's schedules (schedules_for, strictest), prescriptions and fingerprints, the dispensings that count (counts) and their statuses (resolve), and the rules (decide)
service/app.py the service, standard library only: five routes, GET /health, the app template's handshake for many shops; one handle answers the web server and the function entry (lambda_handler) alike
Dockerfile · app.json the settings file for hosting ("runs": "function": on CitiXsys hosting it is a function, paid per call) and an image for your own server (built from the kit's root)
tests/ the rules; the service against a stand-in shop (fake_shop.py), two tills on real threads included; the calls and answers against the published contracts; the manifest; canaries.py breaks each guarded rule and expects its test to fail

Run it

python3 -m unittest discover -s samples/s39_pharmacy_counter/tests -p 'test_*.py'
python3 samples/s39_pharmacy_counter/tests/canaries.py
python3 samples/s39_pharmacy_counter/service/app.py samples/s39_pharmacy_counter/service/.env.local

Settings, one shop (service/.env.local, never committed):

Many shops (the app template's handshake): IVN_LIBRARY_APP and a store (IVN_SHOPS_TABLE on CitiXsys hosting, else IVN_SHOPS_FILE). RXC_PREFIX names the record types, fields and sale details (default PP RXC).

Technical notes

What was proven where

Make it your own

A partner's version of this app usually changes:

The service takes its prefix from the setting RXC_PREFIX, so python3 tools/manifest/rename.py samples/s39_pharmacy_counter/manifest.json --prefix "ACME RXC" renames the manifest and the hosting order, and the code needs no change for the name. A shop's schedules, pharmacists and settings move to your copy with tools/deploy/copy_records.py (Make one of our apps your own).

Needs this serves

This page in the kit