# Entités POS - Spécifications Détaillées

## Convention Générale

Toutes les entités POS suivent les patterns existants du projet :

```php
namespace App\Entity\Pos;

use App\Entity\Traits\TimestampableEntity;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
use Gedmo\Mapping\Annotation as Gedmo;

#[ORM\Entity(repositoryClass: XxxRepository::class)]
#[ORM\Table(name: 'pos_xxx')]
#[ORM\Index(name: 'idx_pos_xxx_company', columns: ['company_id'])]
#[ORM\Index(name: 'idx_pos_xxx_deleted', columns: ['deleted_at'])]
#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: false)]
class Xxx
{
    use TimestampableEntity;
    use SoftDeleteableEntity;
}
```

**Prefix table :** `pos_` pour toutes les tables POS.

---

## 1. PosSettings

Configuration complète de la boutique, 1 par Company.

```
Table: pos_settings
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK, auto | Identifiant |
| company_id | integer | FK Company, UNIQUE | Entreprise |
| shop_name | string(255) | nullable | Nom boutique (défaut: company.name) |
| currency | string(3) | default 'XOF' | Code devise ISO 4217 |
| currency_symbol | string(10) | default 'FCFA' | Symbole devise |
| tax_rate | decimal(5,2) | default 18.00 | Taux TVA (%) |
| tax_included | boolean | default true | Prix TTC par défaut |
| decimal_places | integer | default 0 | Décimales affichées |
| low_stock_threshold | integer | default 10 | Seuil alerte stock bas |
| critical_stock_threshold | integer | default 3 | Seuil alerte critique |
| enable_negative_stock | boolean | default false | Autoriser stock négatif |
| allow_discount | boolean | default true | Remises autorisées |
| max_discount_percent | decimal(5,2) | default 50.00 | Remise max (%) |
| require_customer | boolean | default false | Client obligatoire |
| allow_credit_sales | boolean | default false | Vente à crédit |
| credit_limit | decimal(12,2) | default 0 | Plafond crédit client |
| enable_loyalty | boolean | default false | Programme fidélité |
| loyalty_points_per_unit | integer | default 1 | Points par unité monétaire |
| accepted_payment_methods | json | default ['cash'] | Moyens de paiement |
| mobile_money_providers | json | default [] | Fournisseurs mobile money |
| require_employee_pin | boolean | default true | PIN caissier |
| auto_logout_minutes | integer | default 30 | Déconnexion auto |
| allow_employee_void | boolean | default false | Annulation par employé |
| allow_employee_refund | boolean | default false | Remboursement par employé |
| low_stock_alert_enabled | boolean | default true | Alerte email stock bas |
| daily_report_enabled | boolean | default true | Rapport quotidien |
| large_sale_alert_enabled | boolean | default false | Alerte grosse vente |
| large_sale_threshold | decimal(12,2) | default 500000 | Seuil grosse vente |
| receipt_header | text | nullable | En-tête ticket |
| receipt_footer | text | nullable | Pied ticket |
| receipt_show_tax | boolean | default true | Afficher TVA sur ticket |
| receipt_show_logo | boolean | default true | Logo sur ticket |
| barcode_format | string(20) | default 'EAN13' | Format code-barres |
| auto_generate_barcode | boolean | default true | Génération auto |
| auto_generate_sku | boolean | default true | Génération auto SKU |
| is_pos_enabled | boolean | default false | Module POS activé |
| created_at | datetime | auto | Date création |
| updated_at | datetime | auto | Date modification |

**Pas de soft-delete** : la config est liée au cycle de vie de la Company.

---

## 2. ProductCategory

Catégories de produits, hiérarchiques, scopées par Company.

```
Table: pos_product_category
Unique: (company_id, slug)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| uuid | string(36) | UNIQUE | UUID v7 |
| company_id | integer | FK Company | Entreprise |
| parent_id | integer | FK self, nullable | Catégorie parente |
| name | string(100) | required | Nom catégorie |
| slug | string(120) | auto-généré | Slug URL |
| description | text | nullable | Description |
| color | string(20) | default 'blue' | Couleur UI |
| icon | string(50) | default 'fa-tag' | Icône FontAwesome |
| image_media_id | integer | FK Media, nullable | Image catégorie |
| position | integer | default 0 | Ordre affichage |
| depth | integer | default 0 | Profondeur (max 3) |
| path | string(500) | nullable | Chemin complet |
| is_active | boolean | default true | Active |
| product_count | integer | default 0 | Compteur dénormalisé |
| created_at | datetime | | |
| updated_at | datetime | | |
| deleted_at | datetime | nullable | Soft-delete |

---

## 3. Product

Produit en vente dans la boutique.

```
Table: pos_product
Unique: (company_id, sku)
Index: (company_id, is_active), (company_id, barcode)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| uuid | string(36) | UNIQUE | UUID v7 |
| company_id | integer | FK Company | Entreprise |
| category_id | integer | FK ProductCategory, nullable | Catégorie |
| name | string(255) | required | Nom produit |
| slug | string(280) | auto-généré | Slug URL |
| description | text | nullable | Description |
| sku | string(50) | required | Référence interne |
| barcode | string(50) | nullable | Code-barres |
| barcode_type | string(20) | default 'EAN13' | Type code-barres |
| price | decimal(12,2) | required | Prix de vente |
| cost_price | decimal(12,2) | nullable | Prix d'achat |
| tax_rate | decimal(5,2) | nullable | TVA spécifique (null = défaut company) |
| is_tax_included | boolean | nullable | TTC spécifique (null = défaut) |
| current_stock | integer | default 0 | Stock actuel (dénormalisé) |
| reserved_stock | integer | default 0 | Stock réservé |
| min_stock | integer | nullable | Stock minimum (override company) |
| max_stock | integer | nullable | Stock maximum |
| unit | string(20) | default 'piece' | Unité (piece, kg, litre, m, m2, boite) |
| weight | decimal(10,3) | nullable | Poids (kg) |
| has_variants | boolean | default false | A des variantes |
| is_active | boolean | default true | En vente |
| is_featured | boolean | default false | Mis en avant |
| is_service | boolean | default false | Service (pas de stock) |
| allow_discount | boolean | default true | Remise autorisée |
| max_discount_percent | decimal(5,2) | nullable | Remise max spécifique |
| notes | text | nullable | Notes internes |
| tags | json | default [] | Tags libres |
| metadata | json | default {} | Données supplémentaires |
| total_sold | integer | default 0 | Total vendu (dénormalisé) |
| total_revenue | decimal(14,2) | default 0 | CA total (dénormalisé) |
| last_sold_at | datetime | nullable | Dernière vente |
| last_restocked_at | datetime | nullable | Dernier réapprovisionnement |
| created_by_id | integer | FK User | Créateur |
| created_at | datetime | | |
| updated_at | datetime | | |
| deleted_at | datetime | nullable | Soft-delete |

---

## 4. ProductVariant

Variantes d'un produit (taille, couleur, etc.).

```
Table: pos_product_variant
Unique: (product_id, sku)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| product_id | integer | FK Product | Produit parent |
| name | string(100) | required | Nom variante (ex: "Rouge - XL") |
| sku | string(50) | required | SKU variante |
| barcode | string(50) | nullable | Code-barres variante |
| price_adjustment | decimal(12,2) | default 0 | Ajustement prix (+/-) |
| cost_price | decimal(12,2) | nullable | Prix achat variante |
| current_stock | integer | default 0 | Stock variante |
| attributes | json | required | Ex: {"color":"rouge","size":"XL"} |
| is_active | boolean | default true | Active |
| position | integer | default 0 | Ordre affichage |
| created_at | datetime | | |
| updated_at | datetime | | |

**Pas de soft-delete** : suppression cascade avec le produit parent.

---

## 5. ProductMedia

Liaison produit-média pour images produits.

```
Table: pos_product_media
Unique: (product_id, media_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| product_id | integer | FK Product | Produit |
| media_id | integer | FK Media | Fichier média |
| is_primary | boolean | default false | Image principale |
| position | integer | default 0 | Ordre affichage |
| alt_text | string(255) | nullable | Texte alternatif |
| created_at | datetime | | |

---

## 6. Barcode

Gestion avancée des codes-barres.

```
Table: pos_barcode
Unique: (company_id, code)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| company_id | integer | FK Company | Entreprise |
| product_id | integer | FK Product, nullable | Produit lié |
| variant_id | integer | FK ProductVariant, nullable | Variante liée |
| code | string(50) | required | Valeur code-barres |
| type | string(20) | required | EAN13, EAN8, UPC, CODE128, QR |
| is_primary | boolean | default false | Principal pour le produit |
| created_at | datetime | | |

---

## 7. Supplier

Fournisseurs de la boutique.

```
Table: pos_supplier
Unique: (company_id, email)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| uuid | string(36) | UNIQUE | UUID v7 |
| company_id | integer | FK Company | Entreprise |
| name | string(255) | required | Nom fournisseur |
| contact_name | string(255) | nullable | Nom contact |
| email | string(255) | nullable | Email |
| phone | string(50) | nullable | Téléphone |
| address | string(500) | nullable | Adresse |
| city | string(100) | nullable | Ville |
| country | string(100) | default 'Sénégal' | Pays |
| tax_id | string(50) | nullable | Numéro fiscal |
| payment_terms | string(50) | nullable | Conditions paiement |
| notes | text | nullable | Notes |
| is_active | boolean | default true | Actif |
| total_orders | integer | default 0 | Nb commandes (dénormalisé) |
| total_amount | decimal(14,2) | default 0 | Montant total (dénormalisé) |
| last_order_at | datetime | nullable | Dernière commande |
| created_at | datetime | | |
| updated_at | datetime | | |
| deleted_at | datetime | nullable | Soft-delete |

---

## 8. SupplierProduct

Prix et référence d'un produit chez un fournisseur.

```
Table: pos_supplier_product
Unique: (supplier_id, product_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| supplier_id | integer | FK Supplier | Fournisseur |
| product_id | integer | FK Product | Produit |
| supplier_reference | string(100) | nullable | Référence fournisseur |
| purchase_price | decimal(12,2) | required | Prix d'achat |
| min_order_quantity | integer | default 1 | Quantité min commande |
| lead_time_days | integer | nullable | Délai livraison (jours) |
| is_preferred | boolean | default false | Fournisseur préféré |
| last_purchase_price | decimal(12,2) | nullable | Dernier prix |
| last_purchase_at | datetime | nullable | Dernier achat |
| created_at | datetime | | |
| updated_at | datetime | | |

---

## 9. StockMovement

Tout mouvement de stock est tracé.

```
Table: pos_stock_movement
Index: (product_id, created_at), (company_id, type, created_at)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| company_id | integer | FK Company | Entreprise |
| product_id | integer | FK Product | Produit |
| variant_id | integer | FK ProductVariant, nullable | Variante |
| type | string(20) | required | in, out, adjustment, return, transfer, loss, initial |
| quantity | integer | required | Quantité (positif ou négatif) |
| stock_before | integer | required | Stock avant mouvement |
| stock_after | integer | required | Stock après mouvement |
| unit_cost | decimal(12,2) | nullable | Coût unitaire |
| reference | string(100) | nullable | Réf (n° vente, n° commande) |
| reference_type | string(30) | nullable | sale, purchase_order, inventory, manual |
| reference_id | integer | nullable | ID entité liée |
| reason | string(500) | nullable | Motif |
| performed_by_id | integer | FK User | Utilisateur |
| created_at | datetime | | |

**Pas de soft-delete, pas d'update** : entité append-only (audit).

---

## 10. PurchaseOrder

Bons de commande fournisseur.

```
Table: pos_purchase_order
Index: (company_id, status), (supplier_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| uuid | string(36) | UNIQUE | UUID v7 |
| company_id | integer | FK Company | Entreprise |
| supplier_id | integer | FK Supplier | Fournisseur |
| order_number | string(30) | UNIQUE | Numéro commande (auto) |
| status | string(20) | default 'draft' | draft, sent, confirmed, partial, received, cancelled |
| total_amount | decimal(14,2) | default 0 | Montant total |
| tax_amount | decimal(12,2) | default 0 | Montant TVA |
| notes | text | nullable | Notes |
| expected_delivery_at | datetime | nullable | Date livraison prévue |
| received_at | datetime | nullable | Date réception |
| created_by_id | integer | FK User | Créateur |
| approved_by_id | integer | FK User, nullable | Approbateur |
| created_at | datetime | | |
| updated_at | datetime | | |

---

## 11. PurchaseOrderLine

Lignes de bon de commande.

```
Table: pos_purchase_order_line
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| purchase_order_id | integer | FK PurchaseOrder | Commande |
| product_id | integer | FK Product | Produit |
| variant_id | integer | FK ProductVariant, nullable | Variante |
| quantity_ordered | integer | required | Quantité commandée |
| quantity_received | integer | default 0 | Quantité reçue |
| unit_price | decimal(12,2) | required | Prix unitaire |
| total_price | decimal(12,2) | required | Prix total ligne |

---

## 12. Inventory

Session d'inventaire physique.

```
Table: pos_inventory
Index: (company_id, status)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| uuid | string(36) | UNIQUE | UUID v7 |
| company_id | integer | FK Company | Entreprise |
| name | string(255) | required | Nom inventaire |
| description | text | nullable | Description |
| status | string(20) | default 'draft' | draft, in_progress, completed, cancelled |
| type | string(20) | default 'full' | full, partial, category |
| category_id | integer | FK ProductCategory, nullable | Catégorie (si partial) |
| total_products | integer | default 0 | Nb produits comptés |
| total_discrepancies | integer | default 0 | Nb écarts |
| total_value_difference | decimal(14,2) | default 0 | Écart valeur |
| started_at | datetime | nullable | Début inventaire |
| completed_at | datetime | nullable | Fin inventaire |
| approved_by_id | integer | FK User, nullable | Validé par (manager) |
| created_by_id | integer | FK User | Créateur |
| created_at | datetime | | |
| updated_at | datetime | | |

---

## 13. InventoryLine

Comptage par produit dans un inventaire.

```
Table: pos_inventory_line
Unique: (inventory_id, product_id, variant_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| inventory_id | integer | FK Inventory | Inventaire |
| product_id | integer | FK Product | Produit |
| variant_id | integer | FK ProductVariant, nullable | Variante |
| expected_quantity | integer | required | Stock théorique |
| counted_quantity | integer | nullable | Stock compté |
| discrepancy | integer | nullable | Écart (compté - théorique) |
| notes | string(500) | nullable | Commentaire |
| counted_by_id | integer | FK User, nullable | Compteur |
| counted_at | datetime | nullable | Date comptage |

---

## 14. Customer

Clients de la boutique.

```
Table: pos_customer
Index: (company_id, is_active), (company_id, phone)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| uuid | string(36) | UNIQUE | UUID v7 |
| company_id | integer | FK Company | Entreprise |
| first_name | string(100) | required | Prénom |
| last_name | string(100) | required | Nom |
| phone | string(50) | nullable | Téléphone |
| email | string(255) | nullable | Email |
| address | string(500) | nullable | Adresse |
| city | string(100) | nullable | Ville |
| notes | text | nullable | Notes |
| is_active | boolean | default true | Actif |
| loyalty_points | integer | default 0 | Points fidélité |
| credit_balance | decimal(12,2) | default 0 | Solde crédit (dénormalisé) |
| total_purchases | integer | default 0 | Nb achats (dénormalisé) |
| total_spent | decimal(14,2) | default 0 | Total dépensé (dénormalisé) |
| last_purchase_at | datetime | nullable | Dernier achat |
| created_by_id | integer | FK User | Créateur |
| created_at | datetime | | |
| updated_at | datetime | | |
| deleted_at | datetime | nullable | Soft-delete |

---

## 15. CustomerCredit

Suivi des crédits/dettes clients.

```
Table: pos_customer_credit
Index: (customer_id, status)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| customer_id | integer | FK Customer | Client |
| sale_id | integer | FK Sale, nullable | Vente associée |
| type | string(20) | required | credit (dette), payment (règlement) |
| amount | decimal(12,2) | required | Montant |
| balance_before | decimal(12,2) | required | Solde avant |
| balance_after | decimal(12,2) | required | Solde après |
| due_date | datetime | nullable | Date échéance |
| notes | string(500) | nullable | Notes |
| recorded_by_id | integer | FK User | Enregistré par |
| created_at | datetime | | |

**Append-only** : pas de modification ni suppression.

---

## 16. Sale

Transaction de vente.

```
Table: pos_sale
Index: (company_id, created_at), (company_id, status), (register_session_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| uuid | string(36) | UNIQUE | UUID v7 |
| company_id | integer | FK Company | Entreprise |
| sale_number | string(30) | UNIQUE | N° vente (auto-généré) |
| register_session_id | integer | FK RegisterSession, nullable | Session caisse |
| customer_id | integer | FK Customer, nullable | Client |
| seller_id | integer | FK User | Vendeur |
| status | string(20) | default 'completed' | completed, refunded, partial_refund, voided |
| subtotal | decimal(14,2) | required | Sous-total HT |
| tax_amount | decimal(12,2) | default 0 | Montant TVA |
| discount_amount | decimal(12,2) | default 0 | Montant remise |
| discount_percent | decimal(5,2) | nullable | % remise globale |
| total | decimal(14,2) | required | Total TTC |
| amount_paid | decimal(14,2) | default 0 | Montant payé |
| change_amount | decimal(12,2) | default 0 | Monnaie rendue |
| credit_amount | decimal(12,2) | default 0 | Montant à crédit |
| payment_status | string(20) | default 'paid' | paid, partial, credit, pending |
| items_count | integer | default 0 | Nb articles |
| notes | text | nullable | Notes |
| voided_by_id | integer | FK User, nullable | Annulé par |
| voided_at | datetime | nullable | Date annulation |
| void_reason | string(500) | nullable | Motif annulation |
| created_at | datetime | | |
| updated_at | datetime | | |

**Pas de soft-delete** : les ventes ne sont jamais supprimées, seulement annulées (voided).

---

## 17. SaleLine

Lignes de vente (produits vendus).

```
Table: pos_sale_line
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| sale_id | integer | FK Sale | Vente |
| product_id | integer | FK Product | Produit |
| variant_id | integer | FK ProductVariant, nullable | Variante |
| product_name | string(255) | required | Nom produit (snapshot) |
| product_sku | string(50) | required | SKU (snapshot) |
| quantity | integer | required | Quantité |
| unit_price | decimal(12,2) | required | Prix unitaire |
| cost_price | decimal(12,2) | nullable | Prix achat (pour marge) |
| tax_rate | decimal(5,2) | default 0 | TVA appliquée |
| tax_amount | decimal(12,2) | default 0 | Montant TVA |
| discount_amount | decimal(12,2) | default 0 | Remise |
| discount_percent | decimal(5,2) | nullable | % remise ligne |
| total | decimal(14,2) | required | Total ligne |
| refunded_quantity | integer | default 0 | Quantité remboursée |

---

## 18. Payment

Paiements associés à une vente (multi-paiement possible).

```
Table: pos_payment
Index: (sale_id), (company_id, method, created_at)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| company_id | integer | FK Company | Entreprise |
| sale_id | integer | FK Sale | Vente |
| method | string(30) | required | cash, mobile_money, card, transfer, credit, loyalty |
| amount | decimal(12,2) | required | Montant payé |
| reference | string(100) | nullable | Référence (n° transaction mobile money, etc.) |
| provider | string(50) | nullable | Fournisseur (orange_money, wave, etc.) |
| status | string(20) | default 'completed' | completed, pending, failed, refunded |
| metadata | json | default {} | Données supplémentaires |
| processed_by_id | integer | FK User | Traité par |
| created_at | datetime | | |

---

## 19. Refund

Remboursements.

```
Table: pos_refund
Index: (company_id, created_at)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| uuid | string(36) | UNIQUE | UUID v7 |
| company_id | integer | FK Company | Entreprise |
| sale_id | integer | FK Sale | Vente originale |
| refund_number | string(30) | UNIQUE | N° remboursement |
| amount | decimal(12,2) | required | Montant remboursé |
| reason | string(500) | required | Motif |
| method | string(30) | required | Moyen remboursement |
| status | string(20) | default 'completed' | completed, pending, approved |
| items | json | required | [{product_id, quantity, amount}] |
| requested_by_id | integer | FK User | Demandé par |
| approved_by_id | integer | FK User, nullable | Approuvé par (manager) |
| approved_at | datetime | nullable | Date approbation |
| created_at | datetime | | |

---

## 20. Register

Caisse enregistreuse physique ou virtuelle.

```
Table: pos_register
Unique: (company_id, name)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| company_id | integer | FK Company | Entreprise |
| name | string(100) | required | Nom caisse (ex: "Caisse 1") |
| description | string(500) | nullable | Description |
| location | string(255) | nullable | Emplacement |
| is_active | boolean | default true | Active |
| is_default | boolean | default false | Caisse par défaut |
| current_session_id | integer | FK RegisterSession, nullable | Session en cours |
| created_at | datetime | | |
| updated_at | datetime | | |

---

## 21. RegisterSession

Session d'ouverture/fermeture de caisse.

```
Table: pos_register_session
Index: (register_id, status), (opened_by_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| register_id | integer | FK Register | Caisse |
| company_id | integer | FK Company | Entreprise |
| opened_by_id | integer | FK User | Ouvert par |
| closed_by_id | integer | FK User, nullable | Fermé par |
| status | string(20) | default 'open' | open, closed, suspended |
| opening_amount | decimal(12,2) | required | Fond de caisse ouverture |
| expected_closing_amount | decimal(12,2) | nullable | Montant attendu |
| actual_closing_amount | decimal(12,2) | nullable | Montant réel |
| difference | decimal(12,2) | nullable | Écart |
| total_sales | decimal(14,2) | default 0 | Total ventes session |
| total_refunds | decimal(12,2) | default 0 | Total remboursements |
| total_cash_in | decimal(12,2) | default 0 | Entrées caisse |
| total_cash_out | decimal(12,2) | default 0 | Sorties caisse |
| sales_count | integer | default 0 | Nb ventes |
| notes | text | nullable | Notes fermeture |
| opened_at | datetime | required | Heure ouverture |
| closed_at | datetime | nullable | Heure fermeture |

---

## 22. CashMovement

Entrées/sorties de caisse hors vente.

```
Table: pos_cash_movement
Index: (register_session_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| register_session_id | integer | FK RegisterSession | Session caisse |
| company_id | integer | FK Company | Entreprise |
| type | string(10) | required | in, out |
| amount | decimal(12,2) | required | Montant |
| reason | string(500) | required | Motif |
| performed_by_id | integer | FK User | Effectué par |
| approved_by_id | integer | FK User, nullable | Approuvé par |
| created_at | datetime | | |

---

## 23. PosEmployeeRole

Rôle POS granulaire par employé par Company.

```
Table: pos_employee_role
Unique: (user_id, company_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| user_id | integer | FK User | Employé |
| company_id | integer | FK Company | Entreprise |
| pos_role | string(30) | required | cashier, stock_clerk, seller, supervisor |
| pin_code | string(255) | nullable | PIN hashé (pour caisse) |
| can_apply_discount | boolean | default false | Appliquer remises |
| can_void_sale | boolean | default false | Annuler ventes |
| can_process_refund | boolean | default false | Traiter remboursements |
| can_manage_stock | boolean | default false | Gérer stock |
| can_view_reports | boolean | default false | Voir rapports |
| can_manage_customers | boolean | default false | Gérer clients |
| can_open_register | boolean | default true | Ouvrir caisse |
| max_discount_percent | decimal(5,2) | nullable | Remise max autorisée |
| is_active | boolean | default true | Actif |
| assigned_by_id | integer | FK User | Assigné par (manager) |
| created_at | datetime | | |
| updated_at | datetime | | |

---

## 24. PosAuditLog

Journal d'audit de toutes les opérations POS.

```
Table: pos_audit_log
Index: (company_id, created_at), (user_id, action), (entity_type, entity_id)
```

| Champ | Type | Contraintes | Description |
|-------|------|-------------|-------------|
| id | integer | PK | Identifiant |
| company_id | integer | FK Company | Entreprise |
| user_id | integer | FK User | Utilisateur |
| action | string(50) | required | sale_created, stock_adjusted, price_changed, etc. |
| entity_type | string(50) | required | product, sale, register, inventory, etc. |
| entity_id | integer | required | ID de l'entité |
| description | string(500) | required | Description lisible |
| old_values | json | nullable | Anciennes valeurs |
| new_values | json | nullable | Nouvelles valeurs |
| ip_address | string(45) | nullable | IP |
| user_agent | string(500) | nullable | Navigateur |
| metadata | json | default {} | Données supplémentaires |
| created_at | datetime | | |

**Append-only** : jamais modifié ni supprimé.
