# Sales Module — Architecture Map

> **Last updated:** 2026-06-10
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Sales module is the **Point-of-Sale (POS) and sales management** module. It manages:
- POS interface for creating sales transactions
- Invoice generation with sequential numbering (INV-YYYY-NNNNNN)
- Multi-customer-type sales (members, players, guests)
- Sales package bundles (grouped items at discounted prices)
- Sales refunds with optional stock return
- Sales representative commission tracking (per-sale and referral)
- Customer-specific pricing (custom price lists per member)
- Sales reporting (daily, monthly, by-item)
- Member referral commission on activation

It does **NOT** directly manage:
- Stock levels (delegates to Inventory/StockService)
- Payment processing (delegates to Payments/PaymentService)
- Receipt generation (Payments module creates receipts)
- Accounting journal entries (Accounting listens to events)

---

## 2. Directory & File Structure

```
app/Modules/Sales/
├── bootstrap.php                              # Menu + permissions + event listeners
├── Routes.php                                 # Web routes (28 routes)
├── Controllers/
│   ├── SaleController.php                    # Main controller (POS, CRUD, refund, void)
│   ├── SaleReportController.php              # Daily/monthly/by-item reports
│   ├── CommissionController.php              # Representatives, commissions, customer pricing
│   ├── PackageController.php                 # Package CRUD
│   └── ReferralController.php                # Member referral commission report
├── Models/
│   ├── Sale.php                              # Main sale model (search, statuses)
│   ├── SaleItem.php                          # Line items model
│   ├── SaleRefund.php                        # Refund model
│   ├── Package.php                           # Bundle package model
│   └── PackageItem.php                       # Package component items
├── Services/
│   ├── SaleService.php                       # Core: createSale(), voidSale(), processPayment()
│   ├── RefundService.php                     # processRefund() with stock return
│   ├── CommissionService.php                 # Auto-commission on sale.completed event
│   ├── ReferralCommissionService.php         # Commission on member.activated event
│   ├── InvoiceNumberGenerator.php            # Sequential INV/REF number generation
│   ├── InventoryPaymentService.php           # Thin adapter for guest payments
│   ├── PackageService.php                    # Package create/update with items
│   └── CustomerPricingService.php            # Custom price lists per member
└── Views/
    ├── pos/
    │   ├── index.php                         # POS screen
    │   └── invoice.php                       # Print invoice view
    ├── sales/
    │   ├── index.php                         # Sales list with filters
    │   ├── show.php                          # Sale detail with items + refunds
    │   ├── refund.php                        # Refund form
    │   └── member_history.php                # Member purchase history
    ├── packages/
    │   ├── index.php                         # Package list
    │   ├── form.php                          # Package create/edit form
    │   └── show.php                          # Package detail
    ├── commissions/
    │   ├── representatives.php               # Rep list
    │   └── report.php                        # Commission report
    ├── referrals/
    │   └── index.php                         # Member referral commission report
    └── reports/
        ├── daily.php                         # Daily sales report
        ├── monthly.php                       # Monthly sales report
        └── by_item.php                       # Sales by item report
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `sales` Table (0 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| invoice_number | varchar(50) | NO | UNI | Format: INV-YYYY-NNNNNN |
| warehouse_id | bigint unsigned | NO | MUL | FK to warehouses |
| customer_type | varchar(20) | NO | MUL | 'member', 'player', 'guest' |
| member_id | bigint unsigned | YES | MUL | FK to members (null for guests) |
| player_id | bigint unsigned | YES | MUL | FK to players |
| guest_name | varchar(200) | YES | | Guest customer name |
| guest_phone | varchar(30) | YES | | Guest phone |
| subtotal | decimal(15,2) | NO | | Sum of line totals before discount |
| discount_amount | decimal(15,2) | NO | | Overall sale discount |
| discount_reason | varchar(200) | YES | | |
| tax_amount | decimal(15,2) | NO | | Total tax |
| total_amount | decimal(15,2) | NO | | Final amount (subtotal + tax - discount) |
| currency_code | varchar(3) | YES | | Default: 'EGP' |
| exchange_rate | decimal(15,6) | YES | | Default: 1.000000 |
| representative_id | int unsigned | YES | | FK to sales_representatives |
| amount_paid | decimal(15,2) | NO | | Amount actually paid |
| amount_refunded | decimal(15,2) | NO | | Running total of refunds |
| payment_id | bigint unsigned | YES | MUL | FK to payments (created by SaleService) |
| payment_method | varchar(30) | YES | | cash/card/credit |
| status | varchar(30) | NO | MUL | Default: 'draft' |
| sold_by | bigint unsigned | YES | MUL | FK to employees |
| sale_date | date | NO | MUL | |
| notes | text | YES | | |
| branch_id | bigint unsigned | YES | | FK to branches |
| is_archived | tinyint(1) | NO | | Default: 0 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |
| updated_by | bigint unsigned | YES | | |

**Status values:** draft, completed, partially_refunded, fully_refunded, voided

### 3.2 `sale_items` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| sale_id | bigint unsigned | NO | MUL | FK to sales |
| item_id | bigint unsigned | YES | MUL | FK to inventory_items (null if package) |
| package_id | bigint unsigned | YES | MUL | FK to packages (null if individual item) |
| item_name_ar | varchar(300) | NO | | Snapshot of item name at sale time |
| quantity | decimal(15,3) | NO | | |
| unit_price | decimal(15,2) | NO | | |
| discount_amount | decimal(15,2) | NO | | Default: 0.00 |
| tax_amount | decimal(15,2) | NO | | Default: 0.00 |
| line_total | decimal(15,2) | NO | | qty * price + tax - discount |
| cost_price | decimal(15,2) | YES | | COGS at time of sale |
| batch_id | bigint unsigned | YES | MUL | FK to item_batches (for expiry tracking) |
| is_refunded | tinyint(1) | NO | | Default: 0 |
| refunded_quantity | decimal(15,3) | NO | | Default: 0.000 |

### 3.3 `sale_refunds` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| sale_id | bigint unsigned | NO | MUL | FK to sales |
| refund_number | varchar(50) | NO | UNI | Format: REF-YYYY-NNNNNN |
| refund_amount | decimal(15,2) | NO | | |
| reason | text | NO | | Required reason |
| return_to_stock | tinyint(1) | NO | | Whether items returned to inventory |
| warehouse_id | bigint unsigned | YES | MUL | Target warehouse for returned stock |
| payment_id | bigint unsigned | YES | MUL | FK to payments |
| refunded_by | bigint unsigned | YES | | FK to employees |
| refund_date | date | NO | MUL | |
| created_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |

### 3.4 `packages` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(30) | NO | UNI | Unique package code |
| name_ar | varchar(200) | NO | | Arabic name |
| name_en | varchar(200) | YES | | English name |
| description_ar | text | YES | | |
| package_price_member | decimal(15,2) | NO | | Price for members |
| package_price_nonmember | decimal(15,2) | NO | | Price for non-members |
| items_total_price | decimal(15,2) | NO | | Sum of individual item prices |
| active_from | date | YES | | Start of validity |
| active_to | date | YES | | End of validity |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| is_archived | tinyint(1) | NO | | Default: 0 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.5 `package_items` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| package_id | bigint unsigned | NO | MUL | FK to packages |
| item_id | bigint unsigned | NO | MUL | FK to inventory_items |
| quantity | decimal(15,3) | NO | | Default: 1.000 |
| sort_order | int | NO | | Default: 0 |

### 3.6 `sales_representatives` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | int unsigned | NO | PRI | auto_increment |
| employee_id | int unsigned | YES | MUL | FK to employees (optional) |
| code | varchar(20) | NO | UNI | Unique rep code |
| name_ar | varchar(255) | NO | | |
| name_en | varchar(255) | YES | | |
| phone | varchar(20) | YES | | |
| email | varchar(100) | YES | | |
| commission_type | enum | NO | | 'flat', 'percentage', 'tiered' |
| commission_rate | decimal(5,2) | NO | | |
| target_amount | decimal(15,2) | YES | | Sales target |
| is_active | tinyint(1) | NO | | Default: 1 |

### 3.7 `sales_commissions` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | int unsigned | NO | PRI | auto_increment |
| representative_id | int unsigned | NO | MUL | FK to sales_representatives |
| sale_id | int unsigned | NO | MUL | FK to sales |
| sale_amount | decimal(15,2) | NO | | Total sale amount |
| commission_rate | decimal(5,2) | NO | | Rate at time of calculation |
| commission_amount | decimal(15,2) | NO | | Calculated commission |
| period_month | date | NO | MUL | Commission period |
| status | enum | NO | MUL | 'calculated', 'approved', 'paid' |
| paid_date | date | YES | | |
| payment_reference | varchar(100) | YES | | |
| created_at | datetime | NO | | |

### 3.8 `member_referral_commissions` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| representative_id | int unsigned | NO | MUL | FK to sales_representatives |
| member_id | bigint unsigned | NO | MUL | FK to members (referred member) |
| membership_value | decimal(15,2) | NO | | Membership value at activation |
| commission_rate | decimal(5,2) | NO | | Rate at time |
| commission_amount | decimal(15,2) | NO | | Calculated commission |
| commission_date | date | NO | | |
| status | enum | NO | MUL | 'pending', 'approved', 'paid' |
| paid_at | datetime | YES | | |

### 3.9 `customer_price_lists` and `customer_item_prices` Tables

Custom per-member pricing overrides for inventory items.

---

## 4. Core Business Flows

### 4.1 Create Sale (POS)

```
1. POST /sales (SaleController@store)
   - Parse items/packages from form
   - Validate: warehouse required, at least 1 item, total > 0
   - Credit limit check for member customers (CreditLimitService)
   - Multi-currency exchange rate resolution

2. SaleService::createSale() [inside transaction]
   - Generate invoice number (INV-YYYY-NNNNNN)
   - Calculate subtotal, tax, discount, total
   - For each line item:
     - If item has BOM: deduct BOM components instead
     - If item tracking_type=expiry: FEFO allocation via BatchService
     - Otherwise: StockService::moveStock(sale_out)
   - For package items: expand components and deduct each
   - Create payment via PaymentService (members) or InventoryPaymentService (guests)
   - Link payment_id to sale

3. After commit: dispatch 'sale.completed' event
   - CommissionService calculates rep commission
   - Accounting posts GL entries
```

### 4.2 Void Sale

```
1. POST /sales/{id}/void (reason required)
2. SaleService::voidSale() [inside transaction]
   - Reverse stock for each line item (return_in movements)
   - Set status='voided'
   - Void linked payment via PaymentService::voidPayment()
3. Dispatch 'sale.voided' event
   - Accounting reverses GL entries
```

### 4.3 Refund

```
1. POST /sales/{id}/refund (amount, reason, items)
2. RefundService::processRefund() [inside transaction]
   - Validate: not voided/fully_refunded, amount <= remaining
   - Generate refund number (REF-YYYY-NNNNNN)
   - If return_to_stock: create return_in movements for each item
   - Update sale_items.refunded_quantity
   - Update sales.amount_refunded, status
3. Dispatch 'sale.refunded' event
   - Accounting posts refund GL entries
```

### 4.4 Member Referral Commission

```
1. Member activates (member.activated event)
2. ReferralCommissionService::onMemberActivated()
   - Check member has referred_by_representative_id
   - Calculate commission: membership_value * rep.commission_rate / 100
   - Insert into member_referral_commissions (idempotent check)
```

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `sale.completed` | `{sale_id, invoice_number, member_id, customer_type, total_amount}` | SaleService::createSale |
| `sale.voided` | `{sale_id, member_id, total_amount, invoice_number, reason}` | SaleService::voidSale |
| `sale.refunded` | `{sale_id, refund_id, refund_number, amount}` | RefundService::processRefund |

---

## 6. Events Consumed

| Event | Listener | Action |
|-------|----------|--------|
| `sale.completed` | CommissionService | Calculate rep commission (priority 50) |
| `sale.completed` | Accounting | Post GL journal entry |
| `sale.voided` | Accounting | Reverse GL journal entry |
| `sale.refunded` | Accounting | Post refund GL entry |
| `member.activated` | ReferralCommissionService | Calculate referral commission (priority 60) |

---

## 7. Cross-Module Dependencies

### 7.1 Sales IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Inventory | StockService (stock deduction), BatchService (FEFO), BomService (BOM expansion), CreditLimitService (credit check), Warehouse model |
| Payments | PaymentService (process/void payments) |
| Accounting | MultiCurrencyService (exchange rates) |
| Members | Member model (customer lookup) |

### 7.2 Other modules that IMPORT FROM Sales:

| Module | What it imports |
|--------|----------------|
| Accounting | Listens to sale.completed, sale.voided, sale.refunded events |
| Members | References sales_representatives (referred_by_representative_id) |
| ActivitySubscriptions | SaleController referenced for patterns |

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| sales.view | View sales list and details |
| sales.create | Create a new sale (POS access) |
| sales.void | Void an existing sale |
| sales.refund | Process a refund |
| package.view | View packages |
| package.manage | Create/edit packages |
| report.sales | Access sales reports |
| sales.commission.view | View commissions and representatives |
| sales.commission.manage | Manage representatives and commissions |
| sales.pricing.view | View customer pricing |
| sales.pricing.manage | Manage customer price lists |
| sales.referral.view | View member referral report |

---

## 9. Number Generation

| Type | Format | Table.Column | Example |
|------|--------|--------------|---------|
| Invoice | INV-YYYY-NNNNNN | sales.invoice_number | INV-2026-000001 |
| Refund | REF-YYYY-NNNNNN | sale_refunds.refund_number | REF-2026-000001 |

Generation uses MAX(column) LIKE pattern — same race condition risk as Members module.

---

## 10. High-Risk Areas

### 10.1 Stock Deduction in createSale (CRITICAL)
- Multiple deduction paths: direct item, BOM components, package expansion, batch FEFO
- If any path fails mid-transaction, stock can be inconsistent
- SELECT FOR UPDATE in StockService mitigates but doesn't eliminate race conditions

### 10.2 Payment Integration
- SaleService calls PaymentService directly — if payment fails after stock deduction, transaction rolls back
- Guest payments use a thin adapter (InventoryPaymentService) that wraps PaymentService with member_id=null
- Voiding a sale voids the linked payment which triggers payment.voided → Treasury custody reversal

### 10.3 BOM + Package Expansion
- Packages expand component items and deduct each separately
- BOM items deduct component materials instead of the finished item
- If a sale item has BOTH a BOM and is also in a package, behavior could compound

### 10.4 Refund Stock Return
- refunded_quantity tracked per sale_item — must never exceed original quantity
- Stock returned to the same warehouse as the original sale
- No batch tracking on returns (items go back as standard stock even if originally batch-tracked)

### 10.5 Commission Calculation
- CommissionService runs AFTER commit (event-driven) — if it fails, sale exists but commission doesn't
- Uses flat rate from sales_representatives.commission_rate — no tiered logic despite enum supporting 'tiered'
- DB schema has `commission_type` enum ('flat','percentage','tiered') but code only handles 'percentage' and 'fixed_per_invoice'

---

## 11. Known Patterns & Gotchas

1. **Multi-currency support**: exchange_rate stored on sale, but currency resolution happens at sale creation only
2. **Customer types**: 'member' (uses PaymentService with member_id), 'player' (InventoryPaymentService with guest_name), 'guest' (InventoryPaymentService)
3. **Package stock deduction**: When selling a package, the package itself has no stock — its component items are deducted individually with multiplied quantities
4. **Item name snapshot**: sale_items.item_name_ar stores the name at sale time (protects against future item renames)
5. **Cost price tracking**: sale_items.cost_price stored for profit margin calculation in reports
6. **Credit limit check**: Only enforced for member customers, not guests/players. If credit_limit=0, check passes (unlimited)
7. **No sales table data yet**: 0 sales in production (feature recently deployed or not yet used)

---

## 12. Configuration Dependencies

| Config | Purpose |
|--------|---------|
| Currency exchange rates | MultiCurrencyService for non-EGP sales |
| sales_representatives | Commission rates and types |
| customer_price_lists | Per-member custom pricing |

---

## Appendix: Files to Check Before Modifying Sales

1. `app/Modules/Inventory/Services/StockService.php` — Stock movement core
2. `app/Modules/Inventory/Services/BatchService.php` — FEFO batch allocation
3. `app/Modules/Inventory/Services/BomService.php` — BOM component deduction
4. `app/Modules/Inventory/Services/CreditLimitService.php` — Credit limit enforcement
5. `app/Modules/Payments/Services/PaymentService.php` — Payment creation/void
6. `app/Modules/Accounting/bootstrap.php` — GL posting event listeners
7. `app/Modules/Accounting/Services/MultiCurrencyService.php` — Exchange rates
