- Replace dated migrations with ordered 2026_04_07_* chain; fold users update into base migration - Update OrganisationScope, AppServiceProvider, seeders, api routes, and .env.example - Refresh Cursor rules, CLAUDE.md, Makefile, README, and docs (API, SCHEMA, SETUP) - Adjust admin/app/portal HTML, packages, api-client, events types, and theme config - Update docker-compose and VS Code settings; remove stray Office lock files from resources Made-with: Cursor
284 lines
7.3 KiB
Plaintext
284 lines
7.3 KiB
Plaintext
---
|
|
description: Database schema conventions, ULID primary keys, JSON column rules, soft delete strategy, and index requirements for Crewli
|
|
globs: ["api/database/**/*.php", "api/app/Models/**/*.php"]
|
|
alwaysApply: true
|
|
---
|
|
|
|
# Database & Schema Rules
|
|
|
|
## Primary Keys: ULID (NOT UUID v4)
|
|
|
|
All business tables use ULID (Universally Unique Lexicographically Sortable Identifier). UUID v4 is forbidden because it is random, causing B-tree index fragmentation in InnoDB. ULID is monotonically increasing (time-ordered).
|
|
|
|
### Migration Pattern
|
|
```php
|
|
$table->ulid('id')->primary();
|
|
```
|
|
|
|
### Model Pattern
|
|
```php
|
|
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
|
|
|
class Event extends Model
|
|
{
|
|
use HasUlids;
|
|
}
|
|
```
|
|
|
|
### Foreign Keys
|
|
```php
|
|
$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete();
|
|
$table->foreignUlid('event_id')->constrained()->cascadeOnDelete();
|
|
$table->foreignUlid('location_id')->nullable()->constrained()->nullOnDelete();
|
|
```
|
|
|
|
### Exception: Pure Pivot Tables
|
|
Pure pivot tables (no business logic, only joins) use auto-increment integer PK for join performance:
|
|
```php
|
|
// organisation_user, event_user_roles, crowd_list_persons, access_zone_days, etc.
|
|
$table->id(); // auto-increment integer
|
|
```
|
|
|
|
## JSON Column Rules
|
|
|
|
### ALLOWED for JSON
|
|
- Opaque configuration: `settings`, `blocks`, `fields`
|
|
- Toggle-sets and item lists: `items`, `milestone_flags`
|
|
- Free-text arrays: `top_feedback`
|
|
- Conditional logic definitions: `conditional_logic`
|
|
|
|
### NEVER use JSON for
|
|
- Dates or periods (use separate columns or pivot tables)
|
|
- Status values (use enum columns)
|
|
- Foreign keys or references (use FK columns)
|
|
- Boolean flags (use bool columns)
|
|
- Anything that needs to be filtered, sorted, or aggregated in queries
|
|
|
|
### Example: Access Zone Days
|
|
```php
|
|
// ❌ WRONG: JSON array of dates
|
|
$table->json('active_days'); // Can't index or query efficiently
|
|
|
|
// ✅ CORRECT: Separate pivot table
|
|
Schema::create('access_zone_days', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->foreignUlid('access_zone_id')->constrained()->cascadeOnDelete();
|
|
$table->date('day_date');
|
|
$table->unique(['access_zone_id', 'day_date']);
|
|
$table->index('day_date');
|
|
});
|
|
```
|
|
|
|
## Soft Delete Strategy
|
|
|
|
### Soft Delete ON (recoverable business records)
|
|
- `organisations`
|
|
- `events`
|
|
- `festival_sections`
|
|
- `shifts`
|
|
- `shift_assignments`
|
|
- `persons`
|
|
- `artists`
|
|
- `companies`
|
|
- `production_requests`
|
|
|
|
```php
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
class Event extends Model
|
|
{
|
|
use SoftDeletes;
|
|
}
|
|
|
|
// Migration:
|
|
$table->softDeletes();
|
|
```
|
|
|
|
### Soft Delete OFF (immutable audit records)
|
|
These are historical records that must never be modified or hidden:
|
|
- `check_ins` (who checked in, when, by whom)
|
|
- `briefing_sends` (delivery tracking)
|
|
- `message_replies` (communication audit)
|
|
- `shift_waitlist` (position tracking)
|
|
- `volunteer_festival_history` (coordinator assessments)
|
|
- `show_day_absence_alerts` (operational audit)
|
|
|
|
## Required Composite Indexes
|
|
|
|
All list queries must target < 50ms. These composite indexes are mandatory:
|
|
|
|
### persons
|
|
```php
|
|
$table->index(['event_id', 'crowd_type_id', 'status']);
|
|
$table->index(['email', 'event_id']);
|
|
$table->index(['user_id', 'event_id']);
|
|
```
|
|
|
|
### shift_assignments
|
|
```php
|
|
$table->index(['shift_id', 'status']);
|
|
$table->index(['person_id', 'status']);
|
|
$table->unique(['person_id', 'time_slot_id']); // DB-enforced conflict detection
|
|
```
|
|
|
|
### check_ins
|
|
```php
|
|
$table->index(['event_id', 'person_id', 'scanned_at']);
|
|
$table->index(['event_id', 'scanned_at']);
|
|
```
|
|
|
|
### briefing_sends
|
|
```php
|
|
$table->index(['status', 'briefing_id']);
|
|
$table->index(['person_id']);
|
|
```
|
|
|
|
### shift_waitlist
|
|
```php
|
|
$table->unique(['shift_id', 'person_id']);
|
|
$table->index(['shift_id', 'position']);
|
|
```
|
|
|
|
### performances (B2B detection via overlap query)
|
|
```php
|
|
$table->index(['stage_id', 'date', 'start_time', 'end_time']);
|
|
```
|
|
|
|
### events
|
|
```php
|
|
$table->index(['organisation_id', 'status']);
|
|
```
|
|
|
|
### festival_sections
|
|
```php
|
|
$table->index(['event_id', 'sort_order']);
|
|
```
|
|
|
|
### time_slots
|
|
```php
|
|
$table->index(['event_id', 'person_type', 'date']);
|
|
```
|
|
|
|
### shifts
|
|
```php
|
|
$table->index(['festival_section_id', 'time_slot_id']);
|
|
$table->index(['time_slot_id', 'status']);
|
|
```
|
|
|
|
## Migration Ordering
|
|
|
|
Migrations must be created in dependency order. Foundation tables first, then dependent tables:
|
|
|
|
1. `users` (update: add timezone, locale, deleted_at)
|
|
2. `organisations`
|
|
3. `organisation_user` (pivot)
|
|
4. `user_invitations`
|
|
5. `events`
|
|
6. `event_user_roles` (pivot)
|
|
7. `locations`
|
|
8. `crowd_types`
|
|
9. `companies`
|
|
10. `festival_sections`
|
|
11. `time_slots`
|
|
12. `persons`
|
|
13. `shifts`
|
|
14. `shift_assignments`
|
|
15. `volunteer_availabilities`
|
|
16. `shift_waitlist`
|
|
17. `accreditation_categories`
|
|
18. `accreditation_items`
|
|
19. `event_accreditation_items`
|
|
20. `accreditation_assignments`
|
|
21. `access_zones` + `access_zone_days` + `person_access_zones`
|
|
22. `stages` + `stage_days`
|
|
23. `artists`
|
|
24. `performances`
|
|
25. `advance_sections` + `advance_submissions`
|
|
26. `briefing_templates` + `briefings` + `briefing_sends`
|
|
27. `public_forms` + `form_submissions`
|
|
28. `check_ins`
|
|
29. `scanners`
|
|
|
|
## Migration Template
|
|
|
|
```php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Illuminate\Database\Migrations\Migration;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
return new class extends Migration
|
|
{
|
|
public function up(): void
|
|
{
|
|
Schema::create('shifts', function (Blueprint $table) {
|
|
// ULID primary key
|
|
$table->ulid('id')->primary();
|
|
|
|
// Foreign keys with constraints
|
|
$table->foreignUlid('festival_section_id')->constrained()->cascadeOnDelete();
|
|
$table->foreignUlid('time_slot_id')->constrained()->cascadeOnDelete();
|
|
$table->foreignUlid('location_id')->nullable()->constrained()->nullOnDelete();
|
|
|
|
// Business columns
|
|
$table->unsignedSmallInteger('slots_total');
|
|
$table->unsignedSmallInteger('slots_open_for_claiming')->default(0);
|
|
$table->string('status')->default('open');
|
|
$table->json('events_during_shift')->nullable(); // opaque config = OK for JSON
|
|
|
|
// Timestamps + soft delete
|
|
$table->timestamps();
|
|
$table->softDeletes();
|
|
|
|
// Composite indexes (from design document section 3.5)
|
|
$table->index(['festival_section_id', 'time_slot_id']);
|
|
$table->index(['time_slot_id', 'status']);
|
|
});
|
|
}
|
|
|
|
public function down(): void
|
|
{
|
|
Schema::dropIfExists('shifts');
|
|
}
|
|
};
|
|
```
|
|
|
|
## Audit Logging
|
|
|
|
Use Spatie laravel-activitylog on these models:
|
|
- `persons`
|
|
- `accreditation_assignments`
|
|
- `shift_assignments`
|
|
- `check_ins`
|
|
- `production_requests`
|
|
|
|
```php
|
|
use Spatie\Activitylog\Traits\LogsActivity;
|
|
use Spatie\Activitylog\LogOptions;
|
|
|
|
class Person extends Model
|
|
{
|
|
use LogsActivity;
|
|
|
|
public function getActivitylogOptions(): LogOptions
|
|
{
|
|
return LogOptions::defaults()
|
|
->logOnly(['status', 'is_blacklisted', 'crowd_type_id'])
|
|
->logOnlyDirty();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Out of Scope (Do NOT create tables for)
|
|
|
|
These features are explicitly excluded from v1.x. No placeholders, no empty JSON fields, no migrations:
|
|
- Travel management (flights, buses)
|
|
- Accommodation management (hotels)
|
|
- Travel Party management
|
|
- CO2/sustainability reporting
|
|
- Native mobile apps
|
|
- Ticket sales (own ticketing system)
|