Actions and results
What each Point of Sale button and automatic behaviour does, with worked numbers and what the system refuses.
On this page (38)
POS setup (API)Registers (API)Till - WorkspaceTill - Open registerSwitch cashier (API)Register sessionsTill - Sell / Cash in-outCash movementsTill - Close sessionApprove close (API)SellingTill - SellSelling (API)Till - PaymentTill - ReceiptTill - Sell / ReturnReturnsVoid a bill (API)Till - Pop-upsSuspended sales (API)Suspended salesPromotionsCouponsLoyaltyGift cardsVouchersStore creditOffline sellingDevice enrolmentPOS dashboardPOS sales and returns listsExceptions and audit logDevice monitorPending transactionsProvider settlement (API)POS features and field settingsRegistersPlanned screens
POS setup (API)
| Action | When | What you do | What happens |
|---|---|---|---|
| Make the company able to sell (setup call) | pos.configure; a test company with no POS data | POST /pos/setup; then POST it again | First call creates 7 payment methods, 14 reason codes and profile RETAIL (tax-inclusive, blind close, line 10 %, bill 5 %, tolerance 5, approval above 50, safe drop 5,000, drawer max 10,000, denominations 1000 to 0.25). Second call creates nothing new (all counts 0) |
Registers (API)
| Action | When | What you do | What happens |
|---|---|---|---|
| Create a register | pos.configure; a profile exists | POST /pos/registers with code 'pos01', name 'Front counter' | Register POS01 (upper-cased), state closed, active, profile = oldest active profile; audit register.created. Registers screen lists it with Session '-' |
| Duplicate or empty register code | POS01 exists | Create again with code POS01; then with an empty code | 'A register with this code already exists.' and 'A register needs a code.' |
| Put a register in maintenance | POS01 closed | PATCH state = maintenance; try to open a session; then PATCH state = banana | Open refused: 'POS01 is maintenance and cannot be opened.'; banana -> 'banana is not a register state.' |
Till - Workspace
| Action | When | What you do | What happens |
|---|---|---|---|
| Open the till from the menu | Cashier with pos.view and pos.sell; POS01 exists | Point of Sale > Point of sale > Launch POS | Full-screen till opens over the ERP with the register card POS01; header ONLINE; 'Leave POS' returns to the ERP. 'Register workspace' and 'My session' open the same till |
| Bootstrap loads everything for offline use | POS01 exists, products with sale enabled | Press Open session on the card; then in the browser turn the network off and reload the till | Products, tenders, reason codes, promotions, profile and session are cached; the till still opens and shows OFFLINE (a fresh online load is needed after an offline browser restart) |
| Heartbeat and last seen | Open till | Check the register's last seen after loading the till | Last seen updates when the till loads (bootstrap) and when it syncs; the till never calls the heartbeat route |
Till - Open register
| Action | When | What you do | What happens |
|---|---|---|---|
| Open a session with a typed float | POS01 closed, no live session; profile RETAIL | Open session > type float 500.00 > Open session | Session numbered from the central sequence (e.g. S-10052); register state open; opening-float journal posted between cash drawer and cash-transfer accounts; cashier recorded; toast 'Session ... open' |
| Open a session with a note-and-coin count | As above | Count 500 x 1, 100 x 5, 50 x 2 and 20 x 5 (total 1,200.00); Open session | Counted opening cash 1,200.00 becomes the float; session opening_float 1,200.00 |
| Denomination count must equal the float (API) | As above | POST /pos/sessions with opening_float 1350.00 and denominations {500: 2, 100: 3} (counts to 1,300.00) | Refused: 'The denomination count comes to 1300.00, not 1350.00.' No session created |
| Fixed-float profile | Profile opening mode = fixed, float 500.00 | Open with 400.00; then with 500.00 | 400 refused: 'This register opens on a fixed float of 500.00.'; 500 opens |
| Negative float refused | As above | Open with float -5 | 'Opening float cannot be negative.' |
| One live session per register | POS01 has an open session | Try to open another from the workspace (button reads Continue) and by API | API: 'POS01 already has session S-10052 open.' (conflict); two simultaneous opens create exactly one session and one opening-float journal |
Switch cashier (API)
| Action | When | What you do | What happens |
|---|---|---|---|
| Hand the drawer to the next cashier | Open session; second user is a member of the company | POST /pos/sessions/{id}/cashier with that user's id | Session stays open with the same float; first cashier's row gets an end time and the second starts; session detail lists both with method 'pin'; audit session.cashier |
| Switch on a closed session | A closed session | Switch cashier | 'The session is not open.' |
Register sessions
| Action | When | What you do | What happens |
|---|---|---|---|
| Session list and detail | A few sessions exist | Point of Sale > Operations > Register sessions; compare with GET /pos/sessions/{id} | Newest first (max 200): Session, Register, Business date, State, Float, Counted, Variance. Detail adds totals and the cashier history |
Till - Sell / Cash in-out
| Action | When | What you do | What happens |
|---|---|---|---|
| Cash in | Open session, float 500.00 | F11 > Cash in > amount 100.00 > reason 'Change from bank' > Record | Toast 'Drawer now 600.00'; movement recorded (+100.00); opening journal-style transfer journal posted once; audit cash.in |
| Cash out | Drawer expected 600.00 | Cash out 50.00 reason 'Courier' | Movement stored as -50.00; drawer now 550.00; audit cash.out (shows in the exception view) |
| Safe drop | Drawer expected 1,250.00 | Safe drop 700.00 reason 'Bank deposit' | -700.00; drawer 550.00; safe_drop movement listed under Cash drops and in the exception view |
| Cannot take out more than the drawer holds | Drawer expected 1,100.50 | Cash out 1,200.00 | 'The drawer holds 1100.50, so 1200.00 cannot be taken out.' Nothing moved |
| Zero or unknown movement | Open session | Amount 0; then (API) kind 'steal' | 'Enter an amount greater than nothing.' and 'steal is not a cash movement.' |
| Retry of a cash movement does not move cash twice | A movement was sent with a uuid | Send the same uuid again (timeout retry) | Second call returns the first movement; drawer and journal unchanged |
| Cash movement made offline | Network off; open session | Record Cash in 20.00 | Toast 'Queued - will sync'; after reconnect it uploads as cash_movement, accepted; movement flagged offline |
Cash movements
| Action | When | What you do | What happens |
|---|---|---|---|
| Cash in/out and Cash drops lists | Movements of several kinds exist | Operations > Cash in / cash out; then Cash drops | First lists all kinds (200 rows, newest first); Cash drops shows only safe drops |
Till - Close session
| Action | When | What you do | What happens |
|---|---|---|---|
| Expected cash worked example | Float 500.00; cash sales (cash received less change) 1,250.50; cash in 100.00; cash out 50.00; safe drop 700.00 | Close (API closing figures with a non-blind profile) or read the close_totals on the session | Expected = 500.00 + 1,250.50 + 100.00 - 50.00 - 700.00 = 1,100.50. Card, QR, gift-card tenders add nothing |
| Blind close hides the expected figure | Profile blind close = Yes; open session | Close tab; read the screen and the closing route | Expected cash, cash sales and cash movements are empty (null) in the server answer, not just hidden on screen |
| Close inside tolerance | Expected 1,100.50; tolerance 5.00 | Count 1,098.00 (variance -2.50); no reason; Close session | Session closed; variance -2.50; register closed; posted to Finance once; toast 'closed - variance -2.50' |
| Close outside tolerance needs a reason | As above | Count 1,090.00 with no reason; then with reason 'Short change' | First: 'The count is -10.50 against what was expected. Give a reason before closing.'; second closes with variance -10.50 |
| Large variance waits for approval | Approval threshold 50.00 | Count 1,040.00 (variance -60.50) with a reason | Session state approval_pending; register close_pending_approval; no Finance posting yet; the register cannot open a new session meanwhile |
| Close with a negative or mismatching count | Open session | Counted cash -1; then typed 1,100.00 with a table counting 1,098.00 | 'Counted cash cannot be negative.' and 'The count comes to 1098.00, not 1100.00.' |
| Closing posts the session to Finance | A closed session with cash 1,000.00, card 63.00 and VAT | Open the session's journal in Finance | One balanced journal: cash drawer and card clearing debited by tenders, revenue and output VAT credited; posted_at and journal id on the session; closing again changes nothing |
| Close offline, then sync | Network off; open session | Close the session offline; reconnect | State closed_pending_sync while offline; the session_close item uploads, session becomes closed and is posted to Finance |
Approve close (API)
| Action | When | What you do | What happens |
|---|---|---|---|
| Second person approves | Session approval_pending; user with pos.variance.approve who did not count | POST approve-close granted = true | Session closed; approved by and at recorded; register closed; session posted to Finance |
| Counter cannot approve own close | As above, same user | approve-close as the user who counted | 'A close is approved by somebody other than the person who counted it.' |
| Refuse the variance | Session approval_pending | approve-close granted = false, reason 'Recount' | Session goes to variance_review, register 'closing'; cashier can count again with Close session and a new figure; audit session.close_refused |
Selling
| Action | When | What you do | What happens |
|---|---|---|---|
| Late offline bill after the session was posted | Session closed and posted; an offline bill from that session arrives | Sync the bill | Bill accepted into the closed session; supplemental journal posted once; expected cash on the closed session unchanged (frozen) |
Till - Sell
| Action | When | What you do | What happens |
|---|---|---|---|
| Scan a product by barcode | Open session; Milk barcode 6291000000011 (test) | Type the barcode, Enter | Milk 10.50 added qty 1; scanning again increases the quantity to 2 on the same line (only when price, unit and discount match) |
| Quantity multiplier 3*code | As above | Type 3*6291000000011, Enter | One line Milk qty 3, 31.50 |
| Packaging barcode | Milk has a 6-pack barcode, ratio 6, name 'Carton' | Scan the carton barcode | Line unit Carton, price 10.50 x 6 = 63.00, base quantity 6 (stock moves 6 units) |
| Scale barcode with embedded weight | Product with PLU 00123, prefix 21, mode weight, 3 decimals, price 8.00/kg | Scan 2100123012503 | Quantity 1.250 kg x 8.00 = 10.00 gross; line marked weighed from barcode_weight. A barcode with a wrong check digit is not treated as a scale barcode |
| Scale barcode with embedded price | Same product, mode price, 2 decimals | Scan 2100123010004 | Price 10.00, quantity 1, weight source barcode_price |
| Search and price check | Open session | F1 type 'milk', click the hit; F5 type 'bread' | F1 adds the product; F5 only shows the price. Up to 25 hits; no hit -> 'Nothing matches.' |
| Tax-inclusive line worked example | Profile tax-inclusive, VAT 5 % | Sell Milk x 1 for 10.50 | Net 10.00, VAT 0.50, total 10.50 (net = 10.50 / 1.05) |
| Three-line basket worked example | As above | Sell Milk x 2 (21.00), Bread x 1 (5.25), Chocolate x 1 (3.99) | Net 20.00 + 5.00 + 3.80 = 28.80; VAT 1.00 + 0.25 + 0.19 = 1.44; total 30.24 (VAT line on the till and on the receipt show 1.44) |
| Rounding to the nearest 5 fils | Profile cash rounding 0.05; the basket above (30.24) | Complete the sale | Rounding +0.01, TOTAL 30.25 (604.8 steps round to 605). A 10.12 basket rounds to 10.10 (rounding -0.02) |
| Line discount inside the limit | Rice 20.00 x 2; line limit 10 % | F4 discount 4.00 on the line (10 %) | Gross 40.00 - 4.00 = 36.00; net 34.29, VAT 1.71, total 36.00; sale completes with no approval |
| Line discount above the limit needs a manager | As above | Discount 4.01 (10.03 %) and finish | Server answers 409 'An independent manager must approve this discount.'; a pending approval and an audit row are kept; no sale, no stock move |
| Bill discount worked example | Basket 100.00 (net 95.24, VAT 4.76); bill limit 5 % | Bill disc. 5.00; then 5.01 | 5.00: payable 95.00 (exactly 5 %, allowed). 5.01: needs approval. Discount larger than the bill: 'A bill discount cannot exceed the bill.' |
| Discount larger than the line | Milk x 1 (10.50) | Line discount 11.00 (API) | 'A discount cannot exceed the line.' |
| Void a line and cancel the transaction | Cart with 3 lines | Select a line, Del; then Cancel Esc | Line removed; Cancel empties the cart and toast 'Transaction cancelled'; nothing is sent to the server |
| Price override | Profile allow price override Yes / No | Press Price, enter 9.50 on Milk | No: dialog says the profile does not allow overrides. Yes: price changes to 9.50 with no manager approval or reason asked |
| Customer on the bill | Online; customer 'Test Customer LLC' exists | F2 type 'Test', choose it; sell; open the bill | Customer stored on the bill; offline the search finds nobody and Walk-in is used |
| Keyboard shortcuts | Sell screen | F1, F2, F3, F4, F5, F6, F7, F8, F11, F12, Del, Esc, Ctrl+L, Ctrl+R; then F9 and F10 | Each opens its function. F9 and F10 go straight to a whole-balance Cash and Card tender |
| Arabic and right-to-left | Switch the ERP to Arabic | Open the till and the POS lists | Labels in Arabic as in the field list; amounts and layout correct |
Selling (API)
| Action | When | What you do | What happens |
|---|---|---|---|
| Server recalculates and checks the till's total | Open session | POST a bill whose lines come to 30.25 but total says 30.10 | 'The till says 30.10 and these lines come to 30.25. The bill was not recorded.' A difference of 0.01 is accepted |
| Refuse bad lines | Open session | Send: qty 0; negative price; archived product; unknown product; 501 lines | 'A line needs a quantity.'; 'A price cannot be negative.'; '<code> is archived and cannot be sold.'; 'Record not found.'; 'A bill takes at most 500 lines.' |
| Stock is taken in the same transaction | Stocked product with 5 units at the register warehouse | Sell 2; then try to sell 4 | First: stock 3, picking and valuation layer created, COGS/inventory journal balanced. Second refused 'Only 3.0000 of <code> is available at <warehouse>.' and no bill is created |
| Register without a warehouse | Register has no warehouse; stocked product | Sell it | 'Set a warehouse on this POS register before selling stocked goods.' |
| Session not open takes no bills | A cancelled or draft session id | Post a bill to it | 'Session ... is ... and takes no bills.' (Closed and approval-pending sessions still take late bills) |
| Same bill uuid sent twice | A bill was recorded | POST the identical bill again | Second answer returns the same bill id and number with duplicate = true; one bill, one stock move, one journal effect |
| Return with no original bill | Open session | POST kind return with no origin | Accepted as a blind return with no check on quantity or price |
Till - Payment
| Action | When | What you do | What happens |
|---|---|---|---|
| Cash sale with change | Basket 30.24 (no rounding) | PAYMENT F12 > quick cash 50 > Complete sale | Change 19.76 shown; bill paid 50.00, change 19.76; expected cash rises by 30.24 (50.00 - 19.76) |
| Exact card sale | Basket 63.00 | PAYMENT > Card > Complete sale | Tender 63.00 on CARD; no change; drawer expected cash does not change |
| Split cash and card | Basket 63.00 | Quick cash 40 then Card (takes the 23.00 balance) > Complete sale | Two tenders 40.00 CASH and 23.00 CARD; expected cash +40.00 only; close totals list both tenders |
| Underpayment refused | Basket 30.25 | (API) payments CASH 20.00 | 'The bill is 30.25 and the payments come to 20.00.' |
| Overpayment on a method that gives no change | Basket 30.24 | (API) single CARD tender of 40.00 | 'The overpayment has no cash tender that can give change.' When a CASH tender is also present the change is given on cash |
| Change sent by the till is checked | Basket 30.24, cash 50 | (API) stated change 25.00 | 'The stated change is greater than the payment overage.' |
| Online-only tenders when offline | Network off; payment screen | Open the payment panel | QR, gift card, voucher, store credit and customer account buttons are hidden; Cash and Card remain (Card is 'allowed with limit') |
| Payment method limits | Card max amount 5,000.00 | (API) CARD 6,000.00 | 'Card takes at most 5000.00.' |
| Card payment is manual | Card tender | Complete a card sale | Till records a card tender with no terminal call and no authorisation code; the payment-intent routes exist but the till does not use them |
Till - Receipt
| Action | When | What you do | What happens |
|---|---|---|---|
| Receipt screen after a sale | A completed sale | Look at the receipt panel | Receipt number POS01-YYYYMMDD-000nnn, Change, Recorded: 'Sent' (online) or 'Queued - will sync' (offline); New sale clears it |
| Print / reprint | A receipt on screen | Print / reprint (or Ctrl+R) | Browser print opens and 'Print recorded' (first) or 'Reprint recorded'; on a non-fiscal profile the till prints locally. On a fiscal profile it first checks legal issuance |
| Fiscal issuance on a fiscal profile | Profile fiscal_required on; adapter registered | Sell online | Bill goes straight to the server (never queued); fiscal status accepted with a Legal ID; rejected or unknown shows an exception and the receipt is not printable as compliant |
| Printer failure keeps the legal identity | Accepted fiscal receipt | Record a print as failed, then printed (same legal id, new key) | First attempt 'original' failed; second is a 'reprint' with the same legal id; a repeat of the same key changes nothing; unissued receipt -> 'The receipt is not legally issued and cannot be printed as compliant.' |
| Receipt number format | Register code POS01, business date 2 Oct 2026 | Sell three bills | POS01-20261002-000001, -000002, -000003; the counter is the device's own and restarts on a new date |
Till - Sell / Return
| Action | When | What you do | What happens |
|---|---|---|---|
| Return a whole bill | Bill with Milk x 2 = 21.00 paid in cash | F8 > receipt number > Find > Return these > pay > Complete | Return bill total -21.00; cash tender -21.00 so expected cash falls by 21.00; stock goes back at the original cost; journal negative |
| Partial returns cannot exceed what was sold | Bill with Milk x 3 | Return 1 (API with origin_line), then 2, then 1 more | First two succeed (remaining 0); third refused 'Return quantity exceeds what remains on the original sale.' |
| Return reuses the original price and discount | Original: Rice 20.00 x 2 with 4.00 line discount; promo no longer active | Return 1 unit | Refund 18.00 (half of 36.00), promotion not re-applied; net 17.14, VAT 0.86 |
Returns
| Action | When | What you do | What happens |
|---|---|---|---|
| Returns list | Returns exist | Operations > Returns; Transactions > POS returns | Both list kind = return bills with negative totals |
Void a bill (API)
| Action | When | What you do | What happens |
|---|---|---|---|
| Void a bill with a manager | User with pos.void; manager id; bill in a posted or open session | POST /pos/sales/{id}/void with reason 'Scanned twice' and approver | Bill state voided (never deleted); approval row granted; stock reversed; expected cash falls by the cash taken; in a posted session a supplemental reversing journal is posted |
| Void refusals | A bill | No reason; no approver; void twice; void a coupon / loyalty / gift-card bill | 'A void needs a reason.'; 'A void after the sale needs a manager's approval.'; 'This bill is already void.'; 'Coupon, loyalty and stored-value sales must be reversed with an original-sale return, not a void.' |
Till - Pop-ups
| Action | When | What you do | What happens |
|---|---|---|---|
| Hold a bill | Cart with Milk x 2; Suspended sales switch on | F6; then add another sale; F7 | Cart clears, toast 'Bill held - F7 to recall'; stock for the lines is reserved for 30 minutes; F7 lists the held bill |
| Recall a held bill | A held bill | F7 > Recall | Cart restored; hold consumed once; a second recall is refused ('This held sale is no longer active.') |
| Held stock cannot be sold from under the hold | 1 unit left; held on till 1 | Sell the last unit on till 2 | Refused: 'Only 0 of <code> is available ...' (the hold owns it); completing the held bill on till 1 works |
Suspended sales (API)
| Action | When | What you do | What happens |
|---|---|---|---|
| Cancel or expire a hold | A hold; wait past 30 minutes or DELETE it | Cancel one; let another expire (scheduler) | Reservation released in both cases; state cancelled / expired; recalling an expired hold -> 'This held sale's stock reservation has expired.' |
| Empty or invalid hold | Open session | Hold an empty cart; a line with quantity 0 | 'There is nothing to hold.' and 'A held line needs a positive quantity.' |
Suspended sales
| Action | When | What you do | What happens |
|---|---|---|---|
| Menu entry shows 'Not built yet' | Menu Operations > Suspended sales | Open it | Planned placeholder appears although the backend and the till hold/recall work |
Promotions
| Action | When | What you do | What happens |
|---|---|---|---|
| Percentage promotion | Promotion MILK3: 10 %, product Milk, min quantity 3 | Sell Milk x 3 (31.50) | Promotion 3.15; line 28.35; net 27.00; VAT 1.35; total 28.35. Milk x 2 gets nothing |
| Special-price promotion | Special price 8.00 on Milk, min quantity 2 | Sell Milk x 2 (21.00) | Discount 21.00 - 2 x 8.00 = 5.00; line 16.00; net 15.24, VAT 0.76 |
| Fixed-amount promotion | Fixed 2.00 on category Beverages | Sell 1 x 1.50 drink and 3 x 10.50 drink | 2.00 is taken once per line, never more than the line (1.50 line -> 1.50 discount) |
| Priority and stacking | Rule A 10 % priority 10 not stackable; rule B 5 % priority 5 | Sell a matching 100.00 line | Only A applies (10.00). With A stackable: A 10.00 then B 5 % of the remaining 90.00 = 4.50 (total 14.50); codes joined 'A+B' |
| Validity window, weekday and time | Rule 16:00-18:00 on Friday only | Sell at 17:00 Friday, 17:00 Saturday, 19:00 Friday | Applies only on the first. Test around midnight and early evening because the server uses the UTC sale time |
| Offline eligibility | Rule with Allowed offline off | Sell offline | Rule ignored on the offline bill, till and server agree on the total |
| Server wins over the till | A rule the till does not know yet | Sell with a stale cache | Server recalculates; if its total differs the bill is refused with the 'The till says ...' message |
| Create and edit a promotion | Company admin | Pricing & promotions > Promotions > New promotion; fill Code, Value; Save; Edit; Discard | Saved record opens view-first; Edit/Save/Discard work; duplicate code 'Promotion code already exists.'; value 150 % -> 'Enter a valid promotion value.' |
Coupons
| Action | When | What you do | What happens |
|---|---|---|---|
| Percentage coupon | Coupon WELCOME10: 10 %, min basket 50.00 | Online sale 120.00 (net 114.29, VAT 5.71); F9 Coupon WELCOME10 | Discount 12.00; payable 108.00; coupon redemption counted once; code shown in totals |
| Fixed coupon larger than the basket | Fixed 20.00, min basket 10.00 | Basket 15.00 | Discount limited to 15.00; payable 0.00 |
| Coupon refusals | Coupons with limits | Unknown code; expired; basket below minimum; used up; second use by the same customer; no customer when limit set; offline; on a return | 'Coupon is not valid.'; 'Coupon is outside its validity period.'; 'Coupon needs a basket of at least 50.00.'; 'Coupon has reached its redemption limit.'; 'This customer has already used the coupon.'; 'This coupon requires a customer.'; 'Coupons must be checked online.'; 'Coupons can only be redeemed on a sale.' |
| Two tills redeem the last use together | Coupon max redemptions 1 | Submit two sales with the coupon at the same moment | One is accepted, the other refused (row-locked); redemption count 1 |
| Create and edit a coupon | Company admin | Pricing > Coupons > New coupon; fill Code, Value, Max redemptions 100; Save | Saved; Usage column shows 0/100; Allowed offline is locked off; field rule for End date (Features > Fields) is respected |
| Return of a coupon sale | A bill with 12.00 coupon discount on 120.00; return half | Linked return of half the lines | Coupon value comes back in proportion (6.00); no new coupon is redeemed |
Loyalty
| Action | When | What you do | What happens |
|---|---|---|---|
| Earn points | Programme: 1 point per AED, 0.01 AED per point, min 100; customer chosen | Sell 105.00 online | Customer earns 105 points (value 1.05); sale shows loyalty earned 105; walk-in sales earn nothing |
| Redeem points | Customer balance 600; payable 105.00 | F10 Loyalty > 500 > Redeem > pay | Discount 5.00; payable 100.00; balance 100 + points earned on the new total (100) = 200; FIFO lots consumed oldest first |
| Redeem more than the bill | Payable 3.00; balance 600 | Redeem 500 | Discount capped at 3.00; only 300 points used |
| Redeem refusals | As above | 50 points; 5,000 points; offline; no customer; on a return | 'Enter enough points for the minimum redemption.'; 'Loyalty points balance is insufficient.'; 'Loyalty points must be checked online.'; 'Choose a customer to redeem loyalty points.'; 'Loyalty redemption needs a customer sale.' |
| Return reverses earned points | Bill 200.00 earned 200, redeemed 0; return 50.00 (ratio 0.25) | Linked return of 50.00 | Points reversed 50; balance falls by 50. If points were redeemed on the bill the same ratio is restored with the original expiry |
| Points expiry run | Programme with expiry days 30; old points | Loyalty > Run expiry (twice) | Toast 'Expired N points across M accounts'; unused points only; second run expires 0 (idempotent); liability released to breakage income |
| One active programme | Programmes A (active) and B | Save B as Active | A becomes inactive automatically |
| Create and edit a programme | Company admin | Loyalty > New program; code STD, 1 point per AED, 0.01 per point, minimum 100; Save; Edit | Saved; duplicate code 'Loyalty program code already exists.'; negative rates -> 'Loyalty rates and limits cannot be negative.' |
Gift cards
| Action | When | What you do | What happens |
|---|---|---|---|
| Issue a gift card | Company admin; online | Gift cards > Issue > value 100.00 > Issue | Banner shows the full token once ('Copy this token now...'); list shows masked code like GC-XXXX-1A2B, balance AED 100.00; issue liability journal posted |
| Spend a gift card on a sale | Card with 100.00; basket 80.00 | Payment > Gift card > paste token > Apply > Complete | Card balance 20.00; liability reduces; tender recorded with the masked reference |
| Spend more than the balance | Balance 20.00 | Pay 30.00 with the card | 'Stored-value balance is insufficient.' and no bill |
| Two simultaneous spends cannot overdraw | Balance 100.00 | Two sales of 80.00 at the same moment | One accepted, one refused; balance 20.00 |
| Recharge, expire, reverse | Active card | Manage > Recharge 50.00; Expire / breakage; Reverse a movement | Balance +50.00 with a liability journal; Expire sets balance to 0 and posts breakage; Reverse of a sale-funded movement -> 'Reverse sale-funded stored value through the original sale return.' |
| Expired or wrong card | Expired card | Use it; use a voucher token on the gift-card method | 'This stored-value instrument has expired.' and 'The token does not match this payment method.' |
| Gift card tender on a void or offline bill | A gift-card sale | Void it; try the card offline | Void refused (use a return); offline -> 'Gift cards and store credit must be checked online.' |
Vouchers
| Action | When | What you do | What happens |
|---|---|---|---|
| Partial-use voucher | Voucher 50.00 | Spend 30.00, then 20.00, then 5.00 | Balance 20.00 then 0.00; third refused as insufficient |
Store credit
| Action | When | What you do | What happens |
|---|---|---|---|
| Store credit belongs to one customer | Credit issued to customer A | Issue without a customer; spend it on customer B's bill | 'Store credit belongs to a customer.'; 'This store credit belongs to another customer.' |
Offline selling
| Action | When | What you do | What happens |
|---|---|---|---|
| Sell with the network off | Open session, products cached; network off | Sell Milk x 1 for cash | Receipt shows 'Queued - will sync'; header OFFLINE with Pending 1; receipt number minted on the till; bill stored in the outbox with its uuid |
| Automatic sync on reconnect | Pending bills in the outbox | Turn the network on; wait up to 20 seconds or press Sync now | Toast 'Synced N'; pending 0; bills appear in POS sales flagged Offline = Yes; stock and Finance effects happen once |
| Duplicate protection on sync | A bill already accepted | Replay the same upload (or cut the line during an upload and retry) | Answered 'duplicate'; one bill; no second stock move or journal |
| Batch upload is partial | Batch of 3: good, bad (unknown product), good | POST /pos/sync | accepted 2, failed 1; the bad one stays in the outbox as failed with its reason and the payload; the good ones are saved |
| Upload order | Batch sent as: sale, session_open, cash_movement, session_close | Upload | Applied as session_open, cash_movement, sale, session_close regardless of the order sent |
| Offline bill refused when the profile says online only | Profile offline selling = online_only | Try to sell offline | Till says 'This operation requires a connection.'; server: 'This register is not allowed to sell offline.' |
| Offline time limit | Profile offline max minutes 30; register last contact 31 minutes ago | Sell an offline bill | Refused 'This register has been offline for more than 30 minutes. Restore the connection before selling again.'; changing the PC clock does not help (server clock) |
| Offline amount limit | Offline max amount 2,000.00; 1,990.00 already queued | Sell 20.00 offline | Till: 'This sale exceeds the approved offline amount. Reconnect first.'; server: 'This sale would take the register to 2010.00 offline, which is its limit of 2000.00. Sync before selling again.' |
| Online-only tender on an offline bill | Offline | (API) offline bill paid by QR | 'QR / wallet must be authorized online.' no bill, no stock |
| Coupons, loyalty and gift cards need a connection | Offline | Press Coupon; try Loyalty; pay by gift card | 'Coupons require an online check'; loyalty needs online + customer; gift-card button hidden |
| Hold made offline recalls after sync | Offline hold; reconnect | Hold offline, reconnect, recall on another till | Hold uploaded before the recall; other till can recall once; recall offline is queued as hold_recall |
| Open a session offline | Signed or unsigned device, network off | Open session; reconnect | session_open uploads; if the server already has a live session it answers with that one (no second drawer) |
| Receipt numbering offline | Network off | Sell 3 bills | POS01-<date>-000001..3 made on the till; unique per register; after sync the same numbers show in the journal |
| Upload batch limits and bad items | A batch | 201 items; an item with no uuid; an unknown entity | 'Send at most 200 items at a time.'; item failed 'The item carries no identifier.'; '<entity> is not something a till uploads.' |
Device enrolment
| Action | When | What you do | What happens |
|---|---|---|---|
| Secure this device | pos.sync.manage; register with no device | Workspace > Secure this device | Key pair made in the browser, device POS-<uuid> enrolled, register marked 'require signed sync'; Devices list shows state active and last sequence 0 |
| Signed items advance the sequence | Enrolled device with 3 queued bills | Sync | Each item signed (sequence 1,2,3); device last_sequence 3; exact resend is 'duplicate' |
| Changed payload on a used identity is quarantined | Enrolled device; sequence 4 already used | Resend sequence 4 with a different amount (or reuse the uuid with another payload) | Status 'quarantined' with a conflict id; the original sale is unchanged; Sync conflicts list shows reason sequence_reuse / uuid_reuse |
| Sequence rollback | Last sequence 10 | Send sequence 7 with a new uuid | Quarantined (sequence_rollback) |
| Unsigned browser on an enrolled register | Register has an enrolled device | Open a session or sync from another browser with no key | 'This register requires an enrolled signed device.'; opening needs the signed command |
| Revoke a device | Active device | POST /pos/devices/{id}/revoke; then sync from it | State revoked; new uploads refused 'This device credential has been revoked.'; earlier bills and conflicts stay; revoking again does nothing; the register stays signed-only |
| Bad enrolment input | pos.sync.manage | No register; no device; bad key; re-enrol active device with new key | 'Choose the register this device belongs to.'; 'Give the device a stable identifier.'; 'The Ed25519 public key is not valid.'; 'Revoke the active device before changing its key or register.' |
POS dashboard
| Action | When | What you do | What happens |
|---|---|---|---|
| Dashboard figures | Today: 3 sales of 105.00, 1 return of 10.50, 1 offline sale, 1 open session | Open the dashboard | Sales today 315.00, Transactions 3, Average basket 105.00, Returns -10.50 (returns are stored negative), Offline sales 1, Open sessions 1; voided and cancelled bills are excluded |
POS sales and returns lists
| Action | When | What you do | What happens |
|---|---|---|---|
| Journal filters | Sales, returns, voids, offline bills exist | Open POS sales, POS returns, Voids, Cancelled bills, Offline audit | Each shows the matching kind/state/offline set (100 rows max); columns Receipt, Sold at, Register, Kind, State, Offline, Total |
Exceptions and audit log
| Action | When | What you do | What happens |
|---|---|---|---|
| Exception view | Voids, closes refused, offline bills, cash outs and drops exist | Transactions > Exception transactions; compare with Administration > POS audit log | Exceptions list only offline items, sale.voided, session.close_refused, price/discount overrides, cash.out, cash.safe_drop; the audit log lists everything |
Device monitor
| Action | When | What you do | What happens |
|---|---|---|---|
| Monitor every till | Two registers, one with 2 queued items | Hardware > Device monitor | One row per active register: state, pending 2, offline sales on the live session, last sync; needs pos.sync.manage |
Pending transactions
| Action | When | What you do | What happens |
|---|---|---|---|
| Pending and failed list | Items pending and one failed | Synchronisation > Pending transactions and Failed sync | Both menus open the same list (state, tries, error); count is the full count even if only 200 rows show |
Provider settlement (API)
| Action | When | What you do | What happens |
|---|---|---|---|
| Reconcile a card batch | Provider adapter and transactions exist; pos.configure | Record capture 105.00 and refund 5.00; reconcile with deposit 98.00, fees 2.00, expected 100.00 | Net 100.00 = 98.00 bank + 2.00 fee; one balanced journal (bank, fee expense, clearing); posting once even if repeated |
| Reconciliation refusals | As above | Deposit 97.00; a reference twice; a transaction already settled; different currency | 'Bank deposit plus provider fees must equal captures less refunds.'; 'Send each provider transaction reference exactly once.'; 'A provider transaction is already in another settlement.'; 'All provider transactions must use the settlement currency.' |
| Card data never accepted | pos.sell / pos.configure | Send a body containing 'cvv' or 'pan' | 'Never send or store PAN, CVV, PIN or card track data.' |
| Unknown card outcome is queried, not recaptured | Adapter that times out | Create a payment intent; resolve it | State unknown (no second capture); resolve queries the provider and ends captured or failed; a bill can only use a captured intent whose amount equals the tender |
POS features and field settings
| Action | When | What you do | What happens |
|---|---|---|---|
| Turn off Promotions and coupons | Company admin | Applications > Point of Sale > Features: switch off Promotions and coupons | Menu items Promotions, Coupons and POS price rules disappear; new or changed promotions/coupons are refused (capability_disabled); lists still readable; 'Selling' and 'Offline' cannot be switched off |
| Switch off loyalty / suspended sales / card terminals | As above | Switch each off in turn | Loyalty, gift cards, vouchers, store credit menus go; Suspended sales menu goes and hold routes are refused; card terminal routes (intents, settlements) are refused; reads still work |
| Coupon end date rule | Features > Fields: Coupon end date | Set Required, then Hidden, then Read only; open a coupon | Required: asterisk and cannot save without an end; Hidden: box gone and an end sent on a new coupon is refused; stored value stays visible and locked |
| Gift card expiry rule | Fields: Gift card and voucher expiry | Set Required then Hidden; open Issue | Required: 'Expires at *' needed; Hidden: box not shown and an expiry sent is refused |
Registers
| Action | When | What you do | What happens |
|---|---|---|---|
| Registers, profiles, tenders and reasons are read-only lists | Any POS user | Open POS setup > Registers, POS profiles, Payment methods and Administration > Reason codes | Lists only: no New or Edit button on any of the four; settings can only be changed by API or direct data change |
Planned screens
| Action | When | What you do | What happens |
|---|---|---|---|
| Not-built screens tell the truth | Any POS user | Open Cashier shifts, Reprints, Return policies, Sales reports and Alert rules | Each shows 'Not built yet', its purpose and 'What is missing', with a disabled button; none shows an empty table or a working-looking button |