| Page | RetailSaleResource CreateRecord (custom, big-target layout) | form | creates: retail_sales + retail_sale_lines; next number from the RS- series (separate from OEM SO series) | — |
| Customer | Select + recent-chips shortcut | FK nullable | customers scoped to channel ∈ {retail, both}; chips = this user's most recent sales; Walk-in ⇒ customer_id null (still consolidates, no price memory) | customers |
| + New customer (slide-over) | CreateAction slideOver(), inline from the Select — trimmed to till-speed fields | form | creates: customers (display_name, phone, area nullable) — same table Sell → Customers writes; channel is hard-set to retail here (not a field, hidden) and terms/b2b_einvoice/notes stay at column defaults; on create the new customer is selected for this sale (create-inline-then-sell). Promoting a kedai to B2B (channel=both, b2b_einvoice) happens later in Retail → Customers, not at the till | customers |
| Lines | Repeater (product, unit, qty, price) | rows | retail_sale_lines (variant_id, qty_base, unit_price); staff enters qty in ctn/box, the UI converts to base pcs live ("= N pcs/boxes" hint) and only the base qty is stored — no separate UoM column on the line; stock hint = live FG on-hand at current factory | variants / batches |
| Pack conversion | hard-coded per item, not user-entered | item data | STC30: 1 ctn = 24 boxes (confirm pack size), 1 box = 15 sachets — sold by box or ctn only, sachets never sold loose. Double Root Coffee: 1 ctn = 30 pcs — sold by ctn or pcs. Same hard-coded UoM rule set used across Buy/Make/Sell; retail entry just reuses it | items.pack_uom_factor |
| Price autofill | TextInput, afterStateUpdated query | money, derived default | query: last price this customer paid for this variant (OEM SO lines + retail sale lines feed ONE memory); sub-line shows last paid · band · floor live | sale history / variant_prices |
| Floor flag | Validation + policy (soft, retail-only) | rule | price < variant_prices.floor_price ⇒ line flags red and writes a notification row for the owner; the sale still posts and Confirm stays enabled. Retail is deliberately soft — kedai haggling is normal and staff need to close the sale. OEM keeps the hard block + approval_requests queue; that table is OEM-only and is never written from this screen | variant_prices / notifications |
| Payment | ToggleButtons (large) | enum cash / transfer / unpaid | retail_sales.payment_status; Unpaid sets due_date = today + customer terms and feeds the follow-up list | — |
| Confirm | CreateAction — always enabled | action | posts FEFO retail_sale stock movements (qty_base spent, batch trace intact); updates price memory; below-floor lines additionally write a notification for the owner; sale joins consolidation_runs for the month (unless customer B2B-flagged → individual QBO export) | stock_movements / consolidation_runs / notifications |
| Print receipt (success panel) | Action, opens print view | stub | same print-preview pattern as print-packing-slip.html's retail variant (RS-26-038 example) — not wired in this mockup | — |