# RULE FILE 00 — Master Conventions

These are absolute. Every migration, model, service, controller, and view MUST comply. No exceptions.

---

## Naming Conventions

### Tables
- Plural snake_case: `training_programs`, `invoice_items`, `pos_transactions`
- Pivot tables: alphabetical order of the two tables, singular: `guardian_participant`, `permission_role`
- NEVER: camelCase, PascalCase, singular for non-pivot

### Columns
- snake_case always: `first_name`, `academy_id`, `created_by`
- Foreign keys: `{singular_related_table}_id` → `academy_id`, `branch_id`, `participant_id`
- Self-referencing FK: `parent_id` (for trees)
- Polymorphic: `{name}_type` + `{name}_id` → `billable_type`, `billable_id`
- Boolean: `is_` or `has_` prefix → `is_active`, `has_lighting`, `is_recurring`
- Amounts: descriptive name, ALWAYS bigint (piasters) → `total_amount`, `unit_cost`, `wallet_balance`
- Status/type: plain name → `status`, `type`, `payment_method`, `gender`
- Timestamps: `_at` suffix → `approved_at`, `closed_at`, `last_login_at`
- Dates: `_date` suffix OR `_at` for datetime → `start_date`, `birth_date`, `expected_delivery_date`
- Counts: `_count` suffix → `transactions_count`, `total_sold`
- Paths: `_path` suffix → `avatar_path`, `photo_path`, `document_path`

### Models
- Singular PascalCase: `TrainingProgram`, `InvoiceItem`, `PosTransaction`
- Located: `app/Domain/{Module}/Models/`

### Services
- `{Entity}Service`: `InvoiceService`, `PaymentService`, `AttendanceService`
- Located: `app/Domain/{Module}/Services/`

### Enums
- PascalCase, singular: `InvoiceStatus`, `PaymentMethod`, `ParticipantStatus`
- Located: `app/Domain/{Module}/Enums/`
- MUST be string-backed (`enum X: string`)
- Values: snake_case strings → `'partially_paid'`, `'in_progress'`

### Events
- Past tense verb + noun: `PaymentReceived`, `InvoicePaid`, `ParticipantSuspended`
- Located: `app/Domain/{Module}/Events/`

### Livewire Components
- kebab-case in views: `participant-list`, `invoice-create`
- PascalCase classes: `ParticipantList`, `InvoiceCreate`
- Located: `app/Livewire/{Module}/`

---

## Column Types — The Exact Rules

| What | PostgreSQL Type | Laravel Migration | Notes |
|------|----------------|-------------------|-------|
| Primary key | bigint | `$table->id()` | Auto-increment |
| UUID | uuid | `$table->uuid('uuid')->unique()` | Public-facing identifier |
| Foreign key | bigint unsigned | `$table->foreignId('x_id')->constrained()` | Always constrained |
| Money/amounts | bigint | `$table->bigInteger('amount')` | Piasters (÷100 for display) |
| Short string | varchar(255) | `$table->string('name')` | Default |
| Limited string | varchar(N) | `$table->string('code', 10)` | Specify length |
| Long text | text | `$table->text('notes')` | Nullable always |
| Boolean | boolean | `$table->boolean('is_active')->default(true)` | Always has default |
| Date | date | `$table->date('start_date')` | No time component |
| Time | time | `$table->time('opens_at')` | No date component |
| Datetime | timestamp | `$table->timestamp('approved_at')` | Nullable unless required |
| Day of week | smallint | `$table->smallInteger('day_of_week')` | 0=Sunday, 6=Saturday |
| Percentage | decimal(5,2) | `$table->decimal('rate', 5, 2)` | 0.00 to 100.00 |
| Coordinates | decimal(10,7) | `$table->decimal('latitude', 10, 7)` | GPS precision |
| JSONB | jsonb | `$table->jsonb('metadata')->default('{}')` | Always default '{}' or '[]' |
| Status/type | varchar(30) | `$table->string('status', 30)` | With CHECK constraint |
| Sort order | integer | `$table->integer('sort_order')->default(0)` | Always default 0 |

---

## Universal Columns (on EVERY table unless stated)

### Required on ALL tenant-scoped tables:
```php
$table->id();
$table->foreignId('academy_id')->constrained('organizations');
$table->timestamps(); // created_at, updated_at
```

### Required on ALL public-facing entities:
```php
$table->uuid('uuid')->unique(); // route key
```

### Required on ALL soft-deletable entities:
```php
$table->softDeletes(); // deleted_at
```

### Required on ALL user-created entities:
```php
$table->foreignId('created_by')->constrained('users');
```

---

## CHECK Constraints — Exact Pattern

```php
// In migration — ALWAYS use DB::statement for CHECK constraints
DB::statement("ALTER TABLE {table} ADD CONSTRAINT {table}_{column}_check CHECK ({column} IN ('value1', 'value2', 'value3'))");
```

Naming: `{table}_{column}_check`

The PHP enum values MUST be identical to the CHECK values:
```php
// Enum
enum InvoiceStatus: string {
    case Draft = 'draft';           // ← exact match
    case Sent = 'sent';             // ← exact match
    case PartiallyPaid = 'partially_paid'; // ← exact match
}

// CHECK constraint in migration
// CHECK (status IN ('draft', 'sent', 'partially_paid', ...))
```

**ENFORCEMENT**: Before writing any enum, grep the migration for the CHECK constraint values. Copy them character-for-character.

---

## Index Rules

### Always index:
- `academy_id` (every tenant table)
- `status` (every table with status)
- `created_at` (for sorting/filtering)
- Foreign keys (automatic with `constrained()`)
- Columns used in WHERE clauses of list queries

### Composite indexes (order matters — leftmost first):
```php
$table->index(['academy_id', 'status']);           // List filtered by status
$table->index(['academy_id', 'created_at']);       // List sorted by date
$table->index(['person_id', 'date']);              // Attendance lookup
$table->index(['facility_id', 'date', 'start_time']); // Space collision check
```

### Unique constraints:
```php
$table->unique(['academy_id', 'slug']);            // Slug unique per tenant
$table->unique(['product_id', 'warehouse_id']);    // One level per product per warehouse
$table->unique(['academy_id', 'order_number']);    // Order number unique per tenant
```

### NEVER create duplicate indexes:
- `nullableMorphs()` already creates a composite index — don't add another
- `foreignId()->constrained()` already creates a single-column index

---

## Default Values — Complete List

| Column Pattern | Default | Reason |
|---------------|---------|--------|
| `is_active` | `true` | New records are active |
| `is_recurring` | `false` | Most things are one-time |
| `sort_order` | `0` | No ordering by default |
| `quantity` | `1` | Minimum meaningful quantity |
| `total_amount` | none | Must be calculated, never assumed |
| `status` (general) | `'draft'` or `'active'` | Depends on entity lifecycle |
| `metadata` / `jsonb` | `'{}'` or `'[]'` | Never null for JSONB |
| `count` columns | `0` | Start at zero |
| `priority` | `0` | Default priority |
| `level` (permission) | none | Must be explicitly set |

---

## Nullable Rules

### ALWAYS nullable:
- `notes`, `description` (text fields)
- `*_path` (file paths — not always uploaded)
- `approved_at`, `completed_at`, `closed_at` (timestamps that haven't happened yet)
- `approved_by`, `cancelled_by` (user refs for actions not yet taken)
- `end_date` (null = indefinite)
- `parent_id` (root nodes have no parent)
- Polymorphic `reference_type` + `reference_id` (not all records have references)
- `phone`, `email` on non-user tables (optional contact info)
- `latitude`, `longitude` (not always provided)
- `metadata` is NOT nullable — always default `'{}'`

### NEVER nullable:
- `academy_id` (every record belongs to a tenant)
- `status` (every stateful entity has a status)
- `name` / `name_ar` (entities must be named)
- `created_by` (we always know who did it)
- `amount` fields on financial records (must be explicit, even if 0)
- `uuid` (auto-generated)

---

## Cascade & Delete Rules

| Relationship | On Delete | Reason |
|-------------|-----------|--------|
| `academy_id` → organizations | CASCADE | Tenant deletion removes all data |
| `branch_id` → branches | RESTRICT | Can't delete branch with active data |
| `created_by` → users | RESTRICT | Can't delete user who created records |
| `participant_id` → participants | RESTRICT | Use soft-delete instead |
| `invoice_id` → invoices | RESTRICT | Financial records are immutable |
| `parent_id` (self-ref) | SET NULL | Orphan children become root nodes |
| Pivot tables | CASCADE on both FKs | Clean up relationships |

**RULE**: Financial records (transactions, payments, invoices) are NEVER deleted. Use status changes and soft-deletes.

---

## Money Rules — ZERO TOLERANCE

1. **ALL amounts stored as bigint (piasters)**: 150.00 EGP = `15000` in DB
2. **Display conversion happens ONLY in Blade**: `number_format($amount / 100, 2)`
3. **NEVER use float/double/decimal for money**
4. **NEVER do arithmetic in PHP on formatted amounts** — always work with integers
5. **Rounding**: when dividing (e.g., splitting), round DOWN and assign remainder to last item
6. **Negative amounts**: allowed ONLY for credits, refunds, and adjustments
7. **Zero amounts**: valid (e.g., free trial invoice, 100% discount)
8. **Currency**: implied EGP everywhere — no multi-currency support needed
9. **Display helper**:
```php
// app/Helpers/money.php
function format_money(int $piasters): string {
    return number_format($piasters / 100, 2) . ' ج.م';
}
function piasters_to_pounds(int $piasters): float {
    return $piasters / 100;
}
function pounds_to_piasters(float $pounds): int {
    return (int) round($pounds * 100);
}
```
