iVendNextDevelopers Request a sandbox

S37 — Age-restricted sale check (a capture on scan and a check before payment)

A shop that sells alcohol, tobacco, vapes, knives or some medicines must see the customer's ID before it sells them. This app gives each restricted item group (or item) its age. When the cashier scans a restricted item, the till shows the date of birth the customer must be born on or before. One tap records the check on the sale. At Pay, a restricted line the sale's check does not cover is refused:

When What the cashier sees
the scan of a restricted item a sheet titled Age check: "WHISKY-70: customers 18 or over. Born on or before 22 Oct 2008. Ask for ID if they look under 25.", then "This will be added to Single Malt 70 cl: PP AGE ID Checked: 18 or over". Add with these details puts the item on the sale and records the check; Don't add it leaves both off
a later restricted item of the same sale, needing that age or less nothing: the sale was asked about already
Pay, a restricted line with no check on the sale (picked from a list, Don't add it earlier, or a value in Sale details that is not an age, such as a date of birth) "This sale can't be paid" and "Line 2: WHISKY-70 is for customers 18 or over. See their ID, then enter 18 in Sale details, or take it off."
Pay, a line needing more than the recorded check "Line 3: VAPE-01 is for customers 21 or over; the sale's ID check says 18. See their ID, then enter 21 in Sale details."

Everything else is allowed. A refund is never judged; an exchange is judged for what it sells. No date of birth or ID number is asked or kept: the sale records the age band the cashier confirmed. The recipe: recipes/age_check.md.

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 type PP AGE Settings, a field Customers must be at least this old on Item Group and on Item, the sale detail PP AGE ID Checked, the role PP AGE Integration, the two services and the feed registration, the services switched off. Nothing changes at the till yet.
  2. Make the app's integration user (the integration user recipe): tick Is Desk User, give it the role PP AGE Integration alone and generate its API key. In Role Permissions, give that role a plain read on Item Group and Item (the app reads the group tree and the items' ages). Choose the user on the feed registration PP AGE Check, then press Connect again on the app's record, which sends that user's key to the service.
  3. Make the settings record Default in PP AGE Settings: the shop's hours from UTC (4 for Dubai, 3 for Nairobi; the date of birth shown is the shop's own day's), and, if the shop has the policy, Ask for ID from anyone who looks under (25).
  4. Set the ages: on each restricted item group (Alcohol 18, Vapes 21), Customers must be at least this old; a group's age covers every group below it unless a group below sets its own. On an item whose age differs from its group's (a knife among kitchenware), the same field on the item.
  5. Name the restricted groups on the scan service: open Age check in Retail Extension Subscription and add the groups to its settings, as "item_groups": ["Alcohol", "Tobacco"] beside "event" and "item_field" (the field already asks about items with their own age). The check at Pay reads every group's age itself, so a group left off this list is still caught at Pay.
  6. Switch both services on: open Age check and Age check at Pay, read what each is sent, tick the consent, choose the POS Profiles and switch each on. One scan service per POS Profile: where another app already answers the scan (Till guard rules, Warranty capture), Age check cannot be switched on for that profile; switch on Age check at Pay alone there, and the cashier enters the age in Sale details when Pay asks. A POS Profile also holds at most three checks before payment, 10 seconds together (this one takes 3 s; 4 s on CitiXsys hosting).

Where it shows. At the till, a sheet at the scan of a restricted item, and a refusal at Pay. The check is kept on the sale as the sale detail PP AGE ID Checked, under the basket's 🗒 Sale details, where the cashier can also type it. In Desk, the settings record and the field on Item Group and Item. A scan or a sale the service could not check goes on and is flagged on Sales to review.

What leaves the shop. At the scan of a restricted item: the item, its group, quantity, unit, batch and serial, the code scanned, the sale's reference, the store, the POS Profile, the till's language, the cashier's user and the customer. At Pay, and before each refund the till pays, the priced basket: the store, the POS Profile, the company and the till's day and terminal, the cashier's user, the customer and their customer group, the price list, the currency and language, the coupons, the sale details (the ID check, and any other detail the sale carries, such as a prescription number at a pharmacy), each line's item, group, quantity, unit, amounts and details, the totals and the payment methods offered (on a refund also the original sale); never a card. The service reads the shop's settings, item groups and items with the integration user's key and writes nothing. The manifest names one address; the shop sees it, and each service's consent, on the install screen and on each service's own record before it is switched on.

What is in the folder

File What it is
manifest.json the record type PP AGE Settings (the record Default), a whole-number field on Item Group and on Item, the sale detail PP AGE ID Checked (never required), a feed registration naming the app's one integration user, a help address, and two services: a capture on scan filtered by the item field, and a check, both installed switched off; when the service is down the item goes on, or the sale is paid, flagged
agecheck.py the pure core: the signature check (verify), the shape checks, each item's age (age_of), the date of birth (born_by), the answers (on_scan, on_check) and the sales already asked about (Asked)
service/app.py the service, standard library only: POST /age/scan, POST /age/check, GET /health, and 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 over the wire with signed calls (one shop and many), the shop's reads against a stand-in shop, every answer 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/s37_age_check/tests -p 'test_*.py'
python3 samples/s37_age_check/tests/canaries.py
python3 samples/s37_age_check/service/app.py samples/s37_age_check/service/.env.local

Settings, one shop (service/.env.local, never committed): IVN_SCAN_SECRET (handed back at install as pp_age:sub:scan), IVN_CHECK_SECRET (pp_age:sub:check), IVN_SHOP_URL, IVN_SHOP_KEY (<api key>:<api secret> of the integration user), PORT. Many shops (the app template's handshake): IVN_LIBRARY_APP and a store (IVN_SHOPS_TABLE on CitiXsys hosting, else IVN_SHOPS_FILE); each shop hands over the two secrets when it connects and its integration user's key on Connect again; until the key arrives the service answers 503 and the down answers apply. AGE_PREFIX names the record type, the fields and the sale detail (default PP AGE).

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 AGE_PREFIX, so python3 tools/manifest/rename.py samples/s37_age_check/manifest.json --prefix "ACME AGE" renames the manifest and the hosting order, and the code needs no change for the name. A shop's ages 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