Chapter 14: POS Client Application
The Point of Sale Terminal
The POS Client is the primary interface for retail associates. It must be fast, reliable, and work offline when network connectivity is lost. This chapter provides complete specifications for building a production-grade POS terminal.
14.1 Technology Stack
| Component | Technology | Rationale |
|---|---|---|
| Framework | .NET MAUI Blazor Hybrid | Cross-platform, native performance |
| Local Database | SQLite | Embedded, zero-config, reliable |
| State Management | Fluxor or custom MVVM | Predictable state changes |
| Hardware API | Platform Invoke (P/Invoke) | Direct hardware access |
| Sync Engine | Custom HTTP + SignalR | Real-time + batch sync |
14.2 Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ POS CLIENT APPLICATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Views │ │ ViewModels │ │ Services │ │ Hardware │ │
│ │ (XAML/ │◄─┤ (State + │◄─┤ (Business │◄─┤ Drivers │ │
│ │ Blazor) │ │ Commands) │ │ Logic) │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │ │
│ └────────────────┴────────────────┴────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Local SQLite │ │
│ │ Database │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Sync Engine │ │
│ │ (Online/Queue) │ │
│ └────────┬────────┘ │
└──────────────────────────────────┬──────────────────────────────────┘
│
┌────────▼────────┐
│ Central API │
│ (When Online) │
└─────────────────┘
14.3 Screen Specifications
Screen 1: Login Screen
Purpose: Authenticate retail associates with fast PIN entry.
Route: /login
╔════════════════════════════════════════════════════════════════════╗
║ ║
║ ┌──────────────────────────┐ ║
║ │ │ ║
║ │ STORE LOGO │ ║
║ │ [128x128] │ ║
║ │ │ ║
║ └──────────────────────────┘ ║
║ ║
║ NEXUS CLOTHING ║
║ Greenbrier Mall (GM) ║
║ ║
║ ┌──────────────────────────┐ ║
║ │ │ ║
║ │ Enter Employee PIN │ ║
║ │ │ ║
║ │ ● ● ● ○ ○ ○ │ ║
║ │ │ ║
║ └──────────────────────────┘ ║
║ ║
║ ┌─────┬─────┬─────┐ ║
║ │ 1 │ 2 │ 3 │ ║
║ ├─────┼─────┼─────┤ ║
║ │ 4 │ 5 │ 6 │ ║
║ ├─────┼─────┼─────┤ ║
║ │ 7 │ 8 │ 9 │ ║
║ ├─────┼─────┼─────┤ ║
║ │ CLR │ 0 │ ENT │ ║
║ └─────┴─────┴─────┘ ║
║ ║
║ [Manager Override] ║
║ ║
║ ───────────────────────────────────────────────────────────── ║
║ Status: ● Online | Last Sync: 2 min ago | v1.2.0 ║
╚════════════════════════════════════════════════════════════════════╝
Components:
| Component | Specification |
|---|---|
| Logo | 128x128px, tenant-specific |
| Store Name | 24px, Bold, Primary color |
| Location | 14px, Secondary text |
| PIN Display | 6 circles, filled = entered |
| Numpad | 80x80px buttons, touch-friendly |
| Clear (CLR) | Resets PIN entry |
| Enter (ENT) | Submits PIN for validation |
| Manager Override | Opens manager auth dialog |
| Status Bar | Connection, sync, version |
Behavior:
- PIN validated locally first (hash comparison)
- Failed attempts: 3 max before lockout
- Lockout duration: 5 minutes (configurable)
- Auto-login timeout: 30 seconds of inactivity returns to login
Screen 2: Main Sale Screen
Purpose: Primary transaction interface for ringing up sales.
Route: /sale
╔════════════════════════════════════════════════════════════════════╗
║ NEXUS CLOTHING - GM Sarah M. 12/29/2024 2:45 PM ║
╠════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌─────────────────────────────────────────────────────────────┐ ║
║ │ [Scan Item or Enter SKU...] [SEARCH] │ ║
║ └─────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌──────────────────────────────────┐ ┌───────────────────────┐ ║
║ │ CART (3) │ │ TOTALS │ ║
║ ├──────────────────────────────────┤ ├───────────────────────┤ ║
║ │ │ │ │ ║
║ │ 1. Galaxy V-Neck Tee $29 │ │ Subtotal: $104.00 │ ║
║ │ Size: M | Color: Navy │ │ │ ║
║ │ Qty: 2 [-] [+] $58 │ │ Discount: -$10.00 │ ║
║ │ [DEL] │ │ │ ║
║ │ ─────────────────────────────────│ │ Tax (6%): $5.64 │ ║
║ │ 2. Slim Fit Chinos $46 │ │ │ ║
║ │ Size: 32 | Color: Khaki │ │ ───────────────────── │ ║
║ │ Qty: 1 [-] [+] $46 │ │ │ ║
║ │ [DEL] │ │ TOTAL: $99.64 │ ║
║ │ ─────────────────────────────────│ │ │ ║
║ │ │ │ │ ║
║ │ │ └───────────────────────┘ ║
║ │ │ ║
║ │ │ ┌───────────────────────┐ ║
║ │ │ │ [DISCOUNT] [HOLD] │ ║
║ │ │ │ │ ║
║ │ │ │ [CUSTOMER] [VOID] │ ║
║ │ │ │ │ ║
║ └──────────────────────────────────┘ │ ┌───────────────────┐ │ ║
║ │ │ │ │ ║
║ ┌──────────────────────────────────┐ │ │ PAY │ │ ║
║ │ Customer: John Smith │ │ │ $99.64 │ │ ║
║ │ Loyalty: Gold (2,450 pts) │ │ │ │ │ ║
║ │ [Remove Customer] │ │ └───────────────────┘ │ ║
║ └──────────────────────────────────┘ └───────────────────────┘ ║
║ ║
╠════════════════════════════════════════════════════════════════════╣
║ [F1 Help] [F2 Lookup] [F3 Returns] [F4 Reports] ● Online Rcpt#42 ║
╚════════════════════════════════════════════════════════════════════╝
Layout Regions:
| Region | Width | Content |
|---|---|---|
| Header | 100% | Store, associate, date/time |
| Search Bar | 100% | SKU/barcode entry with search |
| Cart Panel | 60% | Line items with quantity controls |
| Totals Panel | 40% | Running totals, discounts, tax |
| Action Buttons | 40% | Discount, Hold, Customer, Void |
| Pay Button | 40% | Large, prominent payment trigger |
| Customer Info | 60% | Attached customer details |
| Footer | 100% | Function keys, status, receipt # |
Cart Item Layout:
┌─────────────────────────────────────────────────────────────┐
│ 1. Galaxy V-Neck Tee $29 │
│ Size: M | Color: Navy │
│ Qty: 2 [-] [+] $58 │
│ [DEL] │
└─────────────────────────────────────────────────────────────┘
Keyboard Shortcuts:
| Key | Action |
|---|---|
| F1 | Help overlay |
| F2 | Product lookup |
| F3 | Returns mode |
| F4 | Quick reports |
| F5 | Price check |
| F8 | Suspend sale |
| F9 | Recall sale |
| F12 | Manager functions |
| Enter | Add scanned item |
| Esc | Cancel current action |
Screen 3: Customer Lookup
Purpose: Find or create customer records for loyalty tracking.
Route: /customer-lookup (Modal overlay)
╔════════════════════════════════════════════════════════════════════╗
║ CUSTOMER LOOKUP [X] ║
╠════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ [Search by name, phone, email, or loyalty #...] │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ RESULTS (3 found) │ ║
║ ├──────────────────────────────────────────────────────────────┤ ║
║ │ │ ║
║ │ ○ John Smith │ ║
║ │ Phone: (555) 123-4567 │ ║
║ │ Email: john.smith@email.com │ ║
║ │ Loyalty: Gold (2,450 pts) | Last Visit: 12/15/2024 │ ║
║ │ │ ║
║ │ ─────────────────────────────────────────────────────────── │ ║
║ │ │ ║
║ │ ○ Johnny Smith Jr. │ ║
║ │ Phone: (555) 234-5678 │ ║
║ │ Email: johnny.jr@email.com │ ║
║ │ Loyalty: Silver (890 pts) | Last Visit: 11/20/2024 │ ║
║ │ │ ║
║ │ ─────────────────────────────────────────────────────────── │ ║
║ │ │ ║
║ │ ○ Jonathan Smithson │ ║
║ │ Phone: (555) 345-6789 │ ║
║ │ Email: j.smithson@work.com │ ║
║ │ Loyalty: None | Last Visit: 10/05/2024 │ ║
║ │ │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌────────────────────────────────────────────────────────────────╢
║ │ ║
║ │ [NEW CUSTOMER] [SELECT] [CANCEL] ║
║ │ ║
║ └────────────────────────────────────────────────────────────────╢
╚════════════════════════════════════════════════════════════════════╝
New Customer Form:
╔════════════════════════════════════════════════════════════════════╗
║ NEW CUSTOMER [X] ║
╠════════════════════════════════════════════════════════════════════╣
║ ║
║ First Name * Last Name * ║
║ ┌──────────────────┐ ┌──────────────────────────────────────────┐ ║
║ │ John │ │ Smith │ ║
║ └──────────────────┘ └──────────────────────────────────────────┘ ║
║ ║
║ Phone * Email ║
║ ┌──────────────────────────┐ ┌────────────────────────────────┐ ║
║ │ (555) 123-4567 │ │ john.smith@email.com │ ║
║ └──────────────────────────┘ └────────────────────────────────┘ ║
║ ║
║ Address Line 1 ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ 123 Main Street │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ City State ZIP ║
║ ┌────────────────────┐ ┌─────────┐ ┌───────────────────────────┐║
║ │ Virginia Beach │ │ VA ▼ │ │ 23451 │║
║ └────────────────────┘ └─────────┘ └───────────────────────────┘║
║ ║
║ [ ] Enroll in Loyalty Program ║
║ [ ] Subscribe to email marketing ║
║ ║
║ ┌────────────────────────────────────────────────────────────────╢
║ │ ║
║ │ [SAVE] [CANCEL] ║
║ │ ║
║ └────────────────────────────────────────────────────────────────╢
╚════════════════════════════════════════════════════════════════════╝
Screen 4: Returns Processing
Purpose: Process merchandise returns and exchanges.
Route: /returns
╔════════════════════════════════════════════════════════════════════╗
║ RETURNS PROCESSING [Exit Return]║
╠════════════════════════════════════════════════════════════════════╣
║ ║
║ STEP 1: FIND ORIGINAL TRANSACTION ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ Receipt #: [________________] OR [Lookup by Customer] │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ ORIGINAL TRANSACTION #12345 12/20/2024 │ ║
║ ├──────────────────────────────────────────────────────────────┤ ║
║ │ Customer: John Smith │ ║
║ │ Payment: Visa ****4242 │ ║
║ ├──────────────────────────────────────────────────────────────┤ ║
║ │ │ ║
║ │ [x] 1. Galaxy V-Neck Tee (M, Navy) $29.00 │ ║
║ │ Reason: [Wrong Size ▼] │ ║
║ │ Condition: [Good - Resellable ▼] │ ║
║ │ │ ║
║ │ [ ] 2. Slim Fit Chinos (32, Khaki) $46.00 │ ║
║ │ │ ║
║ │ [ ] 3. Leather Belt (M) $35.00 │ ║
║ │ │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌────────────────────────┐ ┌───────────────────────────────────┐║
║ │ RETURN SUMMARY │ │ REFUND TO │║
║ ├────────────────────────┤ ├───────────────────────────────────┤║
║ │ Items: 1 │ │ ○ Original Payment (Visa ****42) │║
║ │ Subtotal: $29.00 │ │ ○ Store Credit │║
║ │ Tax Refund: $1.74 │ │ ○ Cash │║
║ │ ────────────────── │ │ ○ Exchange (Add to New Sale) │║
║ │ TOTAL: $30.74 │ │ │║
║ └────────────────────────┘ └───────────────────────────────────┘║
║ ║
║ Manager Approval Required: [ ] Over $100 [ ] No Receipt ║
║ ║
║ ┌────────────────────────────────────────────────────────────────╢
║ │ ║
║ │ [SCAN RETURN ITEMS] [PROCESS RETURN] [CANCEL] ║
║ │ ║
║ └────────────────────────────────────────────────────────────────╢
╚════════════════════════════════════════════════════════════════════╝
Return Reasons (Configurable):
- Wrong Size
- Wrong Color
- Defective
- Changed Mind
- Gift Return
- Price Adjustment
- Other
Return Conditions:
- Good - Resellable
- Damaged - Cannot Resell
- Missing Tags - Markdown
Screen 5: Inventory Lookup
Purpose: Check stock levels across all locations.
Route: /inventory (Modal overlay)
╔════════════════════════════════════════════════════════════════════╗
║ INVENTORY LOOKUP [X] ║
╠════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ [Search by SKU, name, or scan barcode...] [SEARCH]│ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ Galaxy V-Neck Tee $29.00 │ ║
║ │ SKU: NXJ1078-NAV-M │ ║
║ │ ────────────────────────────────────────────────────────────│ ║
║ │ │ ║
║ │ VARIANTS: │ ║
║ │ ┌────────────┬─────┬─────┬─────┬─────┬─────┬───────┐ │ ║
║ │ │ Size/Color │ HQ │ GM │ HM │ LM │ NM │ TOTAL │ │ ║
║ │ ├────────────┼─────┼─────┼─────┼─────┼─────┼───────┤ │ ║
║ │ │ S / Navy │ 12 │ 3 │ 2 │ 4 │ 1 │ 22 │ │ ║
║ │ │ M / Navy │ 15 │ 5*│ 3 │ 2 │ 0 │ 25 │ │ ║
║ │ │ L / Navy │ 8 │ 4 │ 1 │ 3 │ 2 │ 18 │ │ ║
║ │ │ XL / Navy │ 4 │ 2 │ 0 │ 1 │ 1 │ 8 │ │ ║
║ │ │ S / Black │ 10 │ 2 │ 3 │ 2 │ 2 │ 19 │ │ ║
║ │ │ M / Black │ 18 │ 6 │ 4 │ 5 │ 3 │ 36 │ │ ║
║ │ └────────────┴─────┴─────┴─────┴─────┴─────┴───────┘ │ ║
║ │ │ ║
║ │ * Current Location (GM) │ ║
║ │ │ ║
║ │ Last Updated: 12/29/2024 2:30 PM │ ║
║ │ │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌────────────────────────────────────────────────────────────────╢
║ │ ║
║ │ [REQUEST TRANSFER] [PRICE CHECK] [CLOSE] ║
║ │ ║
║ └────────────────────────────────────────────────────────────────╢
╚════════════════════════════════════════════════════════════════════╝
Screen 6: End of Day
Purpose: Close register, balance cash, generate reports.
Route: /end-of-day
╔════════════════════════════════════════════════════════════════════╗
║ END OF DAY - Close Register [Cancel] ║
╠════════════════════════════════════════════════════════════════════╣
║ ║
║ Register: REGISTER-01 (GM) Date: 12/29/2024 ║
║ Cashier: Sarah Miller Shift: 9:00 AM - 5:30 PM ║
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ SALES SUMMARY │ ║
║ ├──────────────────────────────────────────────────────────────┤ ║
║ │ │ ║
║ │ Total Transactions: 47 │ ║
║ │ Gross Sales: $3,245.67 │ ║
║ │ Returns: -$125.00 │ ║
║ │ Discounts: -$89.50 │ ║
║ │ ────────────────────────────────────── │ ║
║ │ Net Sales: $3,031.17 │ ║
║ │ Tax Collected: $181.87 │ ║
║ │ │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ CASH COUNT │ ║
║ ├──────────────────────────────────────────────────────────────┤ ║
║ │ │ ║
║ │ Starting Cash: $200.00 │ ║
║ │ Cash Sales: $845.50 │ ║
║ │ Cash Returns: -$45.00 │ ║
║ │ ────────────────────────────────────── │ ║
║ │ Expected Cash: $1,000.50 │ ║
║ │ │ ║
║ │ Counted Cash: [_______________] <-- Enter amount │ ║
║ │ │ ║
║ │ Variance: $___.__ (Calculates automatically) │ ║
║ │ │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ PAYMENT BREAKDOWN │ ║
║ ├──────────────────────────────────────────────────────────────┤ ║
║ │ Cash: $845.50 (28 trans) │ ║
║ │ Credit Card: $1,856.32 (15 trans) │ ║
║ │ Debit Card: $254.35 (3 trans) │ ║
║ │ Store Credit: $75.00 (1 trans) │ ║
║ └──────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌────────────────────────────────────────────────────────────────╢
║ │ ║
║ │ [PRINT REPORT] [RECOUNT] [CLOSE REGISTER] ║
║ │ ║
║ └────────────────────────────────────────────────────────────────╢
╚════════════════════════════════════════════════════════════════════╝
14.4 Payment Screen
Purpose: Process various payment methods.
╔════════════════════════════════════════════════════════════════════╗
║ PAYMENT [X] ║
╠════════════════════════════════════════════════════════════════════╣
║ ║
║ AMOUNT DUE: $99.64 ║
║ ║
║ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ║
║ │ │ │ │ │ │ ║
║ │ [CREDIT] │ │ [DEBIT] │ │ [CASH] │ ║
║ │ CARD │ │ CARD │ │ │ ║
║ │ │ │ │ │ │ ║
║ └─────────────────┘ └─────────────────┘ └─────────────────┘ ║
║ ║
║ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ║
║ │ │ │ │ │ │ ║
║ │ [GIFT] │ │ [STORE] │ │ [SPLIT] │ ║
║ │ CARD │ │ CREDIT │ │ PAYMENT │ ║
║ │ │ │ │ │ │ ║
║ └─────────────────┘ └─────────────────┘ └─────────────────┘ ║
║ ║
║ ═══════════════════════════════════════════════════════════════ ║
║ ║
║ CASH QUICK AMOUNTS: ║
║ ║
║ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ║
║ │ $20 │ │ $50 │ │ $100 │ │ $120 │ │ EXACT │ │ OTHER │ ║
║ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ ║
║ ║
║ Amount Tendered: $________ ║
║ Change Due: $________ ║
║ ║
║ ┌────────────────────────────────────────────────────────────────╢
║ │ ║
║ │ [PROCESS] [CANCEL] ║
║ │ ║
║ └────────────────────────────────────────────────────────────────╢
╚════════════════════════════════════════════════════════════════════╝
14.5 State Management
Application State Model
public class PosState
{
// Authentication
public AuthState Auth { get; set; }
// Current Transaction
public TransactionState Transaction { get; set; }
// Cart Items
public List<CartItem> Cart { get; set; }
// Customer
public CustomerState Customer { get; set; }
// Register
public RegisterState Register { get; set; }
// Sync Status
public SyncState Sync { get; set; }
// UI State
public UiState Ui { get; set; }
}
public class TransactionState
{
public string TransactionId { get; set; }
public TransactionType Type { get; set; } // Sale, Return, Exchange
public TransactionStatus Status { get; set; }
public decimal Subtotal { get; set; }
public decimal DiscountTotal { get; set; }
public decimal TaxTotal { get; set; }
public decimal GrandTotal { get; set; }
public List<PaymentEntry> Payments { get; set; }
public decimal BalanceDue { get; set; }
}
State Actions
| Action | Description |
|---|---|
AddToCart | Add item with quantity |
UpdateQuantity | Change line item quantity |
RemoveFromCart | Delete line item |
ApplyDiscount | Add transaction/line discount |
AttachCustomer | Link customer to sale |
ProcessPayment | Record payment entry |
VoidTransaction | Cancel entire transaction |
SuspendSale | Park sale for later |
RecallSale | Resume suspended sale |
14.6 Sync Service Design
Sync Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ SYNC ENGINE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ OUTBOUND │ │ INBOUND │ │ CONFLICT │ │
│ │ QUEUE │────▶│ HANDLER │────▶│ RESOLVER │ │
│ │ (SQLite) │ │ (API Sync) │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LOCAL SQLITE DATABASE │ │
│ │ - Transactions (pending sync) │ │
│ │ - Products (cached catalog) │ │
│ │ - Customers (cached records) │ │
│ │ - Inventory (last known levels) │ │
│ │ - Sync metadata (timestamps, versions) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Sync Interval
The sync engine runs on a 30-second polling cycle with immediate sync on reconnect:
| Trigger | Behavior |
|---|---|
| Timer (every 30s) | Poll for outbound queue items and inbound updates |
| Reconnect | Immediately flush all pending outbound items when connectivity restored |
| Manual | User can trigger via status bar “Sync Now” action |
| Transaction complete | Immediate outbound sync attempt for completed transactions |
Sync Priorities
| Priority | Data Type | Frequency | Direction |
|---|---|---|---|
| 1 (Critical) | Transactions | Immediate | Outbound |
| 2 (High) | Inventory Changes | 5 min | Both |
| 3 (Medium) | Customers | 15 min | Both |
| 4 (Low) | Products | 1 hour | Inbound |
| 5 (Batch) | Reports | Daily | Outbound |
Conflict Resolution: When offline transactions sync, inventory conflicts (e.g., stock sold by another terminal) are resolved using the strategies defined in Chapter 05 Section 5.6 (Offline-First Architecture). The POS Client applies partial-fulfillment or last-write-wins depending on the entity type. See also Chapter 04 Section L.10A.1 for CRDT-based conflict resolution patterns.
Offline Queue Schema
CREATE TABLE sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_type TEXT NOT NULL, -- 'transaction', 'customer', etc.
entity_id TEXT NOT NULL,
action TEXT NOT NULL, -- 'create', 'update', 'delete'
payload TEXT NOT NULL, -- JSON serialized data
priority INTEGER DEFAULT 5,
retry_count INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
last_attempt TEXT,
status TEXT DEFAULT 'pending' -- 'pending', 'syncing', 'failed', 'synced'
);
CREATE INDEX idx_sync_queue_status ON sync_queue(status, priority);
Offline Queue Limits
| Threshold | Behavior |
|---|---|
| < 80 queued transactions | Normal operation |
| 80 queued transactions | Yellow warning banner: “80 transactions pending sync – connect to network soon” |
| 100 queued transactions | Block new sales. Red banner: “Maximum offline transactions reached. Connect to network to sync before processing new sales.” |
Rationale: The 100-transaction limit prevents unbounded local data growth and reduces conflict risk during bulk sync. The warning at 80 gives associates time to find connectivity before hitting the hard limit.
Parked Sales (Hold/Recall)
Associates can suspend (“park”) an in-progress transaction to serve the next customer, then recall it later.
| Constraint | Value |
|---|---|
| Maximum parked sales | 5 per register |
| Time-to-live (TTL) | 4 hours from park time |
| Expiry behavior | Auto-void after TTL, items returned to available inventory |
| Recall | Any associate on the same register can recall |
Parked Sale States:
Active Sale → [HOLD] → Parked (timer starts)
│
├── [RECALL] → Resume as Active Sale
└── [4h TTL expires] → Auto-Void → Inventory restored
14.7 Hardware Integration
Receipt Printer
public interface IReceiptPrinter
{
Task<bool> PrintReceiptAsync(Receipt receipt);
Task<bool> OpenCashDrawerAsync();
Task<bool> CutPaperAsync();
Task<PrinterStatus> GetStatusAsync();
}
public class EpsonTM88Printer : IReceiptPrinter
{
private readonly string _portName;
public async Task<bool> PrintReceiptAsync(Receipt receipt)
{
var commands = new List<byte>();
// Initialize printer
commands.AddRange(new byte[] { 0x1B, 0x40 }); // ESC @
// Center align
commands.AddRange(new byte[] { 0x1B, 0x61, 0x01 }); // ESC a 1
// Store header (double width/height)
commands.AddRange(new byte[] { 0x1D, 0x21, 0x11 }); // GS ! 0x11
commands.AddRange(Encoding.ASCII.GetBytes(receipt.StoreName + "\n"));
// Reset text size
commands.AddRange(new byte[] { 0x1D, 0x21, 0x00 });
// ... additional formatting
// Cut paper
commands.AddRange(new byte[] { 0x1D, 0x56, 0x00 }); // GS V 0
return await SendToPortAsync(commands.ToArray());
}
}
Barcode Scanner
public interface IBarcodeScanner
{
event EventHandler<BarcodeScannedEventArgs> BarcodeScanned;
Task StartListeningAsync();
Task StopListeningAsync();
}
public class HoneywellScanner : IBarcodeScanner
{
public event EventHandler<BarcodeScannedEventArgs> BarcodeScanned;
private SerialPort _port;
public async Task StartListeningAsync()
{
_port = new SerialPort("COM3", 9600);
_port.DataReceived += OnDataReceived;
_port.Open();
}
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
var barcode = _port.ReadLine().Trim();
BarcodeScanned?.Invoke(this, new BarcodeScannedEventArgs(barcode));
}
}
Cash Drawer
public interface ICashDrawer
{
Task<bool> OpenAsync();
Task<bool> IsOpenAsync();
}
public class ApgCashDrawer : ICashDrawer
{
private readonly IReceiptPrinter _printer;
public async Task<bool> OpenAsync()
{
// Most cash drawers open via printer kick command
return await _printer.OpenCashDrawerAsync();
}
}
Receipt Printing Workflow
Sale Completed
│
├── Print receipt via ESC/POS to thermal printer
│ ├── Success → Open cash drawer (if cash payment)
│ └── Failure → Show "Printer Error" toast
│ ├── [Retry] → Resend ESC/POS commands
│ └── [Email Receipt] → Fallback to email
│
├── Reprint from Transaction History
│ └── Tap receipt icon on any past transaction → reprint
│
└── Email Receipt (optional)
└── If customer has email on file → send digital receipt
| Trigger | Action |
|---|---|
| Sale completed | Auto-print if printer connected |
| Cash payment | Print receipt + open cash drawer |
| Card payment | Print receipt (drawer stays closed) |
| Return completed | Print return receipt with refund details |
| Reprint request | From transaction history, tap receipt icon |
| Printer offline | Offer email receipt as fallback |
14.8 Local Database Schema
The POS Client uses 6 core SQLite tables for offline-first operation. These tables are the local source of truth when offline, synced to the central PostgreSQL database when connectivity is restored.
-- ============================================================
-- TABLE 1: products (cached product catalog from central)
-- ============================================================
CREATE TABLE products (
id TEXT PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
barcode TEXT,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
cost REAL,
category_id TEXT,
tax_rate REAL DEFAULT 0,
quantity_on_hand INTEGER DEFAULT 0, -- cached stock at this location
is_active INTEGER DEFAULT 1,
last_synced TEXT NOT NULL
);
CREATE INDEX idx_products_barcode ON products(barcode);
CREATE INDEX idx_products_sku ON products(sku);
-- ============================================================
-- TABLE 2: customers (cached customer records)
-- ============================================================
CREATE TABLE customers (
id TEXT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
phone TEXT,
email TEXT,
loyalty_tier TEXT,
loyalty_points INTEGER DEFAULT 0,
last_synced TEXT NOT NULL
);
-- ============================================================
-- TABLE 3: transactions (local-first, queued for sync)
-- Includes line items and payments as JSON for atomic sync
-- ============================================================
CREATE TABLE transactions (
id TEXT PRIMARY KEY,
transaction_number INTEGER NOT NULL,
type TEXT NOT NULL, -- 'sale', 'return', 'exchange'
status TEXT NOT NULL, -- 'in_progress', 'completed', 'voided'
customer_id TEXT,
associate_id TEXT NOT NULL,
register_id TEXT NOT NULL,
subtotal REAL NOT NULL,
discount_total REAL DEFAULT 0,
tax_total REAL NOT NULL,
grand_total REAL NOT NULL,
line_items TEXT NOT NULL, -- JSON array of line items
payments TEXT, -- JSON array of payment entries
created_at TEXT NOT NULL,
completed_at TEXT,
synced_at TEXT,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
-- ============================================================
-- TABLE 4: sync_queue (pending outbound sync items)
-- Max 100 pending transactions before blocking new sales
-- ============================================================
CREATE TABLE sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_type TEXT NOT NULL, -- 'transaction', 'customer', etc.
entity_id TEXT NOT NULL,
action TEXT NOT NULL, -- 'create', 'update', 'delete'
payload TEXT NOT NULL, -- JSON serialized data
priority INTEGER DEFAULT 5,
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 10,
created_at TEXT NOT NULL,
last_attempt TEXT,
status TEXT DEFAULT 'pending' -- 'pending', 'syncing', 'failed', 'synced'
);
CREATE INDEX idx_sync_queue_status ON sync_queue(status, priority);
-- ============================================================
-- TABLE 5: parked_sales (suspended transactions)
-- Max 5 per register, 4-hour TTL
-- ============================================================
CREATE TABLE parked_sales (
id TEXT PRIMARY KEY,
register_id TEXT NOT NULL,
associate_id TEXT NOT NULL,
customer_id TEXT,
cart_json TEXT NOT NULL, -- JSON: full cart state (items, discounts, customer)
subtotal REAL NOT NULL,
note TEXT, -- optional note ("Customer getting wallet")
parked_at TEXT NOT NULL,
expires_at TEXT NOT NULL, -- parked_at + 4 hours
recalled_at TEXT,
status TEXT DEFAULT 'parked' -- 'parked', 'recalled', 'expired'
);
CREATE INDEX idx_parked_sales_register ON parked_sales(register_id, status);
-- ============================================================
-- TABLE 6: config (local settings and sync metadata)
-- ============================================================
CREATE TABLE config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- Default config entries:
-- 'register_id' → UUID of this register
-- 'location_code' → Store code (e.g., 'GM')
-- 'tenant_id' → Tenant identifier
-- 'last_full_sync' → ISO timestamp of last complete sync
-- 'last_product_sync' → ISO timestamp of last product catalog sync
-- 'sync_interval_ms' → Sync polling interval (default: 30000)
-- 'offline_tx_limit' → Max queued transactions (default: 100)
-- 'offline_tx_warning' → Warning threshold (default: 80)
-- 'parked_sale_limit' → Max parked sales (default: 5)
-- 'parked_sale_ttl_min' → TTL in minutes (default: 240)
14.9 Performance Requirements
Performance Budget
| Operation | Target | Percentile | Measurement |
|---|---|---|---|
| Complete checkout | < 500ms | p99 | Tap “Pay” to receipt printed (the critical path) |
| Barcode scan to item display | < 200ms | p95 | SQLite lookup + cart render |
| Product search | < 200ms | p95 | Keystroke to results |
| App cold start | < 3 seconds | p95 | Launch to login screen ready |
| Sync cycle | < 5 seconds | p95 | Background sync round-trip |
| Offline switch | Instant | p99 | Seamless transition, no UI jank |
| Receipt print | < 1 second | p95 | ESC/POS command to paper cut |
| Parked sale recall | < 300ms | p95 | Select to full cart restored |
Checkout Budget (500ms p99): This is the most critical performance target. The 500ms budget covers: validate cart (50ms) + calculate tax (50ms) + record payment (100ms) + update inventory (50ms) + generate receipt (100ms) + print receipt (150ms). All operations hit local SQLite; server sync happens asynchronously after the customer interaction completes.
14.10 Security Considerations
| Concern | Mitigation |
|---|---|
| PIN Storage | Hashed with bcrypt, salted |
| Local DB | SQLCipher encryption |
| API Tokens | Secure storage (Keychain/DPAPI) |
| PCI Compliance | No card data stored locally |
| Session Timeout | Auto-logout after inactivity |
| Audit Trail | All actions logged with timestamp |
14.11 Summary
The POS Client Application is designed for:
- Speed: Sub-second response times for all common operations
- Reliability: Full offline capability with automatic sync
- Usability: Touch-friendly, keyboard shortcuts, minimal training
- Security: PIN auth, encrypted storage, audit logging
- Integration: Hardware support for printers, scanners, drawers
Cross-Reference: For detailed offline conflict resolution logic, see Chapter 05 Section 1.16.3.
Next: Chapter 15: Tenant Admin Portal covers the Merchant Dashboard.
Document Information
| Attribute | Value |
|---|---|
| Version | 5.0.0 |
| Created | 2025-12-29 |
| Updated | 2026-02-25 |
| Author | Claude Code |
| Status | Active |
| Part | V - Frontend |
| Chapter | 14 of 32 |
This chapter is part of the POS Blueprint Book. All content is self-contained.