---
description: Enum values MUST match CHECK constraints exactly. Single source of truth.
globs: ["app/Domain/**/Enums/**", "database/migrations/**"]
---

# Enums & CHECK Constraints — Character-for-Character Match

## The Rule

The PHP enum case values and the PostgreSQL CHECK constraint values must be IDENTICAL.
When writing an enum, COPY from the migration. When writing a migration, DEFINE the values first.

## Enum Pattern

```php
namespace App\Domain\{Module}\Enums;

enum StatusName: string
{
    case Draft = 'draft';
    case Active = 'active';
    // Values are ALWAYS snake_case strings
}
```

## CHECK Pattern in Migration

```php
DB::statement("ALTER TABLE table_name ADD CONSTRAINT table_name_column_check 
    CHECK (column IN ('draft', 'active'))");
```

## Naming Convention

- Constraint: `{table}_{column}_check`
- Enum class: PascalCase singular noun (`InvoiceStatus`, `PaymentMethod`)
- Enum values: snake_case (`'partially_paid'`, `'in_progress'`)
- Enum cases: PascalCase (`PartiallyPaid`, `InProgress`)

## Complete Enum Registry

### InvoiceStatus
`'draft', 'pending', 'awaiting_approval', 'sent', 'paid', 'partially_paid', 'overpaid', 'overdue', 'cancelled', 'refunded', 'partially_refunded', 'written_off', 'disputed'`

### PaymentStatus
`'pending', 'confirmed', 'failed', 'cancelled', 'refunded'`

### PaymentMethod
`'cash', 'card', 'bank_transfer', 'wallet', 'online', 'cheque', 'other'`

### TransactionType
`'payment_received', 'payment_made', 'refund', 'transfer', 'adjustment', 'fee', 'discount', 'write_off', 'opening_balance'`

(This was registered as `'debit', 'credit'` — a vocabulary the database has
never accepted. `type` describes the movement; the two account columns carry
the direction.)

### AccountType
`'asset', 'liability', 'equity', 'revenue', 'expense'`

### TaxMode — `revenue_mappings.tax_mode`
`'none', 'inclusive', 'exclusive'`

### AllocationType — `revenue_mapping_lines.allocation_type`
`'percentage', 'fixed', 'remainder'`

### RevenueSource — `revenue_mappings.source_key`
`'subscription', 'training_program', 'product', 'kit', 'event', 'space_reservation', 'service_fee', 'external', 'other'`

Deliberately **not** CHECK-constrained: a source key is added by shipping code
that can produce that kind of money, and a constraint would make every such
addition a schema migration on every tenant. `RevenueSource::forItemable()` is
total — an unrecognised itemable falls to `other` rather than to nothing — so an
unknown key can never leave money unrouted. The key is permanent once shipped:
renaming a case orphans every rule a client wrote against it.

### ParticipantStatus
`'registered', 'active', 'frozen', 'suspended', 'inactive', 'graduated', 'transferred', 'withdrawn', 'blacklisted'`

### EnrollmentStatus
`'pending', 'active', 'completed', 'cancelled', 'expired', 'waitlisted'`

### GroupStatus
`'forming', 'active', 'full', 'on_hold', 'completed', 'cancelled'`

### SessionStatus
`'scheduled', 'in_progress', 'completed', 'cancelled', 'rescheduled'`

### AttendanceStatus
`'expected', 'present', 'late', 'excused', 'absent', 'no_show', 'left_early', 'partial', 'cancelled', 'exempt'`

### Gender
`'male', 'female'`

### Classification
`'regular', 'vip', 'scholarship', 'staff_child', 'trial'`

### FacilityStatus
`'active', 'maintenance', 'closed', 'reserved', 'unavailable'`

### LayoutType
`'grid', 'lanes', 'zones', 'custom'`

### ReservationStatus
`'confirmed', 'tentative', 'cancelled'`

### AssignmentScope
`'full', 'partial', 'supervising', 'assisting', 'observing'`

### AssignmentStatus
`'active', 'suspended', 'completed', 'cancelled'`

### MovementDirection
`'in', 'out'`

### PurchaseOrderStatus
`'draft', 'submitted', 'confirmed', 'partially_received', 'received', 'cancelled'`

### WalletStatus
`'active', 'frozen', 'closed'`

### UserStatus
`'active', 'inactive', 'suspended', 'pending'`

### PricingAdjustmentType
`'percentage_discount', 'fixed_discount', 'fixed_price', 'percentage_increase'`

### PricingRuleType
`'age', 'membership_duration', 'family_size', 'sibling_order', 'classification', 'enrollment_timing', 'enrollment_volume', 'seasonal', 'gender', 'branch', 'day_time', 'loyalty', 'custom'`

## Validation Rules (in FormRequests)

```php
'status' => 'required|in:' . implode(',', array_column(StatusEnum::cases(), 'value'))
```

This ensures form validation accepts EXACTLY what the DB accepts.
