# POS System — Design Document

**Date:** 2026-02-27
**Phase:** Retail POS (Phase 1), Restaurant mode planned for Phase 2

---

## 1. Overview

Full-page Point of Sale system inside Moon ERP (`/app/pos`). Fullscreen layout without sidebar, optimized for tablet + desktop with touch support. Online-first with offline fallback.

## 2. Requirements Summary

| Requirement | Decision |
|-------------|----------|
| Type | Retail POS (Phase 1), Restaurant (Phase 2) |
| Devices | Tablet + Desktop (responsive, touch-friendly) |
| Payment | Cash, Card, Credit (on account), Check, Split payment |
| Sessions | Yes — open/close with reconciliation report |
| Connectivity | Online-first, offline fallback with auto-sync |
| Structure | Page inside Moon ERP at `/app/pos` (fullscreen, no sidebar) |
| Customers | Optional (default: Walk-in / cash customer) |
| Returns | From within POS |
| Discounts | Manual (line/invoice) + Coupon codes |
| Multi-POS | Multiple cashiers per branch, each with independent session |

## 3. Screen Layout

### 3.1 Main POS Screen

```
┌──────────────────────────────────────────────────────────────────┐
│ 🔍 [بحث/باركود...........] │ 👤 عميل نقدي ▼ │ ⏰ وردية #3 │ ⚙ │ ✕ │
├────────────────────────────────────────┬─────────────────────────┤
│ [كل الفئات] [مشروبات] [وجبات] [حلويات]│     🛒 السلة (3)        │
│                                        │                         │
│  ┌────────┐ ┌────────┐ ┌────────┐     │ ┌─ بيبسي ───── x2 ───┐ │
│  │  📷    │ │  📷    │ │  📷    │     │ │ 5.00  ➖ 2 ➕  10.00│ │
│  │ بيبسي  │ │ شيبس  │ │  ماء   │     │ └─────────────────────┘ │
│  │ 5.00   │ │ 3.50   │ │ 1.00   │     │ ┌─ شيبس ───── x1 ───┐ │
│  └────────┘ └────────┘ └────────┘     │ │ 3.50  ➖ 1 ➕   3.50│ │
│  ┌────────┐ ┌────────┐ ┌────────┐     │ └─────────────────────┘ │
│  │  📷    │ │  📷    │ │  📷    │     │                         │
│  │ سندويش │ │  عصير  │ │  قهوة  │     ├─────────────────────────┤
│  │ 12.00  │ │ 8.00   │ │ 15.00  │     │ المجموع:        13.50  │
│  └────────┘ └────────┘ └────────┘     │ الضريبة (15%):    2.03  │
│                                        │ ─────────────────────── │
│                                        │ الإجمالي:        15.53  │
│  [1] [2] [3] [كمية] [خصم%] [حذف]     ├─────────────────────────┤
│  [4] [5] [6] [كوبون] [سعر]  [🗑]      │ [💵 كاش ] [💳 بطاقة]   │
│  [7] [8] [9] [  .  ] [  0  ] [⏎]      │ [📝 آجل ] [✂ تقسيم]   │
│                                        │ [↩ مرتجع] [🎫 معلق]   │
└────────────────────────────────────────┴─────────────────────────┘
```

**Sections:**
- **Top bar:** Search/barcode input + customer picker + session info + settings + exit
- **Left top:** Category tabs (horizontal, scrollable)
- **Left center:** Product grid (cards with image + name + price), touch-friendly large tiles
- **Left bottom:** Numpad + quick action buttons (qty, discount%, coupon, price, delete)
- **Right top:** Cart — each item with ➕➖ quantity controls
- **Right center:** Totals summary (subtotal, tax, total)
- **Right bottom:** Payment buttons (cash, card, credit, split, return, hold)

### 3.2 Session Open Dialog

```
┌─────────────────────────────────────┐
│         فتح وردية جديدة             │
│                                     │
│  الكاشير:  أحمد محمد (تلقائي)      │
│  الفرع:    الفرع الرئيسي ▼         │
│  المخزن:   مخزن البيع ▼            │
│  نقطة البيع: كاشير 1 ▼            │
│                                     │
│  المبلغ الافتتاحي: [______] SAR    │
│  ملاحظات: [__________________]     │
│                                     │
│         [ 🟢 فتح الوردية ]          │
└─────────────────────────────────────┘
```

- Shown on entering `/app/pos` when no active session exists
- Cashier = current logged-in user (auto)
- Select branch + warehouse + POS terminal
- Enter opening cash balance

### 3.3 Session Close Dialog

```
┌─────────────────────────────────────────────┐
│            إغلاق الوردية #3                 │
│                                             │
│  ┌───────────────┬──────────┬──────────┐    │
│  │ البند         │ المتوقع  │ الفعلي   │    │
│  ├───────────────┼──────────┼──────────┤    │
│  │ كاش           │ 1,250.00 │ [______] │    │
│  │ بطاقات        │   850.00 │ [______] │    │
│  │ آجل           │   300.00 │   300.00 │    │
│  ├───────────────┼──────────┼──────────┤    │
│  │ الإجمالي      │ 2,400.00 │ 2,380.00 │    │
│  │ الفرق         │          │   -20.00 │    │
│  └───────────────┴──────────┴──────────┘    │
│                                             │
│  عمليات البيع: 45    المرتجعات: 2           │
│  ملاحظات الإغلاق: [__________________]     │
│                                             │
│  [ 🔴 إغلاق الوردية ]  [ 🖨 طباعة التقرير ] │
└─────────────────────────────────────────────┘
```

- Cashier counts physical cash and enters actual amounts
- System calculates expected amounts (opening + cash sales - cash returns)
- Shows variance (shortage/surplus)
- Report printable

### 3.4 Payment Dialog

```
┌─────────────────────────────────────────────────────┐
│                    💳 الدفع                          │
│                                                     │
│         الإجمالي المطلوب: 155.25 SAR               │
│                                                     │
│  ┌─────────────────────────────────────────────┐    │
│  │ طريقة الدفع          المبلغ         حذف    │    │
│  ├─────────────────────────────────────────────┤    │
│  │ 💵 كاش              [200.00]        ✕      │    │
│  └─────────────────────────────────────────────┘    │
│                                                     │
│  [+ إضافة طريقة دفع أخرى]                          │
│                                                     │
│  ┌───────────────────┐                              │
│  │ المطلوب:  155.25  │    [7] [8] [9]              │
│  │ المدفوع:  200.00  │    [4] [5] [6]              │
│  │ الباقي:    44.75  │    [1] [2] [3]              │
│  │                   │    [.] [0] [⌫]              │
│  └───────────────────┘    [50] [100] [200]          │
│                           (أزرار مبالغ سريعة)        │
│                                                     │
│           [ ✅ إتمام الدفع وطباعة الإيصال ]          │
│           [    ⬅ رجوع للسلة    ]                    │
└─────────────────────────────────────────────────────┘
```

**Split Payment:** Add multiple payment methods, each with its own amount. Total must equal or exceed invoice total. Cash overpayment calculates change.

**Quick amount buttons:** [50] [100] [200] for fast cash entry.

**After payment success:**
- Auto-creates sales invoice (posted) + payment record (posted)
- Shows success screen with receipt print option
- Returns to main screen after 3s or on button press

### 3.5 Return Dialog

```
┌──────────────────────────────────────────────────┐
│              ↩ مرتجع                              │
│                                                  │
│  🔍 [رقم الفاتورة أو باركود الإيصال...]         │
│                                                  │
│  ┌──────────────────────────────────────────┐    │
│  │ فاتورة: POS-2026-0045                   │    │
│  │ التاريخ: 2026-02-27  العميل: نقدي       │    │
│  ├──────────────────────────────────────────┤    │
│  │ ☑ بيبسي        x2   5.00   10.00       │    │
│  │   ↳ كمية الإرجاع: [1]                   │    │
│  │ ☐ شيبس         x1   3.50    3.50       │    │
│  │ ☑ سندويش       x1  12.00   12.00       │    │
│  │   ↳ كمية الإرجاع: [1]                   │    │
│  └──────────────────────────────────────────┘    │
│                                                  │
│  سبب الإرجاع: [منتج تالف ▼]                     │
│  المبلغ المسترد: 17.00 SAR                      │
│  طريقة الاسترداد: [💵 كاش ▼]                     │
│                                                  │
│  [ ✅ تنفيذ المرتجع ]    [ ⬅ إلغاء ]             │
└──────────────────────────────────────────────────┘
```

**Flow:**
1. Search by invoice number or scan receipt barcode
2. Display original invoice items
3. Select items to return + quantity
4. Choose return reason (damaged, error, customer request, other)
5. Choose refund method (cash, card refund, customer credit)
6. Creates: sales return (auto-post) + inventory restoration + accounting entry

## 4. Sale Flow

```
1. Add Products
   ├── Tap product card → adds to cart (qty 1)
   ├── Tap again → qty +1
   ├── Scan barcode → auto-add (or increment if exists)
   ├── Search by name/code → select from results
   └── Product with variants → quick popup to select variant

2. Modify Cart
   ├── ➕ ➖ buttons to adjust quantity
   ├── Numpad: select item → enter number → [qty] = set quantity
   ├── Numpad: select item → enter number → [price] = custom price
   ├── Numpad: select item → enter number → [discount%] = line discount
   ├── Swipe left on item (tablet) or [delete] = remove from cart
   └── [coupon] → enter code → apply discount

3. Payment
   └── Tap payment button → payment dialog opens

4. Completion
   ├── Creates sales invoice (auto-posted, no approval workflow)
   ├── Records payment(s)
   ├── Prints receipt
   └── Returns to main screen
```

**Special Cases:**

| Case | Behavior |
|------|----------|
| Out of stock | Warning — can still sell if setting allows |
| Price below minimum | Warning — requires manager override |
| Hold order | Save cart temporarily → start new order → retrieve later |
| Held orders | [🎫] button shows list of held orders to restore |

## 5. Online-First with Offline Fallback

```
Transaction
    │
    ▼
Internet available? ──── Yes ──→ Send to API directly ✅
    │
    No (or API failed)
    │
    ▼
Save to IndexedDB (offline queue) 🟡
    │
    ▼
Continue working normally offline
    │
    ▼
Connection restored? ──→ Auto-sync queue in order (FIFO) 🟢
```

### Online Mode (Default)
- All operations go directly to API
- Light cache of products/categories for faster display
- Real-time stock checking

### Offline Fallback (When Connection Drops)
- Detected via: `navigator.onLine` + API call failure
- Transactions saved to IndexedDB sync queue
- POS continues working normally with cached data
- Status indicator in header: 🟢 Online | 🟡 X pending sync | 🔴 Offline

### Auto-Sync (When Connection Returns)
- Queue processed in FIFO order
- Each successful sync → removed from queue
- Failed items → retry with exponential backoff
- Conflict resolution: server timestamp wins

### Cached Data (IndexedDB)
- Products + prices + barcodes — refreshed on session open + every 30 min
- Categories — refreshed on session open
- Customers (last 500) — refreshed on session open
- Tax rates — refreshed on session open
- Active session data — continuous
- Held orders — continuous

## 6. Coupons

**Types:**
- **Percentage** — discount % off total (with optional max amount cap)
- **Fixed amount** — flat discount
- **Free product** — buy X get Y free

**Validation:** Via API when online, cached coupon list when offline.

**Dialog:** Enter code → validate → show discount details → apply to cart.

## 7. Technical Architecture

### 7.1 File Structure

```
src/app/features/pos/
├── pos.routes.ts                    # Route: /pos (fullscreen layout)
├── pos-layout/
│   └── pos-layout.component.ts      # Main fullscreen shell
├── components/
│   ├── pos-header/                  # Top bar (search + customer + session)
│   ├── product-grid/                # Product grid with category tabs
│   ├── cart-panel/                  # Cart + totals summary
│   ├── numpad/                      # Number pad + quick actions
│   ├── payment-dialog/              # Payment screen
│   ├── return-dialog/               # Returns
│   ├── session-open-dialog/         # Open session
│   ├── session-close-dialog/        # Close session + report
│   ├── held-orders-dialog/          # Held/parked orders list
│   ├── variant-picker/              # Variant selection popup
│   └── receipt-preview/             # Receipt display/print
├── services/
│   ├── pos.service.ts               # API calls (sale, payment, return)
│   ├── pos-session.service.ts       # Session management
│   ├── pos-offline.service.ts       # IndexedDB + Sync Queue
│   ├── pos-cart.service.ts          # Cart logic (signal-based)
│   └── pos-receipt.service.ts       # Receipt generation/printing
└── models/
    └── pos.model.ts                 # All POS interfaces
```

### 7.2 Integration with Existing System

| Operation | API Endpoint | Notes |
|-----------|-------------|-------|
| Sale | `POST /api/sales/invoices` + auto-post | Regular invoice with status=posted |
| Payment | `POST /api/sales/payments` + auto-post | Same payment system |
| Return | `POST /api/sales/returns` + auto-post | Same returns system |
| Products | `GET /api/core/products` | Cached in IndexedDB |
| Categories | `GET /api/core/product-categories` | Cached |
| Customers | `GET /api/core/partners?is_customer=1` | Cached (last 500) |
| Tax Rates | `GET /api/accounting/tax-rates` | Cached |
| Warehouses | `GET /api/inventory/warehouses` | For session setup |
| Sessions | **New: `POST /api/pos/sessions`** | Backend needed |
| Coupons | **New: `POST /api/pos/coupons/validate`** | Backend needed |

### 7.3 New Backend Endpoints Required

1. `POST /api/pos/sessions` — Open session
2. `PUT /api/pos/sessions/{id}/close` — Close session with counts
3. `GET /api/pos/sessions/{id}` — Session details + summary
4. `GET /api/pos/sessions` — List sessions (with filters)
5. `POST /api/pos/coupons/validate` — Validate coupon code
6. `GET /api/pos/coupons` — List active coupons (for offline cache)

### 7.4 Key Services Design

**PosCartService (signal-based):**
```
- items: Signal<CartItem[]>
- selectedItem: Signal<CartItem | null>
- subtotal: computed
- taxAmount: computed
- discountAmount: computed
- total: computed
- addProduct(product, variant?)
- updateQuantity(index, qty)
- setPrice(index, price)
- setDiscount(index, percent)
- removeItem(index)
- applyCoupon(code)
- holdOrder()
- restoreOrder(id)
- clear()
```

**PosOfflineService:**
```
- connectionStatus: Signal<'online' | 'pending' | 'offline'>
- pendingCount: Signal<number>
- initDB() — open IndexedDB
- cacheProducts(products[])
- getCachedProducts() → products[]
- enqueue(transaction) — add to sync queue
- processQueue() — sync pending transactions
- onConnectionChange() — auto-trigger sync
```

### 7.5 Route Configuration

```typescript
// pos.routes.ts
{
  path: 'pos',
  component: PosLayoutComponent,  // fullscreen, no main-layout
  canActivate: [authGuard],
  children: []  // single-page, no child routes
}
```

The POS route uses its own layout component (no sidebar, no topbar from main ERP). Exit button navigates back to `/dashboard`.

### 7.6 Receipt Printing

- Generate receipt as HTML
- Use `window.print()` with `@media print` CSS for thermal printer format
- Receipt includes: store name, date/time, items, totals, payment method, barcode
- Optional: connect to ESC/POS compatible printers via Web Serial API (future)

## 8. Calculations

### Line Total
```
lineGross = qty × unitPrice
lineAfterDiscount = lineGross - (lineGross × lineDiscount% / 100)
lineTax = lineAfterDiscount × taxRate%
lineTotal = lineAfterDiscount + lineTax
```

### Invoice Total
```
subtotal = Σ(lineAfterDiscount)
couponDiscount = based on coupon type (% or fixed)
overallDiscount = manual discount (% or fixed)
totalDiscount = couponDiscount + overallDiscount
subtotalAfterDiscount = subtotal - totalDiscount
taxAmount = Σ(lineTax) adjusted proportionally for overall discount
total = subtotalAfterDiscount + taxAmount
```

### Session Reconciliation
```
expectedCash = openingBalance + cashSales - cashReturns
variance = actualCash - expectedCash
```

## 9. Phase 2: Restaurant Mode (Future)

Will add on top of Retail POS:
- Table management (floor plan, table status)
- Order types: dine-in, takeaway, delivery
- Kitchen Display System (KDS)
- Item modifiers (no onion, extra cheese, etc.)
- Course management (starter, main, dessert)
- Bill splitting by seat
- Tips

---

**Approved by:** User
**Next step:** Create implementation plan
