# Database Schema Documentation

## Overview

Schema of the MySQL 8.4 database behind the Happy Food delivery platform: **72 migrations**
producing **~45 tables**, supporting four roles (Admin, Client, Driver, Restaurant — plus a
restaurant-scoped Staff sub-role) across a **multi-tenant** design where almost every table is
scoped to one restaurant.

- **Database:** MySQL 8.4, `utf8mb4_unicode_ci`, spatial support enabled
- **Testing:** SQLite (`database/database.sqlite`) — note MySQL-only raw `ALTER ... MODIFY` enum
  migrations don't replay on SQLite; that's expected, not a bug

This file was last regenerated from the actual migrations in `database/migrations/` (through
`2026_09_12_130000_drop_font_family_from_restaurant_themes_table.php`), not from an older snapshot
— treat it as ground truth over any other schema description in this repo.

---

## Tenancy Model

Every table below belongs to one of two groups:

- **Tenant-scoped** — has a `restaurant_id` (directly, or via a chain: `products` → `restaurant_id`,
  `orders` → `restaurant_id`, `order_product` → `orders.restaurant_id`, etc.). This is nearly
  everything: catalog, orders, drivers, settings, pricing, chat, wallets, staff.
- **Global** — `countries`/`states`/`municipalities` (location reference data), the permission
  tables, and Laravel's own framework tables (`sessions`, `cache`, `jobs`, ...). `settings` has
  exactly one legacy global row (`restaurant_id IS NULL`) left over from before the platform went
  multi-tenant; every restaurant since has its own row.

A **restaurant owner** has their own `restaurants` row (`restaurants.user_id`). A **staff member**
has no `restaurants` row of their own — they're attached to one (or more) restaurants through the
`restaurant_users` pivot instead. Drivers `belongsTo` exactly one restaurant (`drivers.restaurant_id`)
— there is no shared/global driver pool.

---

## Entity Relationship Diagram (grouped by domain)

```
┌──────────────────────────────── IDENTITY ────────────────────────────────┐
│  users ──┬── clients (1:1, both scoped to a restaurant)                  │
│          ├── drivers (1:1, belongs to one restaurant)                    │
│          ├── restaurants (1:1, owner)                                    │
│          ├── staffs (1:1, thin profile)                                  │
│          └── restaurant_users (pivot: staff ↔ restaurant, + owner too)   │
└────────────────────────────────────────────────────────────────────────┘
                                    │
┌──────────────────────────────── TENANT ───────────────────────────────────┐
│  restaurants ──┬── settings (1:1, per-tenant pricing/dispatch/timeouts)   │
│                ├── categories ── products ── media                       │
│                ├── opening_hours, peak_hours, demand_fees                │
│                ├── restaurant_themes, restaurant_onboardings,            │
│                │   restaurant_gallery_images                             │
│                ├── sliders, subscriptions                                │
│                └── wallet (morph)                                        │
└────────────────────────────────────────────────────────────────────────┘
                                    │
┌──────────────────────────────── ORDERS ───────────────────────────────────┐
│  orders ──┬── client (belongsTo)                                         │
│           ├── restaurant (belongsTo)                                     │
│           ├── driver (belongsTo, nullable — assigned)                    │
│           ├── offeredDriver (belongsTo, nullable — pending manual offer) │
│           ├── order_product (pivot) ── products                          │
│           ├── wallet_transactions                                        │
│           └── conversations (lazily created per order state)             │
└────────────────────────────────────────────────────────────────────────┘
                                    │
┌───────────────────────────────── CHAT ─────────────────────────────────────┐
│  conversations ── conversation_user (pivot, per-participant read state)  │
│                └── messages                                              │
└────────────────────────────────────────────────────────────────────────┘
                                    │
┌──────────────────────────────── MONEY ─────────────────────────────────────┐
│  wallets (morph: Restaurant | admin User) ── wallet_transactions          │
│  (drivers have NO wallet — see Financial Management below)               │
└────────────────────────────────────────────────────────────────────────┘
                                    │
┌────────────────────────────── LOCATION ────────────────────────────────────┐
│  countries ── states ── municipalities                                   │
└────────────────────────────────────────────────────────────────────────┘
```

---

## Table Details

### 1. Identity & Tenancy

#### `users`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| name | VARCHAR(255) | |
| email | VARCHAR(255) | UNIQUE |
| email_verified_at | TIMESTAMP | nullable |
| password | VARCHAR(255) | hashed |
| type | ENUM | `user`, `driver`, `restaurant`, `client`, `slider`, `staff` — default `user` |
| phone | VARCHAR(255) | UNIQUE, nullable |
| fcm_token | TEXT | nullable — push token (FCM or Expo) |
| agreed_to_terms_and_conditions | BOOLEAN | default false |
| agreed_to_terms_and_conditions_at | TIMESTAMP | nullable |
| remember_token | VARCHAR(100) | nullable |

`users` also carries `restaurant_id` (nullable FK — the tenant an account belongs to; set for
clients, drivers and staff, null for admins and restaurant owners) and `tenant_key`, a VIRTUAL
generated column (`COALESCE(restaurant_id, 0)`) that exists purely to carry the per-tenant unique
indexes on `email` and `phone`. It is virtual rather than stored because MySQL forbids CASCADE and
SET NULL referential actions on a column a stored generated column reads. `email` is required for
every account type - client, driver and staff sign-up all collect one, because the password-reset
flow is email-based.

Relations: `client`/`driver`/`restaurant`/`staff` (each `hasOne`), `tenant()` (`belongsTo`, the
restaurant the account belongs to — distinct from `restaurant()`, which is the one it owns),
`restaurants()` (`belongsToMany`
via `restaurant_users`), `notifications`, `reviews`, `sliders`, `subscriptions`, `wallet` (morph —
only meaningfully used by the admin role), `declinedOrders`, `createdRestaurants`/`ccreatedDrivers`.

#### `clients`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| restaurant_id | BIGINT | FK → restaurants, cascade delete — the app instance this customer signed up in |
| username | VARCHAR(255) | UNIQUE **per restaurant**, not globally |
| is_active | BOOLEAN | default true |
| user_id | BIGINT | FK → users, UNIQUE, cascade delete |

Each white-label app has its own customer base; clients are not a shared pool.

#### `drivers`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| name | VARCHAR(255) | |
| license_number | VARCHAR(255) | UNIQUE |
| image | VARCHAR(255) | nullable |
| vehicle_id | VARCHAR(255) | nullable |
| is_active | BOOLEAN | default false |
| location | GEOGRAPHY(POINT) | nullable — **not** the geometry type used pre-migration |
| is_online | BOOLEAN | default false |
| is_available | BOOLEAN | default true |
| last_seen_at | DATETIME | nullable |
| restaurant_id | BIGINT | FK → restaurants, **NOT NULL**, cascade — a driver belongs to one restaurant |
| user_id | BIGINT | FK → users, UNIQUE, cascade |
| creator_id | BIGINT | FK → users, nullable |
| deleted_at | TIMESTAMP | soft delete |

#### `staffs`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| user_id | BIGINT | FK → users, cascade |
| name | VARCHAR(255) | |
| is_active | BOOLEAN | default true |

Thin — a staff member's actual restaurant access comes from `restaurant_users`, not this table.

#### `restaurant_users` (pivot)

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| restaurant_id | BIGINT | FK → restaurants, cascade |
| user_id | BIGINT | FK → users, cascade |
| role | VARCHAR(255) | default `client` (effectively "unset") |

UNIQUE `(restaurant_id, user_id, role)`. The owner is typically attached here too, alongside every
staff member — code that combines "everyone on the restaurant side" dedupes by user id.

#### `restaurants`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| bundle_id | VARCHAR(255) | nullable, UNIQUE — the white-labeled app bundle it belongs to |
| name | VARCHAR(255) | |
| logo, cover_image | VARCHAR(255) | nullable |
| tiktok_url, instagram_url, facebook_url, linkedin_url | VARCHAR(255) | nullable |
| address | VARCHAR(255) | |
| location | GEOGRAPHY(POINT) | nullable |
| willaya, commune | VARCHAR(255) | |
| licence | VARCHAR(255) | nullable |
| description | TEXT | nullable |
| is_open, is_active | BOOLEAN | default false |
| opening_hours | JSON | nullable — legacy; real hours are the `opening_hours` table |
| user_id | BIGINT | FK → users, UNIQUE, cascade (the owner) |
| creator_id | BIGINT | FK → users, nullable |

Creating a restaurant automatically creates its `wallet`, default `opening_hours` rows, and an
active `restaurant_themes` row (model event, not a migration default).

---

### 2. Per-Tenant Configuration

#### `settings`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| restaurant_id | BIGINT | FK, nullable, **UNIQUE** — one legacy global row has `NULL` |
| app_name, app_logo, app_favicon | VARCHAR(255) | nullable |
| currency_symbol, currency_code, timezone, email, phone | VARCHAR(255) | nullable |
| user_app_version, admin_panel_version, restaurant_app_version | VARCHAR(255) | nullable |
| base_fee | DECIMAL(10,2) | default 0 |
| price_per_km | DECIMAL(10,2) | default 0 |
| night_multiplier | DECIMAL(5,2) | default 1 |
| night_start_time, night_end_time | TIME | nullable |
| order_revenue, delivery_revenue | DECIMAL(10,2) | default 0.10 — **whole-number percentages** (10 = 10%), not fractions |
| restaurant_timeout, driver_timeout | INTEGER | default 300 (seconds) |
| driver_assignment_timeout | INTEGER | default 1800 (seconds) |
| dispatch_radius | INTEGER | default 5000 (meters) |
| pickup_prompt_radius, arrival_prompt_radius | INTEGER | default 200 (meters) |
| delete_chat_duration | INTEGER | default 86400 (seconds) — chat retention after an order ends |

Resolved via `Setting::forRestaurant($restaurantId)` (a `firstOrNew`, so an unconfigured tenant
still gets sane in-code defaults rather than `null`).

#### `peak_hours`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| start_time, end_time | TIME | |
| multiplier | DECIMAL(5,2) | default 1.00 |
| restaurant_id | BIGINT | FK, cascade |

#### `demand_fees`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| level | ENUM | `low`, `medium`, `high` |
| fee | DECIMAL(10,2) | |
| restaurant_id | BIGINT | FK, cascade |

#### `opening_hours`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| day_of_week | TINYINT | 0 = Sunday .. 6 = Saturday |
| opens_at, closes_at | TIME | nullable |
| is_closed | BOOLEAN | default false |
| restaurant_id | BIGINT | FK, cascade |

Indexed (not unique) on `(restaurant_id, day_of_week)` — a restaurant can have multiple opening
periods on the same day (e.g. split lunch/dinner hours).

#### `restaurant_themes`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| restaurant_id | BIGINT | FK, cascade |
| name | VARCHAR(255) | default "Default Theme" |
| version | VARCHAR(255) | default "1.0.0" |
| is_active | BOOLEAN | default false |
| logo | VARCHAR(255) | nullable |
| primary_color, secondary_color, button_primary_color, button_color, border_color | VARCHAR(255) | hex colors |
| border_radius | INTEGER | default 8 |

(`font_family` existed briefly and was dropped.)

#### `restaurant_onboardings`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| restaurant_id | BIGINT | FK, cascade |
| title | VARCHAR(255) | |
| description | TEXT | nullable |
| image | VARCHAR(255) | |
| order | INTEGER | default 0 |
| is_active | BOOLEAN | default true |

#### `restaurant_gallery_images`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| restaurant_id | BIGINT | FK, cascade |
| image | VARCHAR(255) | |
| order | INTEGER (unsigned) | default 0 |

---

### 3. Catalog

#### `categories`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| name | VARCHAR(255) | |
| icon | VARCHAR(255) | |
| is_active | BOOLEAN | default true |
| restaurant_id | BIGINT | FK, **NOT NULL**, cascade |

UNIQUE `(restaurant_id, name)`.

#### `products`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| name | VARCHAR(255) | |
| price | DECIMAL(10,2) | |
| image | VARCHAR(255) | |
| description | TEXT | nullable |
| addons | JSON | nullable — array of `{id, name, price, is_default}` |
| variants | JSON | nullable — array of `{id, name, price}` |
| rating | DECIMAL(2,1) | nullable |
| is_available | BOOLEAN | default true |
| restaurant_id | BIGINT | FK, nullable, cascade |
| category_id | BIGINT | FK, nullable, null-on-delete |

#### `media`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| path | VARCHAR(255) | |
| product_id | BIGINT | FK, cascade |

Extra gallery images for a product — the primary image lives on `products.image`.

---

### 4. Orders

#### `orders`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| sub_total, total, delivery_fee | DECIMAL(10,2) | |
| distance_km | DECIMAL(8,3) | nullable |
| pricing_breakdown | JSON | nullable — `{base_fee, price_per_km, multipliers}` audit snapshot |
| night_multiplier, demand_multiplier, peak_multiplier | DECIMAL(5,2) | default 1 — snapshotted at order time, never recalculated |
| address | VARCHAR(255) | |
| lng, lat | DECIMAL(10,7) | |
| note | TEXT | nullable |
| status | ENUM | `pending`, `preparing`, `on_the_way`, `delivered`, `cancelled` — default `pending` |
| client_id | BIGINT | FK, nullable, null-on-delete |
| restaurant_id | BIGINT | FK, nullable, null-on-delete |
| driver_id | BIGINT | FK, nullable, null-on-delete — the **assigned** driver |
| offered_driver_id | BIGINT | FK, nullable, null-on-delete — a **pending manual offer**, cleared on claim/reject |
| offered_at | TIMESTAMP | nullable |
| driver_assigned_at | TIMESTAMP | nullable |
| declined_by | BIGINT | FK → users, nullable |
| accepted_at, on_the_way_at, delivered_at, cancelled_at | TIMESTAMP | nullable |
| cancel_reason | VARCHAR(255) | nullable — `restaurant_declined`, `restaurant_timeout`, `no_driver` |
| pickup_prompt_at, arrival_prompt_at | TIMESTAMP | nullable — latches so a driver is only prompted once per leg |
| preparation_time, delivery_time | SMALLINT | nullable, minutes — computed on delivery |
| requires_manual_driver_assignment | BOOLEAN | default false |
| deleted_at | TIMESTAMP | soft delete |

Indexes: `(restaurant_id, status)`, `(status, accepted_at)`.

**No `accepted`/`declined`/`ready_for_pickup` status exists** — `ready_for_pickup` was collapsed
into `preparing`, and both a restaurant decline and a system cancellation land on `cancelled`,
distinguished by `cancel_reason`.

Status transition rules (enforced in code, not a DB constraint):

```
pending ──accept──> preparing ──driver picks up──> on_the_way ──driver delivers──> delivered
   │                    │
   └──decline/cancel──> cancelled <──cancel (no driver found, restaurant timeout)──┘
```

#### `order_product` (pivot)

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| order_id | BIGINT | FK, cascade |
| product_id | BIGINT | FK, cascade |
| quantity | INTEGER | |
| variants | VARCHAR(255) | nullable — display label of the chosen variant |
| addons | JSON | nullable — structured `{id, name, price, quantity}` snapshots (not a display string) |

---

### 5. Chat

#### `conversations`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| restaurant_id | BIGINT | FK, cascade — denormalized from the order |
| order_id | BIGINT | FK, cascade |
| type | VARCHAR(255) | `client_restaurant`, `client_driver`, `restaurant_driver` |

UNIQUE `(order_id, type)`. Indexed `(restaurant_id, updated_at)`. Created **lazily**, based on the
order's current state — not pre-created when the order is placed.

#### `conversation_user` (pivot)

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| conversation_id | BIGINT | FK, cascade |
| user_id | BIGINT | FK, cascade |
| last_read_at | TIMESTAMP | nullable — per-participant read state |

UNIQUE `(conversation_id, user_id)`. The restaurant side of a thread can have several rows: the
owner plus every staff member.

#### `messages`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| conversation_id | BIGINT | FK, cascade |
| sender_id | BIGINT | FK → users, cascade |
| body | TEXT | nullable — a voice note/photo carries no text |
| attachment_path | VARCHAR(255) | nullable — **private disk**, served via a signed URL |
| attachment_type | VARCHAR(16) | nullable — `image` or `audio` |
| attachment_mime | VARCHAR(255) | nullable |
| attachment_size | INTEGER (unsigned) | nullable |
| attachment_duration | SMALLINT (unsigned) | nullable — seconds, audio only |

Indexed `(conversation_id, created_at)`. One attachment per message.

---

### 6. Financial

#### `wallets`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| owner_type, owner_id | morph | `Restaurant` or the platform admin `User` |
| balance | DECIMAL(12,2) | default 0 |

**Drivers do not have a wallet.**

#### `wallet_transactions`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| wallet_id | BIGINT | FK, nullable, null-on-delete |
| order_id | BIGINT | FK, nullable, null-on-delete |
| driver_id | BIGINT | FK → drivers, nullable, null-on-delete |
| restaurant_id | BIGINT | FK, cascade — denormalized for tenant-scoped queries |
| type | ENUM | `credit`, `debit` |
| reason | ENUM | `driver_commission` (historical only — drivers no longer pay commission), `restaurant_commission`, `transfer_in`, `transfer_out`, `driver_earning` |
| amount | DECIMAL(12,2) | |

A `driver_earning` row is written directly against `driver_id` on delivery — a record of what the
driver earned (the full delivery fee), **not** a balance movement (they have no wallet to move it
into/out of). The only real balance movement left is the restaurant → admin `restaurant_commission`
transfer.

---

### 7. Reviews, Subscriptions, Sliders, Notifications

#### `reviews`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| rating | TINYINT | 1–5 |
| comment | TEXT | nullable |
| user_id | BIGINT | FK → users, author |
| reviewable_id, reviewable_type | morph | `Product`, `Restaurant`, or `Driver` |
| restaurant_id | BIGINT | FK, cascade — denormalized |

Indexed `(reviewable_id, reviewable_type)`.

#### `subscriptions`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| amount | DECIMAL(8,2) | |
| from_date, to_date | DATE | |
| type | ENUM | `driver`, `restaurant` |
| restaurant_id | BIGINT | FK, **NOT NULL**, cascade |
| user_id | BIGINT | FK, nullable |
| slider_id | BIGINT | FK, nullable |
| deleted_at | TIMESTAMP | soft delete |

#### `sliders`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| name | VARCHAR(255) | |
| image | VARCHAR(255) | |
| status | BOOLEAN | default true |
| restaurant_id | BIGINT | FK, **NOT NULL**, cascade |
| user_id | BIGINT | FK → users, creator |

#### `notifications`

| Column | Type | Notes |
|---|---|---|
| id | UUID PK | |
| type | VARCHAR(255) | notification class |
| notifiable_type, notifiable_id | morph | |
| data | TEXT | JSON payload |
| read_at | TIMESTAMP | nullable |
| title | VARCHAR(255) | |
| message | TEXT | nullable |
| is_read | BOOLEAN | default false |
| user_id | BIGINT | FK, nullable, null-on-delete |
| restaurant_id | BIGINT | FK, cascade |

Indexed `(notifiable_type, notifiable_id)`.

---

### 8. Location

#### `countries` / `states` / `municipalities`

Standard 3-level hierarchy, unchanged: `countries.name` → `states.{name, country_id}` →
`municipalities.{name, state_id}`. Global reference data, not tenant-scoped.

---

### 9. Auth & Permissions

#### `password_reset_codes`

| Column | Type | Notes |
|---|---|---|
| id | BIGINT PK | |
| email | VARCHAR(255) | indexed |
| code | VARCHAR(6) | |
| expires_at | TIMESTAMP | |
| verified_at | TIMESTAMP | nullable |

#### `personal_access_tokens`

Sanctum's standard table — mobile app tokens.

#### Spatie permission tables

`permissions` (`name`, `guard_name`), `roles` (`name`, `guard_name`), `model_has_permissions`,
`model_has_roles`, `role_has_permissions` — standard Spatie structure, **teams disabled**
(`config/permission.php` `'teams' => false`), so no `team_id` column on any of them.

Roles in use: `admin`, `restaurant` (owner), `staff`.

---

### 10. Framework Tables (Laravel defaults, unmodified)

`sessions`, `password_reset_tokens`, `cache`, `cache_locks`, `jobs`, `job_batches`, `failed_jobs` —
standard Laravel scaffolding, not application-specific.

---

## Enums Reference

| Enum (`app/Enums/`) | Values |
|---|---|
| `UserTypesEnum` | `user`, `driver`, `restaurant`, `client`, `slider`, `staff` |
| `OrderStatusEnum` | `pending`, `preparing`, `on_the_way`, `delivered`, `cancelled` |
| `OrderCancelReasonEnum` | `restaurant_declined`, `restaurant_timeout`, `no_driver` |
| `TransactionEnum` | `credit`, `debit` |
| `TransactionReasonEnum` | `driver_commission` (historical), `restaurant_commission`, `transfer_in`, `transfer_out`, `driver_earning` |
| `DemandFeeEnum` | `low`, `medium`, `high` |
| `DayOfWeekEnum` | `0`..`6`, Sunday..Saturday |
| `ConversationTypeEnum` | `client_restaurant`, `client_driver`, `restaurant_driver` |
| `MessageAttachmentTypeEnum` | `image`, `audio` |

---

## Key Relationships Summary

- **Identity:** `users` → `clients` / `drivers` / `restaurants` / `staffs` (1:1 each); `users` ↔
  `restaurants` (many-to-many via `restaurant_users`, covering both staff and the owner)
- **Catalog:** `restaurants` → `categories` → `products` → `media` (1:N each)
- **Orders:** `clients`/`restaurants`/`drivers` → `orders` (1:N); `orders` ↔ `products`
  (many-to-many via `order_product`); `orders` → `conversations` (1:N, lazy)
- **Chat:** `conversations` ↔ `users` (many-to-many via `conversation_user`); `conversations` →
  `messages` (1:N)
- **Financial:** `restaurants`/admin `users` → `wallets` (1:1, morph) → `wallet_transactions`
  (1:N); `orders` → `wallet_transactions` (1:N); `drivers` → `wallet_transactions` directly (1:N,
  no wallet in between)
- **Reviews:** `products` / `restaurants` / `drivers` → `reviews` (1:N, morph)
- **Location:** `countries` → `states` → `municipalities`

---

## Spatial Data

- `restaurants.location` — `GEOGRAPHY(POINT)`
- `drivers.location` — `GEOGRAPHY(POINT)` (migrated from `GEOMETRY(POINT)`)

Distance between a restaurant and a driver/delivery point is computed in application code
(`App\Support\Geo::distanceMeters()`, haversine) rather than via a spatial SQL query — dispatch
fleets are scoped per restaurant and small enough that this stays simple and avoids SRID mismatches
the codebase hit with spatial queries previously.

---

## Soft Deletes

Tables with a `deleted_at` column: `orders`, `subscriptions`, `drivers`.

---

## What's Gone

`favorites` and `favorite_restaurants` were **dropped** entirely (migration
`2026_08_07_234149_add_tenant_columns_to_existing_tables.php`) when the schema went multi-tenant.
Any reference to a `Favorite` model or a favorites table elsewhere (docs, old code comments) is
stale.

---

## Indexes Summary

- **Primary keys:** `BIGINT` auto-increment on every table except `notifications` (UUID),
  `password_reset_tokens` (email), `sessions` (session id), `cache`/`cache_locks` (cache key)
- **Notable unique constraints:** `users(email, tenant_key)`, `users(phone, tenant_key)`,
  `clients(username, restaurant_id)`, `drivers(license_number, restaurant_id)` — all four are
  per-tenant, so the same person can hold an account in two white-label apps;
  `restaurants.user_id`, `restaurants.bundle_id`, `settings.restaurant_id`,
  `categories(restaurant_id, name)`, `opening_hours` composite index (not unique — multiple periods
  per day allowed), `conversations(order_id, type)`, `conversation_user(conversation_id, user_id)`,
  `restaurant_users(restaurant_id, user_id, role)`
- **Query-shaped indexes:** `orders(restaurant_id, status)`, `orders(status, accepted_at)`,
  `messages(conversation_id, created_at)`, `conversations(restaurant_id, updated_at)`,
  `reviews(reviewable_id, reviewable_type)`, `notifications(notifiable_type, notifiable_id)`
- **Foreign keys:** cascade-on-delete for direct tenant ownership (e.g. `restaurant_id` on
  catalog/settings/pricing tables), null-on-delete for optional/cross-tenant references (e.g.
  `orders.driver_id`, `orders.client_id`)
