Several till rules in one app, with the shop's own settings (till guard rules) — the recipe
A shop rarely wants one rule at the till. It wants a handful: a manager for a large sale, no line at a placeholder price, a most-per-line quantity, a beep on a flagged item. This recipe puts them in one app: a check before payment for the rules at Pay, a capture on scan for the rule at the scan, and one settings record the shop edits in Desk, which your service reads. It is for an iVendNext Partner, or the CitiXsys Consulting Team.
Reference implementation: samples/s23_till_guard/ — Till guard rules.
When to use it. Your rules need numbers or choices that differ from shop to shop, and one service answers many shops. For one rule with fixed settings, the check alone is enough (recipe); for a value captured at the scan, the capture (recipe).
Steps
-
Declare the two services and the settings in your manifest (
samples/s23_till_guard/manifest.json):- a record type
<PREFIX> Settings(an ordinary record type; the service reads the record namedDefault, guide chapter 7). Give System Manager the rights to edit it, and your integration role read only. The install makes that role when the shop lacks it only if its name starts with your prefix (on a site whose POS app is ate149dce4or later): the sample's isPP TGR Integration. Name yours<PREFIX> Integration, and say in your card's setup steps who gives it to the app's integration user; - a Check field on Item (Confirm at the till when scanned), and the capture's filter on it:
"settings": {"event": "scan", "item_field": "<prefix>_confirm_at_scan"}. Online, the server reads the tick at each scan and calls you only for a ticked item; offline you are not called, and the till applies your down answer to the items ticked at its start of day (at most 2,000); - a
checksubscription and acapturesubscription, each at its own address; - a feed registration (
registered_applications) naming your one integration user, so the card says Needs 1 Desk user, and so the shop's Connect again hands that user's key to your service; - a help address on the settings record type (a
documentationproperty setter), so Desk's help on the record opens your page at its settings.
- a record type
-
Choose what happens when your service is down. Till guard rules declares
allow_and_flagfor both: an outage never stops a shop selling, and each sale is flagged on Sales to review. A shop that wants a rule to hold in an outage changes its subscription torefuse(the check's three down answers are in the check recipe). -
Check every call with its own route's secret. The install makes one secret per service,
<app>:sub:<key>. The check's address accepts only the check's secret, the scan's only the scan's (service/app.py,ROUTES). -
Read the shop's settings with your integration user's key, and keep them a minute.
GET /api/resource/<PREFIX> Settings/Default. No record yet: every rule off. The shop cannot be read: use the last rules you read for a short while (S23: ten minutes, kept by each running copy of the service, so a copy that has just started has none), then answer 503 so the till applies your down answer. Never follow a redirect with the key. -
Answer. At Pay, the first refusal wins (
refuse+reason); then a large sale getsask(a yes/no; S23's default, it needs nothing more) orapprove(a manager who is not the cashier signs in). At the scan,confirm+question: the till beeps and asks. What a sale or a return takes back is never judged; what a return sells (an exchange rung without its sale: lines with a positive quantity) is judged by the line rules, and the large-sale rule reads the total the customer pays (on an exchange, net of what it takes back), so an exchange cannot dodge it. On a return the till's headline is This refund can't go ahead. Every sentence is plain text of 1 to 140 characters.Rule Answer a line priced above 0 and at or below the shop's placeholder price {"verdict": "refuse", "reason": "Line 2: PEN-01 is priced 0.01 AED. Correct its price before the sale is paid."}a line holding more than the shop's most (line by line: a promotion may split one item across lines) {"verdict": "refuse", "reason": "Line 1: 60 of WATER-24. The most on one line is 50."}a sale at or above the approval amount (the shop's default, ask){"verdict": "ask", "question": "This sale is 5,000.00 AED or more. Has a manager agreed to it?"}the same, when the shop chose approve{"verdict": "approve", "reason": "Sales of 5,000.00 AED or more need a manager's approval."}a flagged item scanned {"verdict": "confirm", "question": "PEN-01: Check this item before it goes on the sale. Add it?"} -
Tell the shop what to do (the sample's README, and your card's setup steps): after the install, make the
Defaultrecord, tick the items, make the integration user, give it the role the install added, make its key and choose the user on the feed registration, then consent to and switch on each service. Forapprove: first give each manager May Approve a Partner's Check on their Retail Permission and write access to Retail Override Request (the check refuses the next tick until someone other than the cashier can approve), then tick May ask for a manager's approval on the check. Without the tick, anapproveanswer cannot be used and the till applies your down answer. -
Test it. The sample's tests: each rule, each route's secret, the record read and kept, one shop and many, every answer against the contracts;
canaries.pybreaks each guarded rule and expects its test to fail. Then press Pay and scan on a sandbox.
Settings
The shop's record Default of <PREFIX> Settings, each rule off until set (0 or blank = off): a line priced at or below this is refused (a placeholder price such as 0.01; a free line is never refused), most of one item on a line, sales of this amount or more need a manager and how a large sale is checked (ask or approve), and the question on a flagged item. Amounts are one number for every currency, read in the sale's own currency. A change reaches the service within a minute.
What the shop needs from you
| You give | The shop checks |
|---|---|
| an HTTPS address its server trusts, per service | the address is consented; a private address is refused on a customer's site |
| a settings record type and a field, by manifest | the installer makes them and removes them at uninstall |
| a signature check per route | — (yours to do) |
| an answer within the timeout (3 s at Pay, 1.5 s at the scan) | your declared down answer applies otherwise |
Technical notes
- Offline, the till does not call you: each service's declared down answer applies, and the sale is flagged when it syncs.
- Hosting: the service is the app template's shape, so it serves many shops through the template's handshake (guide chapter 14b, One app, many shops); each shop's site hands over the two secrets when it connects after the install, and its integration user's key on Connect again. It only answers presses, so on CitiXsys hosting it runs as a function, paid per call (
app.json"runs": "function"). Until a shop's user key arrives the service answers 503, so your down answer applies.
Needs this serves
- A discount above a set limit needs a manager's approval — Ready, with limits (2)
- A manager must approve a sale over a set amount — Ready, with limits (2)
- No line at a placeholder price such as 0.01 — Ready, with limits (1)
- No more than 50 of one item on a line — Ready, with limits (1)
- No more than so many manager discount approvals per store per day — Ready, with limits (3)
- Selling below the minimum price needs a manager's approval — Ready, with limits (2)