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)

ActionWhenWhat you doWhat happens
Make the company able to sell (setup call)pos.configure; a test company with no POS dataPOST /pos/setup; then POST it againFirst 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)

ActionWhenWhat you doWhat happens
Create a registerpos.configure; a profile existsPOST /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 codePOS01 existsCreate 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 maintenancePOS01 closedPATCH state = maintenance; try to open a session; then PATCH state = bananaOpen refused: 'POS01 is maintenance and cannot be opened.'; banana -> 'banana is not a register state.'

Till - Workspace

ActionWhenWhat you doWhat happens
Open the till from the menuCashier with pos.view and pos.sell; POS01 existsPoint of Sale > Point of sale > Launch POSFull-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 usePOS01 exists, products with sale enabledPress Open session on the card; then in the browser turn the network off and reload the tillProducts, 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 seenOpen tillCheck the register's last seen after loading the tillLast seen updates when the till loads (bootstrap) and when it syncs; the till never calls the heartbeat route

Till - Open register

ActionWhenWhat you doWhat happens
Open a session with a typed floatPOS01 closed, no live session; profile RETAILOpen session > type float 500.00 > Open sessionSession 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 countAs aboveCount 500 x 1, 100 x 5, 50 x 2 and 20 x 5 (total 1,200.00); Open sessionCounted opening cash 1,200.00 becomes the float; session opening_float 1,200.00
Denomination count must equal the float (API)As abovePOST /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 profileProfile opening mode = fixed, float 500.00Open with 400.00; then with 500.00400 refused: 'This register opens on a fixed float of 500.00.'; 500 opens
Negative float refusedAs aboveOpen with float -5'Opening float cannot be negative.'
One live session per registerPOS01 has an open sessionTry to open another from the workspace (button reads Continue) and by APIAPI: 'POS01 already has session S-10052 open.' (conflict); two simultaneous opens create exactly one session and one opening-float journal

Switch cashier (API)

ActionWhenWhat you doWhat happens
Hand the drawer to the next cashierOpen session; second user is a member of the companyPOST /pos/sessions/{id}/cashier with that user's idSession 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 sessionA closed sessionSwitch cashier'The session is not open.'

Register sessions

ActionWhenWhat you doWhat happens
Session list and detailA few sessions existPoint 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

ActionWhenWhat you doWhat happens
Cash inOpen session, float 500.00F11 > Cash in > amount 100.00 > reason 'Change from bank' > RecordToast 'Drawer now 600.00'; movement recorded (+100.00); opening journal-style transfer journal posted once; audit cash.in
Cash outDrawer expected 600.00Cash out 50.00 reason 'Courier'Movement stored as -50.00; drawer now 550.00; audit cash.out (shows in the exception view)
Safe dropDrawer expected 1,250.00Safe 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 holdsDrawer expected 1,100.50Cash out 1,200.00'The drawer holds 1100.50, so 1200.00 cannot be taken out.' Nothing moved
Zero or unknown movementOpen sessionAmount 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 twiceA movement was sent with a uuidSend the same uuid again (timeout retry)Second call returns the first movement; drawer and journal unchanged
Cash movement made offlineNetwork off; open sessionRecord Cash in 20.00Toast 'Queued - will sync'; after reconnect it uploads as cash_movement, accepted; movement flagged offline

Cash movements

ActionWhenWhat you doWhat happens
Cash in/out and Cash drops listsMovements of several kinds existOperations > Cash in / cash out; then Cash dropsFirst lists all kinds (200 rows, newest first); Cash drops shows only safe drops

Till - Close session

ActionWhenWhat you doWhat happens
Expected cash worked exampleFloat 500.00; cash sales (cash received less change) 1,250.50; cash in 100.00; cash out 50.00; safe drop 700.00Close (API closing figures with a non-blind profile) or read the close_totals on the sessionExpected = 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 figureProfile blind close = Yes; open sessionClose tab; read the screen and the closing routeExpected cash, cash sales and cash movements are empty (null) in the server answer, not just hidden on screen
Close inside toleranceExpected 1,100.50; tolerance 5.00Count 1,098.00 (variance -2.50); no reason; Close sessionSession closed; variance -2.50; register closed; posted to Finance once; toast 'closed - variance -2.50'
Close outside tolerance needs a reasonAs aboveCount 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 approvalApproval threshold 50.00Count 1,040.00 (variance -60.50) with a reasonSession 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 countOpen sessionCounted 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 FinanceA closed session with cash 1,000.00, card 63.00 and VATOpen the session's journal in FinanceOne 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 syncNetwork off; open sessionClose the session offline; reconnectState closed_pending_sync while offline; the session_close item uploads, session becomes closed and is posted to Finance

Approve close (API)

ActionWhenWhat you doWhat happens
Second person approvesSession approval_pending; user with pos.variance.approve who did not countPOST approve-close granted = trueSession closed; approved by and at recorded; register closed; session posted to Finance
Counter cannot approve own closeAs above, same userapprove-close as the user who counted'A close is approved by somebody other than the person who counted it.'
Refuse the varianceSession approval_pendingapprove-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

ActionWhenWhat you doWhat happens
Late offline bill after the session was postedSession closed and posted; an offline bill from that session arrivesSync the billBill accepted into the closed session; supplemental journal posted once; expected cash on the closed session unchanged (frozen)

Till - Sell

ActionWhenWhat you doWhat happens
Scan a product by barcodeOpen session; Milk barcode 6291000000011 (test)Type the barcode, EnterMilk 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*codeAs aboveType 3*6291000000011, EnterOne line Milk qty 3, 31.50
Packaging barcodeMilk has a 6-pack barcode, ratio 6, name 'Carton'Scan the carton barcodeLine unit Carton, price 10.50 x 6 = 63.00, base quantity 6 (stock moves 6 units)
Scale barcode with embedded weightProduct with PLU 00123, prefix 21, mode weight, 3 decimals, price 8.00/kgScan 2100123012503Quantity 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 priceSame product, mode price, 2 decimalsScan 2100123010004Price 10.00, quantity 1, weight source barcode_price
Search and price checkOpen sessionF1 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 exampleProfile tax-inclusive, VAT 5 %Sell Milk x 1 for 10.50Net 10.00, VAT 0.50, total 10.50 (net = 10.50 / 1.05)
Three-line basket worked exampleAs aboveSell 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 filsProfile cash rounding 0.05; the basket above (30.24)Complete the saleRounding +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 limitRice 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 managerAs aboveDiscount 4.01 (10.03 %) and finishServer 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 exampleBasket 100.00 (net 95.24, VAT 4.76); bill limit 5 %Bill disc. 5.00; then 5.015.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 lineMilk x 1 (10.50)Line discount 11.00 (API)'A discount cannot exceed the line.'
Void a line and cancel the transactionCart with 3 linesSelect a line, Del; then Cancel EscLine removed; Cancel empties the cart and toast 'Transaction cancelled'; nothing is sent to the server
Price overrideProfile allow price override Yes / NoPress Price, enter 9.50 on MilkNo: dialog says the profile does not allow overrides. Yes: price changes to 9.50 with no manager approval or reason asked
Customer on the billOnline; customer 'Test Customer LLC' existsF2 type 'Test', choose it; sell; open the billCustomer stored on the bill; offline the search finds nobody and Walk-in is used
Keyboard shortcutsSell screenF1, F2, F3, F4, F5, F6, F7, F8, F11, F12, Del, Esc, Ctrl+L, Ctrl+R; then F9 and F10Each opens its function. F9 and F10 go straight to a whole-balance Cash and Card tender
Arabic and right-to-leftSwitch the ERP to ArabicOpen the till and the POS listsLabels in Arabic as in the field list; amounts and layout correct

Selling (API)

ActionWhenWhat you doWhat happens
Server recalculates and checks the till's totalOpen sessionPOST 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 linesOpen sessionSend: 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 transactionStocked product with 5 units at the register warehouseSell 2; then try to sell 4First: 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 warehouseRegister has no warehouse; stocked productSell it'Set a warehouse on this POS register before selling stocked goods.'
Session not open takes no billsA cancelled or draft session idPost a bill to it'Session ... is ... and takes no bills.' (Closed and approval-pending sessions still take late bills)
Same bill uuid sent twiceA bill was recordedPOST the identical bill againSecond answer returns the same bill id and number with duplicate = true; one bill, one stock move, one journal effect
Return with no original billOpen sessionPOST kind return with no originAccepted as a blind return with no check on quantity or price

Till - Payment

ActionWhenWhat you doWhat happens
Cash sale with changeBasket 30.24 (no rounding)PAYMENT F12 > quick cash 50 > Complete saleChange 19.76 shown; bill paid 50.00, change 19.76; expected cash rises by 30.24 (50.00 - 19.76)
Exact card saleBasket 63.00PAYMENT > Card > Complete saleTender 63.00 on CARD; no change; drawer expected cash does not change
Split cash and cardBasket 63.00Quick cash 40 then Card (takes the 23.00 balance) > Complete saleTwo tenders 40.00 CASH and 23.00 CARD; expected cash +40.00 only; close totals list both tenders
Underpayment refusedBasket 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 changeBasket 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 checkedBasket 30.24, cash 50(API) stated change 25.00'The stated change is greater than the payment overage.'
Online-only tenders when offlineNetwork off; payment screenOpen the payment panelQR, gift card, voucher, store credit and customer account buttons are hidden; Cash and Card remain (Card is 'allowed with limit')
Payment method limitsCard max amount 5,000.00(API) CARD 6,000.00'Card takes at most 5000.00.'
Card payment is manualCard tenderComplete a card saleTill 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

ActionWhenWhat you doWhat happens
Receipt screen after a saleA completed saleLook at the receipt panelReceipt number POS01-YYYYMMDD-000nnn, Change, Recorded: 'Sent' (online) or 'Queued - will sync' (offline); New sale clears it
Print / reprintA receipt on screenPrint / 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 profileProfile fiscal_required on; adapter registeredSell onlineBill 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 identityAccepted fiscal receiptRecord 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 formatRegister code POS01, business date 2 Oct 2026Sell three billsPOS01-20261002-000001, -000002, -000003; the counter is the device's own and restarts on a new date

Till - Sell / Return

ActionWhenWhat you doWhat happens
Return a whole billBill with Milk x 2 = 21.00 paid in cashF8 > receipt number > Find > Return these > pay > CompleteReturn 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 soldBill with Milk x 3Return 1 (API with origin_line), then 2, then 1 moreFirst two succeed (remaining 0); third refused 'Return quantity exceeds what remains on the original sale.'
Return reuses the original price and discountOriginal: Rice 20.00 x 2 with 4.00 line discount; promo no longer activeReturn 1 unitRefund 18.00 (half of 36.00), promotion not re-applied; net 17.14, VAT 0.86

Returns

ActionWhenWhat you doWhat happens
Returns listReturns existOperations > Returns; Transactions > POS returnsBoth list kind = return bills with negative totals

Void a bill (API)

ActionWhenWhat you doWhat happens
Void a bill with a managerUser with pos.void; manager id; bill in a posted or open sessionPOST /pos/sales/{id}/void with reason 'Scanned twice' and approverBill 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 refusalsA billNo 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

ActionWhenWhat you doWhat happens
Hold a billCart with Milk x 2; Suspended sales switch onF6; then add another sale; F7Cart clears, toast 'Bill held - F7 to recall'; stock for the lines is reserved for 30 minutes; F7 lists the held bill
Recall a held billA held billF7 > RecallCart restored; hold consumed once; a second recall is refused ('This held sale is no longer active.')
Held stock cannot be sold from under the hold1 unit left; held on till 1Sell the last unit on till 2Refused: 'Only 0 of <code> is available ...' (the hold owns it); completing the held bill on till 1 works

Suspended sales (API)

ActionWhenWhat you doWhat happens
Cancel or expire a holdA hold; wait past 30 minutes or DELETE itCancel 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 holdOpen sessionHold an empty cart; a line with quantity 0'There is nothing to hold.' and 'A held line needs a positive quantity.'

Suspended sales

ActionWhenWhat you doWhat happens
Menu entry shows 'Not built yet'Menu Operations > Suspended salesOpen itPlanned placeholder appears although the backend and the till hold/recall work

Promotions

ActionWhenWhat you doWhat happens
Percentage promotionPromotion MILK3: 10 %, product Milk, min quantity 3Sell 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 promotionSpecial price 8.00 on Milk, min quantity 2Sell 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 promotionFixed 2.00 on category BeveragesSell 1 x 1.50 drink and 3 x 10.50 drink2.00 is taken once per line, never more than the line (1.50 line -> 1.50 discount)
Priority and stackingRule A 10 % priority 10 not stackable; rule B 5 % priority 5Sell a matching 100.00 lineOnly 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 timeRule 16:00-18:00 on Friday onlySell at 17:00 Friday, 17:00 Saturday, 19:00 FridayApplies only on the first. Test around midnight and early evening because the server uses the UTC sale time
Offline eligibilityRule with Allowed offline offSell offlineRule ignored on the offline bill, till and server agree on the total
Server wins over the tillA rule the till does not know yetSell with a stale cacheServer recalculates; if its total differs the bill is refused with the 'The till says ...' message
Create and edit a promotionCompany adminPricing & promotions > Promotions > New promotion; fill Code, Value; Save; Edit; DiscardSaved record opens view-first; Edit/Save/Discard work; duplicate code 'Promotion code already exists.'; value 150 % -> 'Enter a valid promotion value.'

Coupons

ActionWhenWhat you doWhat happens
Percentage couponCoupon WELCOME10: 10 %, min basket 50.00Online sale 120.00 (net 114.29, VAT 5.71); F9 Coupon WELCOME10Discount 12.00; payable 108.00; coupon redemption counted once; code shown in totals
Fixed coupon larger than the basketFixed 20.00, min basket 10.00Basket 15.00Discount limited to 15.00; payable 0.00
Coupon refusalsCoupons with limitsUnknown 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 togetherCoupon max redemptions 1Submit two sales with the coupon at the same momentOne is accepted, the other refused (row-locked); redemption count 1
Create and edit a couponCompany adminPricing > Coupons > New coupon; fill Code, Value, Max redemptions 100; SaveSaved; Usage column shows 0/100; Allowed offline is locked off; field rule for End date (Features > Fields) is respected
Return of a coupon saleA bill with 12.00 coupon discount on 120.00; return halfLinked return of half the linesCoupon value comes back in proportion (6.00); no new coupon is redeemed

Loyalty

ActionWhenWhat you doWhat happens
Earn pointsProgramme: 1 point per AED, 0.01 AED per point, min 100; customer chosenSell 105.00 onlineCustomer earns 105 points (value 1.05); sale shows loyalty earned 105; walk-in sales earn nothing
Redeem pointsCustomer balance 600; payable 105.00F10 Loyalty > 500 > Redeem > payDiscount 5.00; payable 100.00; balance 100 + points earned on the new total (100) = 200; FIFO lots consumed oldest first
Redeem more than the billPayable 3.00; balance 600Redeem 500Discount capped at 3.00; only 300 points used
Redeem refusalsAs above50 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 pointsBill 200.00 earned 200, redeemed 0; return 50.00 (ratio 0.25)Linked return of 50.00Points reversed 50; balance falls by 50. If points were redeemed on the bill the same ratio is restored with the original expiry
Points expiry runProgramme with expiry days 30; old pointsLoyalty > 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 programmeProgrammes A (active) and BSave B as ActiveA becomes inactive automatically
Create and edit a programmeCompany adminLoyalty > New program; code STD, 1 point per AED, 0.01 per point, minimum 100; Save; EditSaved; duplicate code 'Loyalty program code already exists.'; negative rates -> 'Loyalty rates and limits cannot be negative.'

Gift cards

ActionWhenWhat you doWhat happens
Issue a gift cardCompany admin; onlineGift cards > Issue > value 100.00 > IssueBanner 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 saleCard with 100.00; basket 80.00Payment > Gift card > paste token > Apply > CompleteCard balance 20.00; liability reduces; tender recorded with the masked reference
Spend more than the balanceBalance 20.00Pay 30.00 with the card'Stored-value balance is insufficient.' and no bill
Two simultaneous spends cannot overdrawBalance 100.00Two sales of 80.00 at the same momentOne accepted, one refused; balance 20.00
Recharge, expire, reverseActive cardManage > Recharge 50.00; Expire / breakage; Reverse a movementBalance +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 cardExpired cardUse 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 billA gift-card saleVoid it; try the card offlineVoid refused (use a return); offline -> 'Gift cards and store credit must be checked online.'

Vouchers

ActionWhenWhat you doWhat happens
Partial-use voucherVoucher 50.00Spend 30.00, then 20.00, then 5.00Balance 20.00 then 0.00; third refused as insufficient

Store credit

ActionWhenWhat you doWhat happens
Store credit belongs to one customerCredit issued to customer AIssue 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

ActionWhenWhat you doWhat happens
Sell with the network offOpen session, products cached; network offSell Milk x 1 for cashReceipt 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 reconnectPending bills in the outboxTurn the network on; wait up to 20 seconds or press Sync nowToast 'Synced N'; pending 0; bills appear in POS sales flagged Offline = Yes; stock and Finance effects happen once
Duplicate protection on syncA bill already acceptedReplay 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 partialBatch of 3: good, bad (unknown product), goodPOST /pos/syncaccepted 2, failed 1; the bad one stays in the outbox as failed with its reason and the payload; the good ones are saved
Upload orderBatch sent as: sale, session_open, cash_movement, session_closeUploadApplied as session_open, cash_movement, sale, session_close regardless of the order sent
Offline bill refused when the profile says online onlyProfile offline selling = online_onlyTry to sell offlineTill says 'This operation requires a connection.'; server: 'This register is not allowed to sell offline.'
Offline time limitProfile offline max minutes 30; register last contact 31 minutes agoSell an offline billRefused '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 limitOffline max amount 2,000.00; 1,990.00 already queuedSell 20.00 offlineTill: '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 billOffline(API) offline bill paid by QR'QR / wallet must be authorized online.' no bill, no stock
Coupons, loyalty and gift cards need a connectionOfflinePress 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 syncOffline hold; reconnectHold offline, reconnect, recall on another tillHold uploaded before the recall; other till can recall once; recall offline is queued as hold_recall
Open a session offlineSigned or unsigned device, network offOpen session; reconnectsession_open uploads; if the server already has a live session it answers with that one (no second drawer)
Receipt numbering offlineNetwork offSell 3 billsPOS01-<date>-000001..3 made on the till; unique per register; after sync the same numbers show in the journal
Upload batch limits and bad itemsA batch201 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

ActionWhenWhat you doWhat happens
Secure this devicepos.sync.manage; register with no deviceWorkspace > Secure this deviceKey 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 sequenceEnrolled device with 3 queued billsSyncEach item signed (sequence 1,2,3); device last_sequence 3; exact resend is 'duplicate'
Changed payload on a used identity is quarantinedEnrolled device; sequence 4 already usedResend 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 rollbackLast sequence 10Send sequence 7 with a new uuidQuarantined (sequence_rollback)
Unsigned browser on an enrolled registerRegister has an enrolled deviceOpen a session or sync from another browser with no key'This register requires an enrolled signed device.'; opening needs the signed command
Revoke a deviceActive devicePOST /pos/devices/{id}/revoke; then sync from itState 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 inputpos.sync.manageNo 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

ActionWhenWhat you doWhat happens
Dashboard figuresToday: 3 sales of 105.00, 1 return of 10.50, 1 offline sale, 1 open sessionOpen the dashboardSales 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

ActionWhenWhat you doWhat happens
Journal filtersSales, returns, voids, offline bills existOpen POS sales, POS returns, Voids, Cancelled bills, Offline auditEach shows the matching kind/state/offline set (100 rows max); columns Receipt, Sold at, Register, Kind, State, Offline, Total

Exceptions and audit log

ActionWhenWhat you doWhat happens
Exception viewVoids, closes refused, offline bills, cash outs and drops existTransactions > Exception transactions; compare with Administration > POS audit logExceptions list only offline items, sale.voided, session.close_refused, price/discount overrides, cash.out, cash.safe_drop; the audit log lists everything

Device monitor

ActionWhenWhat you doWhat happens
Monitor every tillTwo registers, one with 2 queued itemsHardware > Device monitorOne row per active register: state, pending 2, offline sales on the live session, last sync; needs pos.sync.manage

Pending transactions

ActionWhenWhat you doWhat happens
Pending and failed listItems pending and one failedSynchronisation > Pending transactions and Failed syncBoth menus open the same list (state, tries, error); count is the full count even if only 200 rows show

Provider settlement (API)

ActionWhenWhat you doWhat happens
Reconcile a card batchProvider adapter and transactions exist; pos.configureRecord capture 105.00 and refund 5.00; reconcile with deposit 98.00, fees 2.00, expected 100.00Net 100.00 = 98.00 bank + 2.00 fee; one balanced journal (bank, fee expense, clearing); posting once even if repeated
Reconciliation refusalsAs aboveDeposit 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 acceptedpos.sell / pos.configureSend a body containing 'cvv' or 'pan''Never send or store PAN, CVV, PIN or card track data.'
Unknown card outcome is queried, not recapturedAdapter that times outCreate a payment intent; resolve itState 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

ActionWhenWhat you doWhat happens
Turn off Promotions and couponsCompany adminApplications > Point of Sale > Features: switch off Promotions and couponsMenu 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 terminalsAs aboveSwitch each off in turnLoyalty, 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 ruleFeatures > Fields: Coupon end dateSet Required, then Hidden, then Read only; open a couponRequired: 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 ruleFields: Gift card and voucher expirySet Required then Hidden; open IssueRequired: 'Expires at *' needed; Hidden: box not shown and an expiry sent is refused

Registers

ActionWhenWhat you doWhat happens
Registers, profiles, tenders and reasons are read-only listsAny POS userOpen POS setup > Registers, POS profiles, Payment methods and Administration > Reason codesLists only: no New or Edit button on any of the four; settings can only be changed by API or direct data change

Planned screens

ActionWhenWhat you doWhat happens
Not-built screens tell the truthAny POS userOpen Cashier shifts, Reprints, Return policies, Sales reports and Alert rulesEach shows 'Not built yet', its purpose and 'What is missing', with a disabled button; none shows an empty table or a working-looking button