# State of Play — ShowPrima DDD Decomposition

**Date**: 2026-04-14
**Branch**: `feature/decomp-sprint` (worktree: `showprima-user-account-decomp`)
**Status**: **DEV READY FOR CLIENT DEMO** — full admin flow working: event creation (auto-fork provisions seats), V2 canvas reservation with tier colors, comp bookings, video upload/management, venue editor V2. SES live (external email restored). Revenue/customer reporting accurate. Prod assets synced. Mo has credentials (`mo@globalgala.com`). Revolut sandbox configured but client ID empty (locked out — tomorrow). Comp bookings work without Revolut.

---

## What Is This Branch?

This is the **DDD domain decomposition** of the ShowPrima event ticketing platform. It's a near-total refactor of the Laravel monolith into bounded domains. This branch IS the new architecture — there is no intermediate branch.

```
main (production, monolithic)
  │
  └── feature/decomp-sprint (all domain code, 140+ commits ahead)
```

**Path to production**: `feature/decomp-sprint` → `dev` → `main`

**Worktree cleanup** (2026-03-11): All decomp worktrees consolidated into this single branch. Previously 7 worktrees, now 1. Story 2.1 fork endpoint cherry-picked from gala-decomp. 264 stale branches deleted.

---

## Domain Architecture Status — Honest Assessment

**IMPORTANT**: There are two ways to measure "done" — whether the domain code EXISTS (structure) vs whether API endpoints actually USE it (integration). Previous assessments conflated these. This document separates them.

### Completion Matrix

| Domain | Files | Structure | Integration | Effective | What's Done | What's NOT Done |
|--------|-------|-----------|-------------|-----------|-------------|-----------------|
| **Customer** | 46 | 100% | ~90% | **90%** | Auth, profiles, account mgmt, benefits messaging | Minor controller wiring gaps |
| **Gala** | 88 | 95% | ~75% | **85%** | 21 services, 7 VOs, state machine, fork model, events, **admin API fully decomposed** | Public event API reads still on legacy (archived event union) |
| **Ordering** | 80+ | 85% | ~75% | **80%** | Hold/confirm/release via domain services, SeatController injects interfaces, all payment gateways, OrderCreationService, OrderStateService, OrderRefundService, AdminOrderService, DTOs, domain events | **OrderCancellationService still legacy; RefundService still legacy wrapper; OrderModificationService not decomposed; ReseatService/SeatReassignmentService not decomposed; OrderController (3k LOC) still mixed** |
| **Venue** | 19 | 50% | ~50% | **50%** | VenueReadService (cached), TemplateForkingService, fork DTOs, **forking model enforced (publish is metadata-only), event-side provisioning service (diff + selective apply)** | **VenueTemplate model (992 LOC) still in legacy; VenueSyncService (1,272 LOC) still in legacy though no longer called by save/publish; Seat model in wrong domain** |
| **Notifications** | 49 | 90% | ~75% | **80%** | Email/SMS dispatch, event listeners, contracts | 28/29 tests passing; some method routing incomplete |
| **IAM** | 5 | 40% | ~20% | **30%** | Foundational contracts only | Nothing wired to endpoints |
| **Shared** | 4 | 100% | 100% | **100%** | User model, EmailAddress/PhoneNumber VOs | — |

### Booking Flow — Now Decomposed (as of 2026-03-18)

The critical booking path is fully wired through domain services:

```
POST /api/seats/hold     → SeatController → SeatHoldServiceInterface::holdSeats()                ← DDD ✓
POST /api/seats/confirm  → SeatController → BookingConfirmationServiceInterface::confirmBooking() ← DDD ✓
DELETE /api/seats/hold    → SeatController → SeatHoldServiceInterface::releaseHold()              ← DDD ✓
POST /webhook/stripe      → PaymentCompletionService                                              ← DDD ✓
POST /webhook/revolut     → RevolutWebhookHandler                                                 ← DDD ✓
```

SeatController now injects `SeatHoldServiceInterface`, `BookingConfirmationServiceInterface`, and `PaymentVerificationServiceInterface` via constructor DI. Deprecated static methods on `SeatReservation` model still exist as backward-compat shims but are no longer in the request path.

### Remaining Ordering Gap: Refunds & Cancellation

Domain `OrderRefundService` exists with full gateway abstraction (StripeRefundAdapter, RevolutRefundAdapter), but the cancellation path still routes through legacy:

```
OrderCancellationController → legacy OrderCancellationService → legacy RefundService  ← NOT wired to domain
                                                                                        OrderRefundService exists but unused by cancellation flow
```

### Controller Integration Reality

| Controller | Lines | Uses Domain Services? | Detail |
|-----------|-------|----------------------|--------|
| AdminController | 2,741 | **Admin: YES (via GalaAdminController), Public reads: NO** | All admin event CRUD + reads served by `GalaAdminController` via strangler fig overlay; legacy `getEvent`/`getEvents` remain for public API (archived event union) |
| SeatController | 1,864 | **YES (core booking)** | hold/confirm/release inject domain interfaces; availability/search still raw queries |
| OrderController | 3,010 | **PARTIAL** | Some paths use domain services, some raw |
| OrderCancellationController | — | **NO** | Still uses legacy OrderCancellationService/RefundService |
| VenueController | 2,287 | **55%** | Fork uses TemplateForkingService; save/publish are direct model ops but now correctly forking-aware (no auto-sync); new EventProvisioningController handles the event-side diff/apply flow |

---

## Test Suite Status

### Domain Tests (as of 2026-03-11)

| Domain | Unit | Feature | Total | Status |
|--------|------|---------|-------|--------|
| Customer | 159 pass | 83 pass (2 skip) | **242** | GREEN |
| Gala | 1196 pass (1 skip) | 240 pass (3 skip) | **1436** | GREEN |
| Ordering | 97 pass | 29 pass (1 skip) | **126** | GREEN |
| Venue | 274 pass (2 skip) | — | **274** | GREEN |
| Notifications | — | — | — | 41 failures (BACKLOG) |
| **TOTAL** | | | **2078 pass** | 4 domains green |

**GalaAdminController tests** (25 tests, 89 assertions): `tests/Feature/Domains/Gala/API/Admin/GalaAdminControllerTest.php` — covers index (pagination, filters, search, stats), show (by ID, by slug, 404), write ops (draft, store, update, delete, toggle publish, display order), artist CRUD (attach, detach, pivot, duplicate 409), image upload.

### Full Suite

Still has ~600+ errors/failures from legacy test issues (is_admin column, role attribute, barcode version). These are downstream of 2 root causes, not domain bugs.

---

## Legacy Overlap — The Real Numbers

| Layer | Domain (new) | Legacy (old) | Notes |
|-------|-------------|--------------|-------|
| Models | 34 | 135 (8 aliases, 8 stubs, 119 standalone) | VenueTemplate (992 LOC) still legacy |
| Services | 71 | 94 (27 use domain contracts, 66 standalone) | SeatStateService, VenueSyncService, BookingMetrics all legacy |
| Controllers | 5 | 135 | Controllers are the biggest integration gap |

### Key Legacy Services NOT Yet Decomposed

| Service | Lines | Domain It Belongs To | Why It Matters |
|---------|-------|---------------------|----------------|
| OrderCancellationService | ~100 | Ordering | **Cancellation flow still legacy; doesn't use domain OrderRefundService** |
| RefundService | ~300 | Ordering | **Legacy refund logic; domain OrderRefundService exists but cancellation doesn't use it** |
| OrderModificationService | ~230 | Ordering | Order changes — no domain equivalent |
| ReseatService | ~560 | Ordering | Seat changes — no domain equivalent |
| SeatReassignmentService | ~400 | Ordering | Seat reassignment — no domain equivalent |
| PaymentReconciliationService | ~540 | Ordering | Payment reconciliation — no domain equivalent |
| VenueSyncService | 1,272 | Venue | **No longer called by save/publish** (forking model bypasses it). Still used for legacy resync paths. New event provisioning service handles the diff/apply path independently. |
| AdminController | 2,741 | Gala | **Admin reads now decomposed** — only public API reads (archived event union) remain on legacy |
| OrderController | 3,010 | Ordering | Mixed legacy/domain — largest remaining integration gap |
| SeatStateService | 392 | Venue | Hold/release/block logic |
| AdjacentSeatFinderService | 576 | Venue | Neighbor seat queries |
| BookingMetricsService | 299 | Ordering | Booking analytics (lower priority) |

**Total legacy LOC in critical path: ~10,420 lines across 12 services/controllers**

---

## What's Actually Working Well

1. **Booking critical path is fully DDD** — hold/confirm/release all route through domain services via interface injection
2. **Payment layer is fully DDD** — Gateway factory, 5+ payment adapters (Stripe, Revolut, PayPal, NBE, Bank Transfer), completion/failure/recording/verification services
3. **Order state machine** — OrderStateService enforces valid transitions, fires domain events (OrderPaid, OrderRefunded, OrderCancelled, PaymentFailed)
4. **Refund domain service exists** — OrderRefundService with gateway abstraction (StripeRefundAdapter, RevolutRefundAdapter) — needs wiring to cancellation flow
5. **Gala domain services are excellent** — 21 services, 7 value objects, state machine, fork model, domain events
6. **Type safety achieved** — 6 ordering enums with normalization, replacing all magic strings
7. **Cross-domain contracts work** — 27+ interfaces defining clean boundaries
8. **Read services with caching** — VenueReadService, GalaReadService properly cache-aside
9. **Fork model works** — Template → Gala forking with seat provisioning is production-ready
10. **Domain tests are green** — 1,986 tests passing across 4 domains

---

## What's Alarming

1. **Cancellation/refund flow bypasses domain** — OrderCancellationController → legacy OrderCancellationService → legacy RefundService, even though domain OrderRefundService exists with proper gateway abstraction
2. **VenueTemplate model still in legacy** (`app/Model/VenueTemplate.php`, ~1100 LOC). Core editor + provisioning operations now route correctly (forking model enforced, save creates drafts, publish is metadata-only) but the underlying model still lives in legacy. Promotion to `app/Domains/Venue/Models/` is deferred — touching this file is high-risk because it's in the booking critical path.
3. **OrderController is a god object** — 3,010 LOC, mixed legacy/domain, largest remaining integration gap
4. **Seat model is in the wrong domain** — Lives in Gala, should be Venue
5. **Secondary seat operations undecomposed** — Reseat, reassignment, transfer, modification all legacy
6. **No "push branch upstream"** — event-side seat customizations (admin-painted tiers, blocked seats, manual price overrides) can't be promoted back to the venue template. Currently a one-way pull (venue → event).

---

## Active Work

### Dress Rehearsal + Cleanup Bundle — COMPLETE (2026-04-09)

A single marathon session that took the dev environment from "DDD deployed
and smoke-tested" (2026-04-08) to "real prod data restored, all critical
paths verified end-to-end, 5 main-deploy-blocking bugs fixed, frontend
Vercel dev deploy green for the first time in 14 days."

**Why this session happened**: the 2026-04-08 deploy verified that the
DDD code BOOTS on dev, but nothing had actually exercised it against
realistic data. Before merging to main, we needed the dress rehearsal —
restore real prod data to dev, run all migrations, hit every domain,
validate every writer. The alternative was finding these bugs on prod.

**Critical bugs caught by the dress rehearsal** (each one would have
broken the main deploy):

1. **`migrate_existing_gala_tiers` unique-constraint crash.** The
   migration tried to reassign tier rows between template versions but
   didn't handle the case where multiple older template versions carried
   the same color the latest version no longer has. Real prod data has
   a venue with 190+ template versions — exactly the shape that triggers
   the crash. Fix: delete old-template tiers instead of reassigning
   (the latest version is the canonical tier set; step 1 of the
   migration already migrated seat references to event-scoped copies).
   Commit `eb272946`.

2. **`orders.currency` column missing.** Migration
   `2026_01_08_140000_add_currency_to_orders_and_normalize_events` was
   part of batch 999 — inserted into the migrations table without ever
   running. Dev/prod don't have the column, but decomp code
   (`RevolutRefundService`, `BookingConfirmationService`, `PaymentLogger`,
   ...) reads `$order->currency`. Every payment flow would crash on
   main deploy. Fix: `2026_04_09_120000_fix_batch_999_migrations_that_
   never_ran` adds the column if missing, idempotent.

3. **`orders.status = 'expired'` on 151 prod rows (23% of all orders).**
   The expiration cron (`CleanupAbandonedPayments`) writes `expired`
   directly. Not in any canonical enum. Under the two-axis model
   documented in `docs/architecture/order-state-machine.md`, `expired`
   is a **subtype of cancelled** distinguished by
   `cancellation_reason='hold_expired'`. Fix: add `cancellation_reason`
   column + backfill 151 rows + update the cron to write the canonical
   pair + update `FinancialReportingController::calculateExpiredOrders()`
   to query the new pattern.

4. **8 code paths writing invalid `orders.payment_status` values.**
   With MySQL `strict=false` these silently coerced to empty string
   (98 dev orders already had `payment_status=''`). Fix: walk every
   writer, replace `'succeeded'`→`'paid'`, remove `'failed'` writes
   (belong in `payment_transactions` not `orders`), remove `'cancelled'`
   writes (same), map `'external'`→`'paid'` (payment_method carries
   the signal), map `'voided'`→`'pending'` + `status='cancelled'` +
   `cancellation_reason='admin_cleanup'`.

5. **DI binding mismatch crashing `artisan route:list` and the
   `DomainOrderCancellationService` constructor.** `OrderRefundServiceInterface`
   was bound to `OrderRefundService`, but the class didn't implement
   the interface (different method signatures, four distinct `RefundResult`
   classes in the repo). Worked on normal HTTP requests because lazy
   resolution, would have crashed the first time an admin tried to
   cancel an order on prod. Fix: couple `DomainOrderCancellationService`
   directly to the concrete class.

**Secondary blockers discovered**:

6. **`WristbandPreviewController` env guard in constructor** crashed
   `artisan route:list` on staging. Moved to middleware closure so the
   controller can instantiate cleanly for route introspection.

7. **`Admin::hasPermission()` / `Admin::getPermissions()` crashed on
   every admin API request** because they queried a `permission_overrides`
   table that was archived out of active migrations on 2026-01-23
   (IAM-2.4 / GG-076). Per 2026-04-09 decision, the permissions refactor
   is deferred — admin is effectively "everything via role". Fix:
   remove the override merge step from both methods, restore role-based
   permissions only. Commit `773b2f80`.

8. **`DevOnlyMiddleware` + `RouteServiceProvider::mapDevRoutes()`
   allowlist didn't include `staging`**, so `/__test/admin/setup` and
   `/__test/customer/setup` fixture endpoints were unreachable from
   dev-api. Added `staging` to both allowlists (prod stays out — it's
   `APP_ENV=prod` which is in neither list). Also set
   `DEV_AUTH_LOCALHOST_ONLY=false` in dev `.env` so the fixture
   endpoints accept non-localhost requests.

**Order state machine — two-axis model** (new,
`docs/architecture/order-state-machine.md`):

- `orders.payment_status` = **money mechanics** (accounting truth).
  6 values: `pending, processing, deposit_paid, paid, partially_refunded,
  fully_refunded`. Answers "where's the money?".
- `orders.status` = **business lifecycle** (operations truth).
  6 values: `pending_payment, reserved, paid, partially_refunded,
  fully_refunded, cancelled`. Answers "what is operations doing with
  this order?".
- `cancellation_reason` column distinguishes subtypes of `cancelled`:
  `hold_expired`, `fraud`, `admin_cleanup`, `customer_cancelled`,
  `gateway_failed`, `payment_timeout`, `duplicate`, `legacy_unknown`.
- Previous single-column enum (from the 2025-10 design) would have
  needed 9+ values conflating three different questions. The two-axis
  model keeps each column answering one question with 6 values.
- Gateway-level detail lives in `payment_transactions.status` + metadata
  JSON. `payment_method` on orders is the UX-facing label the customer
  chose ("apple_pay", "card"); `payment_transactions.payment_gateway`
  is the backend processor ("revolut", "stripe", future "egypt_bank" /
  "paris_bank"). **Do not normalize between them** — an Apple Pay
  charge processed by Revolut is legitimately both.

**5 cleanup migrations shipped** (batch 1003 + 1004 on dev):

| Migration | What |
|---|---|
| `2026_04_09_120000_fix_batch_999_migrations_that_never_ran` | Adds `orders.currency` column, idempotently backfills stale `refunded` → `fully_refunded`, NULLs the one `payment_method='test'` row discovered in the diagnostic |
| `2026_04_09_120100_add_cancellation_reason_to_orders` | New VARCHAR(64) column + `(status, cancellation_reason)` index |
| `2026_04_09_120200_backfill_expired_orders_to_cancelled` | 151 `expired` → `cancelled + hold_expired`; 1 legacy `cancelled` → `legacy_unknown` |
| `2026_04_09_120300_fix_orders_status_default_value` | `DEFAULT 'pending'` → `DEFAULT 'pending_payment'` |
| `2026_04_09_120400_sync_payment_status_with_full_refunds` | 4 orders with `status=fully_refunded + payment_status=paid` brought into sync |

All migrations verified on the dev DB (real prod data + all dev-only
migrations applied on top). Post-migration distribution:

```
orders.status:            paid 503 | cancelled 152 | fully_refunded 9
orders.payment_status:    paid 508 | pending 151 | refunded 9
cancellation_reason:      hold_expired 151 | legacy_unknown 1
seat_reservations.status: booked 1045 | blocked 461 | reserved 3
orders.status default:    'pending_payment'
```

**13 code files modified** (summary):

- `PaymentVerificationService.php` — `'confirmed'` → `STATUS_PAID`
- `CleanupAbandonedPayments.php` — writes `cancelled + hold_expired` not `expired`
- `StripePaymentController.php` — 4 writers: `'succeeded'`→`'paid'`, `'failed'`→`'pending'`
- `RevolutPaymentController.php` — 4 writers including seat status `'confirmed'`→`'booked'`, `'released'`→`'cancelled'`
- `AdminOrderService.php`, `BookingConfirmationService.php` — `'external'`→`'paid'`
- `AuditCleanupCommand.php` — `'voided'`→`cancelled + admin_cleanup`
- `FinancialReportingController.php` — reader updated to the new pattern
- `OrderStateService.php` — removed EXPIRED/FAILED from transition targets
- `OrderStatus.php` enum — marked EXPIRED/FAILED `@deprecated`, removed from `canonicalValues()`
- `Order.php` model — added `DEPOSIT_PAID`/`RESERVED` constants, deprecated `FAILED`/`WAIVED`
- `DomainOrderCancellationService.php` + `OrderingServiceProvider.php` — DI coupling fix
- `WristbandPreviewController.php` — env guard moved to middleware
- `Admin.php` — permission overrides removed from `hasPermission()` + `getPermissions()`
- `DevOnlyMiddleware.php` + `RouteServiceProvider.php` — staging allowlist

**Migration dress rehearsal — "archive discovery"**:

Most alarming finding of the session. The `normalize_order_status_values`,
`normalize_payment_method_values`, `add_currency_to_orders`, and related
migrations were archived to `database/migrations_archive/pre_stabilization_
20260123/` in commit `7b9dd835` ("feat(notif): Complete NOTIF domain
schema stabilization and test fixes") on 2026-01-23. **49 migrations total
were archived that day**, spanning Dec 9 2025 → Jan 22 2026.

- 7 of them were manually inserted into the migrations table with
  `batch = 999` to mark them as applied without running. Our fix-up
  migration (migration 1 above) handles this subset.
- Several were re-created in the active dir under the same filename
  (notification_preferences, video_collection_id, publishing_fields,
  etc.) and ran normally as part of the batch 1002 dev migration run.
- **Some were archived with NEITHER treatment** — no batch-999 marker,
  no re-creation in active dir. `permission_overrides` is one of them;
  several others may be too (task #32 is the systematic audit).

**Why this matters for main deploy**: we don't yet know how many
archived migrations are "silently missing" from the prod schema. Some
may have been applied via direct SQL at the time of the archive; some
may not. The permission_overrides case shows the failure mode —
everything works until someone hits the code path, at which point
SQL errors cascade.

**Frontend Vercel dev deploy** (also completed this session):

First clean 3/3 build in 14 days. 5 pre-existing frontend type errors
caught by actually watching the deploy pipeline:

1. `packages/venue-canvas-v2/src/index.ts` — deleted in commit `734ac349`
   14 days ago, but `package.json` + `vite.config.ts` still referenced it.
   Every Vercel build has been failing silently. Restored as a minimal
   barrel file re-exporting `VenueTemplate`, `VenueObject`,
   `CanvasRenderProps` from their internal locations.
2. `V2BookingView.tsx` — passing `tiers` + `cartPosition` props to
   `VenueCanvasReadOnly` that don't exist on its props interface.
   Dead props, removed. Follow-up task #29 to decide whether the
   customer legend should come back.
3. `UseSeatDetailsResult` interface missing `event` field that
   `useSeatDetailsEnhanced` returns.
4. `EventData` type missing `currency` field that `seat-tooltip.tsx`
   reads.
5. `Badge variant="destructive"` (shadcn convention) vs admin-ui's
   variant name `"error"`.

**Deployed URLs (all at decomp FE)**:
- `dev.globalgala.com` → `gg-fe-public` (Vercel dev env)
- `admin.globalgala.com` → `gg-fe-admin` (Vercel dev env)
- `mbs.globalgala.com` → `gg-fe-ticket-office` (Vercel dev env)

Production URLs (www.globalgala.com etc.) **unchanged** — still on the
4-month-old main-branch deployment.

**Backend health verified across all 4 layers**:

- **Layer 1** (app boots): `artisan route:list` returns **788 routes**
- **Layer 2** (domains): all 6 domains load and query (Customer 737 users,
  Gala 2 events/3147 seats/36 tiers, Ordering 664 orders/1509 reservations/
  262 transactions, Venue 191 templates, Notifications, Ticketing)
- **Layer 3** (public API): `/api/health`, `/api/events`,
  `/api/events/{slug}/availability` (700KB), `/api/events/{slug}/venue`
  (570KB) all return HTTP 200 with real data
- **Layer 4** (authenticated admin API): `/__test/admin/setup` →
  `/api/admin/events` (list) → `/api/admin/events/draft` (create) all
  return 200/201 with admin JWT

**Main-deploy gate — CLEARED 2026-04-09 evening**:

Task #32 (the archive audit) is done. Built a static analysis tool
(`database/scripts/audit-archived-migrations.php`, 518 LOC) that
parses every archived migration for DDL, queries information_schema
to check current state, and classifies each file. First run found:

- 5 MISSING tables/columns
- 3 PARTIAL alters (1 false positive, 2 real)
- 4 REPLACED (newer active migrations cover them)
- 7 EXISTS (applied via direct SQL at some point)
- 12 no-DDL (log/checklist only)
- 5 raw DB::statement flagged — all verified EXISTS via SQL queries

Resurrected 6 migrations as 2026-04-09 timestamped files
(`2026_04_09_130000_*` through `_130500_*`):

  1. `create_access_logs_table` — IAM AuditAccess middleware
  2. `create_gala_state_transitions_table` — Gala state metrics
  3. `create_template_versions_table` — TemplateVersion model + fork validation
  4. `fix_order_audit_log_order_id_fk` — audit log column type fix
  5. `add_gala_columns_to_booking_access_codes` — 3 columns used by GalaAccessCode
  6. `add_fk_venue_fork_data_template_id` — events.source_template_id column + FK

All 6 ran cleanly on the prod-restored dev DB.

Explicitly NOT resurrected (documented):
- `create_permission_overrides_table` — permissions refactor deferred,
  `Admin::hasPermission()` already short-circuits past the override system
- `create_features_table` — `FeatureFlagGate` is env-var driven,
  no DB table dependency

Final audit re-run shows **0 PARTIAL + 2 MISSING** (both intentionally
deferred). The main-deploy blocker is cleared.

Audit report committed at `docs/MIGRATION-ARCHIVE-AUDIT-20260409.md`
as the permanent artifact.

**Remaining tasks** (none gating main):
- #17, #22, #24, #25, #26, #27, #29, #30, #31, #33, #34 — cleanups
  and follow-ups, any can be handled after main deploy lands.

**Rollback anchor**: `/home/globalgala/deploy_anchors/dev_pre_prod_
restore_20260409_104224.sql.gz` preserved outside the rotating backup
window so the dev DB can be restored to its pre-session state if
needed.

### Dev Deployment + Deploy Script Hardening — COMPLETE (2026-04-08)

First full deploy of `feature/decomp-sprint` to the dev environment
(`dev-api.globalgala.com`). The backend is now running the full DDD stack
against the dev database (`globalgala_2026_dev`) with queue workers active.

**Deployed commit:** `81a027b1` (feature/decomp-sprint)

**Dev environment:**
- Host: `globalgala_prod` SSH target, cPanel (PHP 8.2.30)
- Path: `~/public_html/2026_backend_dev`
- Database: `globalgala_2026_dev` on localhost MySQL
- Queue worker: screen session `queue-worker-staging`, database-backed queue
- App environment: `staging`

**Hitches encountered on first deploy** (all fixed in the commit above):

1. **`config/app.php` hardcoded dev-only providers without env guards.**
   `Laravel\Telescope\TelescopeServiceProvider` and `App\Providers\TelescopeServiceProvider`
   were registered in the static providers array. Since Telescope is in
   `require-dev`, `composer install --no-dev` on the server strips the
   package → every `artisan` command crashes with "Class not found" →
   composer's post-install `package:discover` can't run → `bootstrap/cache/packages.php`
   never regenerates → locked circular dependency. Moved the registration
   into `AppServiceProvider::register()` guarded by
   `$this->app->environment('local', 'testing')` and
   `class_exists(..., false)` checks.

2. **Pre-deploy DB check used `php artisan tinker --execute="DB::connection()->getPdo();"`.**
   Booting the full framework to test connectivity meant ANY provider /
   autoload issue made the DB check fail even when the database was
   perfectly healthy. Replaced with a direct `mysql` CLI query
   (`check_database_connection` atom) that reads DB_HOST/USER/PASSWORD/
   DATABASE straight from `.env`. Falls back to a standalone PHP PDO
   check if the `mysql` binary isn't available.

3. **`deploy_code_workflow` had no pre-composer cache clearing.**
   Ran composer install straight after git pull, so stale
   `bootstrap/cache/packages.php` from a prior run with dev deps would
   reference packages that `--no-dev` would remove. Added an explicit
   `rm -f bootstrap/cache/*.php` step before composer install to break
   the circular dependency.

4. **`install_php_dependencies` used `--ignore-platform-reqs`** (blanket)
   on cPanel, hiding real platform issues. Narrowed to
   `--ignore-platform-req=ext-exif` (the specific missing extension on
   GlobalGala's cPanel host).

5. **`deploy-dev.sh` line 124 pre-script autoload dump** had a redundant
   `--no-dev` that ran BEFORE the new cache cleanup step. Removed — the
   main composer install in `deploy_code_workflow` handles it properly.

6. **Dirty working directory on the dev server** (log files, daily
   reconciliation CSVs from cron, some mistyped-command garbage files
   like `-`, `Mo,`, `id,`, `mo@globalgala.com,`). Stashed to
   `stash@{0}: pre-decomp-sprint-deploy 2026-04-08` — safe to drop
   later. Cleanup strategy for these should be added to the deploy
   flow (log rotation, reconciliation archive to a separate directory).

7. **`deploy-production.sh` (the 326-line legacy script) is a relic**
   from a pre-cPanel era — assumes Debian/systemd, `/var/www/showprima`,
   `www-data` user, Redis queue, npm Mix. Added a deprecation guard at
   the top that exits with a clear error. The canonical production
   path is `deploy-prod.sh` → `deploy-dev.sh --branch=main` (11-line
   wrapper), so all the hardening above automatically applies to the
   main deploy when we run it.

**New atoms added to `scripts/_deploy-atoms.sh`:**
- `get_db_host`, `get_db_user`, `get_db_password`
- `check_database_connection` (mysql CLI with PDO fallback)

**Verified on dev server after the fixes:**
- Pre-check passes (direct mysql query validates connectivity)
- `composer install --no-dev --optimize-autoloader` succeeds
- `vendor/laravel/telescope` and `vendor/barryvdh/laravel-ide-helper`
  are absent from the server (as expected for --no-dev)
- Laravel boots cleanly, all `artisan` commands work
- Migrations completed (33 ran in the initial deploy, 0 pending since)
- `/api/health`, `/api/events`, `/api/venues`, `/api/cms-images` all return 200
- Queue worker running, database queue connection healthy
- Full deploy takes ~19 seconds

**Outstanding dev server issues (not blocking, noted for follow-up):**
- `scripts/_deploy-molecules.sh` line 65 has a cosmetic bash syntax error
  when parsing `stat` output for the backup verification, but the backup
  file is still created successfully — false negative
- Post-deploy health check contradicts itself on queue worker status
  ("running (PID X)" immediately followed by "NOT running ... Screen
  session found but worker not detected") — false alarm, worker IS running
- 113 pre-existing failed jobs in the queue from previous activity
- 3 Mailables not implementing `ShouldQueue` (will send synchronously)
- `AppServiceProvider::register()` only registers dev providers in
  `local`/`testing` envs — Telescope is intentionally disabled on
  `staging` (dev-api) for security. If needed on dev-api, add `staging`
  to the env list.

**Production readiness:** The deploy-dev run now mirrors what a main
deploy will do, including `--no-dev`. When we're ready to deploy main,
`scripts/deploy-prod.sh --branch=main` should Just Work. Still pending
before that:
- Figure out the main deployment target directory (currently nothing
  documented for where main lives on the server)
- Verify `.env.production` values (Stripe live keys, Revolut live mode,
  etc.)
- Coordinate a maintenance window — the first main deploy WILL run
  20+ new migrations which touch `events`, `venue_templates`, `seats`,
  `seat_reservations`, `price_tiers`, `email_logs`, `users`, `orders`.

### Venue Editor + Forking Model — COMPLETE (2026-04-07)

A near-total rebuild of the venue template editor on the v2 canvas, plus
enforcement of a proper git-style forking model between venue templates
and event seat instances.

**Frontend (`?canvas=v2` editor at `/venues/[id]/templates/[id]/edit`)**:

- Section drawing tool wired into the canvas (rectangle drag → CreateSectionCommand)
- Click selection with parent-aware resolution (clicking a child seat selects its parent table)
- Box/marquee selection respects the same parent-table grouping
- Drag guard prevents moving child seats independently
- Oriented bounding box that persists rotation across click cycles (single-object OBB)
- Pinned rotation handle (handle stays anchored, only the pointer position drives angle)
- Direction gizmo on the table head (short edge), apex pointing outward
- Cascade delete (TABLE → child SEATs) with batch undo (single command, not iterative)
- Section ghost fix: `template.sections[]` now syncs on move/transform of SECTION objects
- Keyboard shortcuts: Delete, Cmd+Z, Cmd+Shift+Z, Escape, V
- Save creates DRAFT versions with proper `venue_id` linkage; auto-redirects to new template ID
- Publish is now metadata-only — see Forking Model below

**Backend forking model enforcement** (`app/Model/VenueTemplate.php`,
`app/Http/Controllers/API/VenueController.php`):

- `VenueTemplate::saveVenueTemplate()` — creates draft versions, no longer
  calls `VenueSyncService::syncSeatsFromTemplate` (was the source of accidental
  event seat mutations on save). Resolves `venue_id` from request body or event.
- `VenueTemplate::publish()` — metadata-only. Marks the version as published,
  archives prior published versions, but does NOT touch any event seats and
  does NOT transfer event_venues links. Removed the `guardAgainstActiveGalas`
  check since publishes can't affect live galas anymore.
- `VenueTemplate::getForEvent()` — now respects `event_venues.venue_template_id`
  as the primary lookup (the fork link). Falls back to the venue's published
  template only if no fork exists. Previously bypassed the fork link entirely,
  so publishing a new venue template instantly "leaked" into all events.
- `VenueTemplate::toVenueTemplate()` — exposes template `id` so the frontend
  can update its URL after saving a new version.
- `VenueController::getPublishPreview` — rewritten as informational only.
  Lists events using the venue but always returns 0 changes (publish doesn't
  touch them). Warning text explains they continue with their existing layouts
  until re-provisioned from the event dashboard.
- `VenueSyncService::generatePublishPreview` bug fix: `$templateSeatIds`
  tracked UUID XOR human-readable id, but the deactivation check compared
  against `$dbSeat->seat_id` (human-readable). Now tracks both forms for
  tables, child seats, and standalone seats.

### Event-Side Provisioning — COMPLETE (2026-04-07)

The "pull venue template updates" flow on the event side. Replaces the
auto-sync that previously bypassed the fork link.

**New backend services** (`app/Services/`):

- **`EventProvisioningDiffService`** — pure read-only diff between an event's
  forked template (base, from event_venues) and the venue's currently
  published template (target). Returns structured changes:
    - Tables added / removed / moved / rotated / resized
    - Seats added / removed / tier_changed / price_changed
    - Each change has a stable `change_id` and a `safe` flag
    - Booked seats automatically marked as unsafe to remove (with reason)
    - UUID-first matching with `seat_id` fallback
- **`EventProvisioningService::apply()`** — write side. Re-runs the diff
  server-side and validates each requested change is still safe (defends
  against stale UI). Mutates inside a transaction with `Seat::withoutEvents()`.
  Booked seats are NEVER removed. Tables with bookings CAN be moved/rotated/
  resized (child seat IDs unchanged). Updates `event_venues.venue_template_id`
  and `events.venue_fork_data.last_sync_at` after success.

**New endpoints** (`app/Http/Controllers/API/Admin/EventProvisioningController`):

- `GET  /api/admin/events/{event}/venue-template-diff`
- `POST /api/admin/events/{event}/venue-template-provision`

**Frontend**:

- `/events/[eventId]/provision` page — diff/merge canvas + change checklist
  sidebar. The canvas updates in real-time as the user toggles checkboxes,
  showing a live preview of the merged result via a client-side merge service.
- `VenueUpdateBanner` on `/events/[eventId]` — shows when the event's fork
  version is older than the venue's published template. Click to navigate to
  the provisioning page.
- `useEventProvisioningDiff` / `useApplyProvisioning` hooks (React Query)
- `useProvisioningSelection` hook for managing per-change checkbox state
- All changes off by default — explicit per-change opt-in (with a "select all
  safe" convenience button)

**Verified end-to-end** against event 28 + venue 25: edit template → save →
publish (event seats untouched) → banner appears on event dashboard → click
through to provision page → review diff → apply → seats merged → fork tracking
updated.

**Out of scope (future work)**:
- Seat reassignment when removing tables with bookings (separate epic)
- Position-based matching for duplicate-overlay tables
- "Push branch upstream" (event-side edits → venue template)
- Orphan seat cleanup (DB seats not in any template version, e.g. from old broken saves)

### Gala Admin API Decomposition — COMPLETE (2026-03-31)

**GalaAdminController** (`app/Domains/Gala/Controllers/GalaAdminController.php`) now serves the entire admin event API via strangler fig route overlay at `routes/api/admin/gala-events.php`:

| Endpoint | Method | What |
|----------|--------|------|
| `GET /api/admin/events` | `index()` | Paginated event list with booking/capacity stats, price ranges, 6 filter types |
| `GET /api/admin/events/{id}` | `show()` | Single event with full admin stats (booking + capacity). Supports ID or slug lookup |
| `POST /api/admin/events` | `store()` | Create event with full validation |
| `POST /api/admin/events/draft` | `createDraft()` | Draft creation (name + start_date minimum) |
| `PUT /api/admin/events/{id}` | `update()` | Partial update with artist sync |
| `DELETE /api/admin/events/{id}` | `destroy()` | Soft delete (409 if bookings exist) |
| `POST /api/admin/events/{id}/toggle-publish` | `togglePublish()` | Publish/unpublish toggle |
| `POST /api/admin/events/display-order` | `updateDisplayOrder()` | Bulk display order |
| `POST /api/admin/events/{id}/image` | `uploadImage()` | Image upload via GalaImageService |
| `POST /api/events/{id}/artists` | `attachArtist()` | Artist management (attach/detach/update pivot) |

**Improvements over legacy**: `index()` uses SQL-level pagination (`skip`/`take`) instead of fetch-all-then-paginate; price range queries are batched across events (legacy ran per-event in a loop).

**Deliberately NOT moved**: Archived event union query stays on legacy `AdminController::getEvents()` for the public API. Archived events use a separate `archived_events` table with a different schema (pre-platform events). Options when we hit this: (1) leave on legacy (read-only, low risk), (2) migrate archived into events table with a flag, (3) dedicated `PublicGalaController`.

### Admin Frontend Cleanup — COMPLETE (2026-03-31)

- Sidebar stripped to core actions (Events, Venues, Orders, Reservations, Customers, Content Library, Scanners, MBS, Support, Administration). Removed Email Management, Marketing, Analytics & Finance — re-add when ready.
- Dashboard rewritten: action tiles with active copy ("Set up an event", "Look at orders"), recent events with inline stats, GBP currency.
- EN/AR i18n: lightweight context-based system, `useI18n()` hook, language toggle in TopBar, Arabic RTL support.
- GalaSetupWizard wired into events page with save vs publish respecting Published checkbox.
- `status_label` from backend used for status badges throughout.

### Event Dashboard (`/events/[id]`) — FUNCTIONAL (2026-04-07)

The event detail page now loads cleanly and is the primary seat management
interface for events. Major surgery completed across multiple sessions:

- Switched from broken public endpoint to authenticated `/admin/events/{id}`
- AdminVenueCanvas rewrite (root cause of earlier 200% CPU + 1GB memory spikes
  was a ResizeObserver feedback loop from a min-h container with content-driven
  legend chrome — fixed by isolating canvas chrome in the parent)
- SeatDetailsPanel shows section name + tier prefix (was showing useless "general")
- Optimistic paint uses each seat's actual tier color on release (was hardcoded green)
- VenueUpdateBanner integration for the new provisioning flow
- Imperative Konva paint on capsule seats (id prop now set on Group for findOne lookups)
- Stage ref exposed via ref callback instead of useEffect (avoids zombie refs after
  resize)

Memory and stability are good. Seat block/release/shadow_sold all flow through
the apiClient with proper auth and error toasts.

### Ordering Phase 2: Refunds & Cancellation Decomp (Starting)

**Goal**: Wire cancellation flow through domain `OrderRefundService` instead of legacy `RefundService`. The domain service already has gateway abstraction — the gap is the controller/orchestration layer.

**What exists (domain)**:
- `OrderRefundService` with `RefundGatewayInterface` adapters (Stripe, Revolut)
- `OrderStateService` with refund/cancellation transitions and domain events
- `OrderCancelled`, `OrderRefunded` domain events

**What needs work**:
- Legacy `OrderCancellationService` still orchestrates cancellation
- Legacy `RefundService` still processes refunds
- `OrderCancellationController` still wired to legacy

### Venue + Gala Integration Epic (Paused)

Epic: `docs/epics/epic-venue-gala-integration.md` (v1.1)
API Contracts: `docs/api-contracts/venue-gala-api-contracts.md`

**Story 2.1**: Fork trigger endpoint — DONE (cherry-picked from gala-decomp)
**Story 2.2**: Fork integration tests — DONE (14 tests, 42 assertions)

---

## Repository Structure (Post-Cleanup)

### Worktrees
```
showprima/                          main (production)
showprima-user-account-decomp/      feature/decomp-sprint (THIS — single source of truth)
```

### Branches (showprima repo — 9 total)
```
main, dev                           Production branches
feature/decomp-sprint               THE DDD branch (all domain work)
feature/gala-decomp                 Reference (cherry-picked, worktree removed)
feature/domain-boundary-enforcement  2 unique commits (boundary enforcement middleware — for later)
hotfix/* (4 branches)               Production hotfixes
```

---

## Key Files & Locations

| What | Where |
|------|-------|
| Domain code | `app/Domains/{Customer,Gala,Ordering,Notifications,Venue,IAM,Shared}/` |
| Legacy models | `app/Model/` (135 files) |
| Legacy services | `app/Services/` (94 files) |
| God controllers | `app/Http/Controllers/API/{AdminController,SeatController,OrderController}.php` |
| Domain tests | `tests/Unit/Domains/`, `tests/Feature/Domains/` |
| Factories | `database/factories/` (root + `App/Model/` subdirs) |
| Schema dump | `database/schema/mysql-schema.sql` |
| Integration epic | `docs/epics/epic-venue-gala-integration.md` |
| API contracts | `docs/api-contracts/venue-gala-api-contracts.md` |

---

## Dev Infrastructure: Database & Admin Seeding

### Test DB vs Dev Server Conflict

The backend `.env` uses `DB_DATABASE=showprima_test`. PHPUnit's `RefreshDatabase` trait runs `migrate:fresh` on first test, which wipes all tables including `admins`. This breaks the dev server (admin login returns 401).

**Fix (2026-03-31)**: `DevAdminSeeder` runs automatically after `migrate:fresh` via `$seed` / `$seeder` properties on the base `TestCase`:

- **Seeder**: `database/seeders/DevAdminSeeder.php` — creates super_admin role + dev admin account
- **TestCase**: `tests/TestCase.php` — `$seed = true; $seeder = DevAdminSeeder::class`
- **Credentials**: `admin@globalgala.com` / `devpassword123` (dev only, seeder refuses to run in production)

The seed runs as part of `migrate:fresh --seed` (before the test transaction begins), so it persists after test rollback.

### Loading a Production Backup

Full runbook: `docs/LOCAL-DB-IMPORT-RUNBOOK.md`

```bash
# 1. Import dump (into showprima_test or globalgala depending on which backend you're running)
gunzip -c ~/Downloads/backup_YYYYMMDD_HHMMSS.sql.gz | mysql -u globalgala -pglobalgala_dev_pass -h 127.0.0.1 showprima_test

# 2. Reset admin passwords (prod hashes won't work locally)
HASH=$(/opt/homebrew/opt/php@8.4/bin/php -r "echo password_hash('devpassword123', PASSWORD_BCRYPT);")
mysql -u globalgala -pglobalgala_dev_pass -h 127.0.0.1 showprima_test -e "UPDATE admins SET password = '$HASH';"

# 3. Run pending migrations (see runbook for skip recipe if tables already exist)
/opt/homebrew/opt/php@8.4/bin/php artisan migrate
```

### Admin Seeders

| Seeder | Purpose | When to use |
|--------|---------|-------------|
| `DevAdminSeeder` | Lightweight — 1 super_admin, hardcoded dev password | Runs automatically after tests; also `php artisan db:seed --class=DevAdminSeeder` |
| `AdminRoleSeeder` + `AdminSeeder` | Full — all roles + 3 admin accounts, env-based passwords | Production deploys, full reseeds (`php artisan db:seed`) |

---

## Decision Log

| Date | Decision | Rationale |
|------|----------|-----------|
| 2025-Q4 | Near-total refactor instead of strangler fig | Legacy god objects too tangled for incremental migration |
| 2026-01 | 7 bounded domains defined | Customer, Gala, Ordering, Notifications, Venue, IAM, Shared |
| 2026-01 | Schema dump approach for tests | 104 tables too many to create via migrations each test run |
| 2026-03-09 | Domain tests first, defer legacy | Need proof-of-life before full legacy test cleanup |
| 2026-03-09 | CLAUDE.md total rewrite | Old docs described architecture-v2 that never existed |
| 2026-03-11 | Consolidate all worktrees | 7 worktrees → 1; cherry-pick unique commits; delete 264 stale branches |
| 2026-03-12 | Honest completion reassessment | Previous 100%/100%/55%/50% ratings were measuring structure not integration |
| 2026-03-18 | Ordering Phase 1 confirmed complete | hold/confirm/release fully decomposed; SeatController injects domain interfaces; deprecated model statics remain as shims only |
| 2026-03-18 | STATE_OF_PLAY corrected | Previous assessment said booking flow was legacy — it was already decomposed; Ordering bumped from 65% to 80% effective |
| 2026-03-31 | Gala admin API fully decomposed | GalaAdminController serves all admin event CRUD + reads via strangler fig; legacy AdminController retained for public API (archived event union) |
| 2026-03-31 | Skip archived events for admin controller | Admin panel never shows archived events (pre-platform); archived event union stays on legacy for public API. Three options documented for future: leave legacy, backfill into events table, or dedicated PublicGalaController |
| 2026-03-31 | DevAdminSeeder for test/dev coexistence | RefreshDatabase wipes admins table, breaking dev server login; DevAdminSeeder auto-runs after migrate:fresh via base TestCase $seed property, persists outside test transaction |
| 2026-04-07 | Forking model enforced — venue publish is metadata-only | Old model auto-synced venue template changes into all linked events on publish, silently mutating booked seat data. New model: `event_venues.venue_template_id` is the immutable fork link; `getForEvent` reads it; publish only flips status flags. Events keep their snapshot until explicitly re-provisioned. |
| 2026-04-07 | Provisioning is per-change opt-in, not all-or-nothing | Replaces the legacy `resync()` flow that applied everything at once. Admin reviews each change in a checklist, selects safe ones (booked seats are unselectable), applies. Backend re-validates server-side to defend against stale UI. |
| 2026-04-07 | Venue template save creates drafts, not published versions | Old code created `status='published'` on every save, immediately conflicting with the venue's actual published version. Now creates drafts; the explicit publish action promotes a draft. |
| 2026-04-07 | VenueTemplate model stays in legacy for now | Editor + provisioning routes correctly via the model's existing methods. Promoting the model itself to `app/Domains/Venue/Models/` is deferred — too risky to touch with the booking critical path going through it. Address in a focused refactor when the venue domain catches up structurally. |
| 2026-04-08 | Dev-only providers conditionally registered, not hardcoded in config/app.php | Telescope + IdeHelper in `require-dev` were hardcoded in the static providers array, breaking any `composer install --no-dev` flow. Now registered conditionally via `AppServiceProvider::register()` with `class_exists(..., false)` guards. Unblocks the production deploy path. |
| 2026-04-08 | Deploy DB check uses direct mysql CLI, not artisan tinker | `artisan tinker` boots the full app, so any provider/autoload issue makes the DB check fail even when the database is fine. Direct mysql CLI query reads credentials from .env and actually tests connectivity. Falls back to standalone PHP PDO if mysql binary missing. |
| 2026-04-08 | Deploy clears bootstrap/cache/*.php before composer install | Prevents the circular dependency where stale cached class references cause `artisan package:discover` to fail during composer install, which leaves the cache stale, which... Direct `rm -f` breaks the loop. |
| 2026-04-08 | `scripts/deploy-production.sh` marked deprecated | Legacy 326-line script assumes Debian/systemd/Linux; canonical production path is `deploy-prod.sh` → `deploy-dev.sh --branch=main` (11-line wrapper). Deprecation guard added so running it fails fast with a clear redirect. |
| 2026-04-09 | Dress rehearsal: restore real prod backup to dev, run dev migrations on top | The 2026-04-08 deploy proved the code boots; this run proved it works against realistic data. Found 5 main-deploy-blocking bugs that synthetic testing missed. The migration dress rehearsal is the single highest-value pre-deploy validation step we've done — it's now a standard practice before any future major merge. |
| 2026-04-09 | Two-axis order state machine — `payment_status` for money, `status` for business lifecycle | A single enum column would have needed 9+ values conflating three different questions ("where's the money?" + "what happened on the last attempt?" + "what's operations doing?"). Splitting into two columns with 6 values each keeps every value answering one question. Attempt-level detail lives in `payment_transactions.status`. See `docs/architecture/order-state-machine.md`. Supersedes the 2025-10 single-column design. |
| 2026-04-09 | Fold `expired` into `cancelled` + `cancellation_reason='hold_expired'` | From the customer's and business's perspective, an expired checkout and an explicit cancellation are the same state — the order is dead. Only the cause differs. Preserving the cause in a separate reason column keeps reports working (abandonment rate = `WHERE cancellation_reason='hold_expired'`) without polluting the top-level state enum. Backfilled 151 prod orders on dev from `expired` → `cancelled + hold_expired`. |
| 2026-04-09 | Do not normalize `orders.payment_method` — it's the UX-facing instrument, not the gateway | Earlier normalize migration wanted to consolidate `card`, `apple_pay`, `google_pay`, `credit_card`, `debit_card`, `revolut_pay` all to `revolut`. Reverted: the stored value is what the customer saw at checkout ("Apple Pay", "Card"). Gateway info ("revolut", "stripe", future "egypt_bank" / "paris_bank") lives in `payment_transactions.payment_gateway`. Conflating the two loses real business info (see real prod order 178 — paid via Apple Pay via Revolut, both facts matter). |
| 2026-04-09 | Permissions refactor deferred — `Admin::hasPermission()` restored to role-based only | IAM-2.4 / GG-076 added a `permission_overrides` grant/revoke layer to the permission check, but the supporting migration was archived out of active migrations on 2026-01-23 and never resurrected. The result: every admin permission check queried a missing table and crashed. Per 2026-04-09 decision, permissions haven't been refactored yet and the proper refactor is the LAST piece of decomp work. Until then, admin effectively = role-based permissions (super_admin has everything). The `PermissionOverride` model is kept in place for when the refactor ships. |
| 2026-04-09 | `DevOnlyMiddleware` + `RouteServiceProvider::mapDevRoutes` allowlist includes `staging` | Team-accessible dev/staging environments need `/__test/admin/setup` and `/__test/customer/setup` fixture endpoints for E2E tests. Previously the middleware only allowed `local`/`testing`, forcing SSH-to-tinker workflows. Now `staging` is allowed on both the route registration and runtime guard layers. Production (`APP_ENV=prod` or `production`) is still in neither list. |
| 2026-04-09 | Discovered 49-migration archive (`pre_stabilization_20260123/`) — systematic audit required before main | NOTIF-domain stabilization commit `7b9dd835` moved 49 migrations out of active migrations on 2026-01-23. Some were re-created under the same filename (batch 1002 today), some were marked batch 999 (the 7 we handled via the fix-up migration), some were archived without any replacement (permission_overrides, and likely several others). Task #32 is the systematic audit — walk each archived file, determine whether prod has the DDL, decide whether to resurrect. **This is the only remaining main-deploy blocker.** |
| 2026-04-09 | `DomainOrderCancellationService` couples to concrete `OrderRefundService` instead of the broken interface | `OrderRefundServiceInterface` had method signatures and return types (`App\DTOs\Ordering\RefundResult`) that didn't match the concrete `OrderRefundService` class (`refund()` with `App\ValueObjects\RefundResult`, four distinct `RefundResult` classes in the repo total). Crashed `artisan route:list` during eager container resolve. Pragmatic unblock: drop the interface from the DI binding, couple directly to the concrete class. Proper reconciliation is follow-up task #27. |
| 2026-04-09 | Venue fork flow generates unique seat UUIDs per event (verified) | Confirmed empirically: event 1 has 1762 unique seat_ids, event 3 has 1385 unique seat_ids, zero collisions between them. `GalaTierService::copyTiersFromTemplate()` also populates `source_tier_id` for lineage tracking (legacy data on dev has NULL lineage because it predates the feature — fresh forks populate correctly). This validates the fork flow is safe to exercise as the test fixture setup mechanism for E2E suites. |
| 2026-04-09 | Built static-analysis audit script for archived migrations | Rather than walking 49 archived migrations manually, built `database/scripts/audit-archived-migrations.php` to parse each file for DDL signatures, query information_schema, and classify each migration as MISSING / PARTIAL / REPLACED / EXISTS / NO-DDL. Reusable for any future archive concerns; can be wired into pre-deploy checks (exits 1 on any MISSING/PARTIAL). First run found 5 MISSING + 3 PARTIAL (1 false positive), which led to the 6 resurrected migrations. Permanent audit trail lives at `docs/MIGRATION-ARCHIVE-AUDIT-20260409.md`. |
| 2026-04-09 | Resurrected 6 archived migrations, intentionally left 2 | 6 archived migrations whose DDL is actively referenced by current code were resurrected as 2026-04-09-timestamped files with `RESURRECTED` headers explaining the history: access_logs, gala_state_transitions, template_versions, fix_order_audit_log_fk, booking_access_codes columns, events.source_template_id + FK. 2 were explicitly NOT resurrected: `permission_overrides` (permissions refactor deferred, `Admin::hasPermission()` already short-circuits) and `features` (FeatureFlagGate is env-var driven, no DB table needed). |
| 2026-04-09 | Audit-script false-positive filter: transient rename helpers | Migrations that use the add-new-copy-drop-rename pattern (e.g. `fix_order_audit_log_fk` adds `order_id_new`, copies data, drops `order_id`, renames `order_id_new` → `order_id`) leave the helper columns non-existent post-migration. The audit script was incorrectly flagging these as PARTIAL. Filter: if a column name ends in `_new` or `_old` AND the same migration contains a `renameColumn()` for that name, treat it as transient and skip the existence check. |
| 2026-04-10 | Fork flow: explicit fork endpoint now backfills legacy venue linkage | `GalaVenueForkController::fork()` was missing `events.venue_id` assignment and `event_venues` pivot row creation — auto-fork path (via `GalaManagementService::linkVenueTemplate`) did both, but the explicit endpoint skipped them. Legacy readers (`VenueTemplate::getForEvent`) gate on these fields and threw "Event X has no venue assigned". Fixed: fork controller mirrors the auto-fork linkage after successful fork. |
| 2026-04-10 | Per-gala tier model adopted — tiers assigned post-fork, not at fork time | `TemplateForkingService` no longer rejects templates with zero tiers. `SeatDTO` allows bookable seats with default £0 price. Fork creates the seat layout; admin assigns tiers afterward from the event dashboard. `GalaTierService::copyTiersFromTemplate()` deep-copies template tiers to gala-scoped tiers with `source_tier_id` lineage. Real bug surfaced on dev: `PriceTier::getFormattedPriceAttribute()` passed MySQL DECIMAL string to `CurrencyService::format()` without `(float)` cast → 500 on tiers endpoint. |
| 2026-04-10 | Bulk tier assignment accepts seat UUIDs + event_id context | Old validator required integer PKs and template-scoped tier matching. New model: frontend sends seat UUID strings (`seat_id` / `seat_number`), scoped by `event_id`. Template-match validation removed — gala tiers have `venue_template_id=null` (gala-scoped). |
| 2026-04-10 | V2 venue editor is now the default | Flipped in both editor page routes — V1 available via `?editor=v1` fallback. V2 fixes shipped: section click-through (sections don't block placement tools), property panel writes to Zustand store (was dead `onTemplateChange` callback), table rotation propagates to children, section auto-membership on table placement (baked into command, survives undo/redo), TypeAdapter V1↔V2 round-trip fixes (section groups no longer get phantom parent_uuid, background_image preserved on save). |
| 2026-04-10 | Background image panel added to V2 venue editor | Upload PNG/JPEG/SVG via CMS API, renders as non-interactive Konva layer behind all objects. Controls: visibility toggle, opacity slider, scale presets (50%/100%/200%/500%), X/Y offset. Canvas rendering was already in place — this adds the UI panel + fixes TypeAdapter to preserve `background_image` through V2→V1→save→load→V1→V2 round-trip. |
| 2026-04-10 | Email deliverability crisis diagnosed and remediated — migrated to AWS SES | Root cause: bot newsletter signups (Jan–Feb 2026) flooded the IP with emails to random/junk addresses, destroying `213.175.208.218` reputation with Gmail (`550 5.7.1 blocked`) and Yahoo (`421 4.7.0 deferred`). Compounded by cron alerts bouncing to Google-hosted `charlie+ggcron@09-07.xyz` every 5 minutes (~12,000 rejected deliveries over 6 weeks). **Fix**: (1) Switched `MAIL_DRIVER=ses` using AWS SES HTTPS API (port 443, bypasses hosting SMTP port blocks on 25/587/465). (2) Redirected cron MAILTO to local `test@globalgala.com` to stop the bounce flood. (3) Submitted Google delisting request with full headers. (4) SES production access requested (pending, typically <24h). (5) DKIM for SES configured (3 CNAME records propagating). Local delivery (`@globalgala.com`) was never affected — only external. **Follow-up by 2026-07-19**: set PTR record at Hyperslice (`213.175.208.218 → glo.globalgala.com`), verify Google reputation recovery via Postmaster Tools, add CAPTCHA to newsletter signup to prevent recurrence. |
| 2026-04-10 | Revenue fix — orders.total_amount replaces seat_reservations.price_snapshot | Revenue was over-reported by ~£47,547 (£1,026,350 vs actual £978,802.60). Root cause: 257 seat_reservations stayed `status='booked'` after their orders were cancelled/refunded. The SUM of price_snapshot included dead reservations. Fix: revenue and order_count now come from `orders WHERE status='paid' AND payment_status='paid'`. Seat count (total_bookings) stays from reservations for capacity planning. Applied to both single-event show and bulk event list endpoints. |
| 2026-04-10 | Seat block/release hardening in AdminController | Block-with-note creates a blocked reservation (seat stays active, shows note like "Available Soon"). Block-without-note deactivates the seat entirely (removed from canvas). Release reactivates + force-deletes all non-booked reservations (frees unique constraint). Cache invalidation on both paths. |
| 2026-04-10 | Bunny CDN video upload via CMS — complete pipeline | Backend: `BunnyNetService::uploadVideo()`, `AdminVideoController` (7 endpoints), `CmsFileController` detects video MIME and routes to Bunny. Frontend: `EventVideoPanel` with multi-video grid, upload zone, library picker, attach/detach via `event_videos` pivot API. `artisan bunny:sync-videos` command backfills existing Bunny library into `cms_files`. CMS files page shows video thumbnails + inline player in details modal. |
| 2026-04-10 | V2 editor: stage + label tools, seat disable, rename propagation | Stage tool: click to place 300x100 stage object. Label tool: click to place text label. Individual seat disable toggle (`available=false`). Table rename propagates to children ("Table 1-A1" → "VIP-A1"). Name input commit-on-blur. MemoizedObject comparison includes `obj.name` and `obj.available`. Table direction gizmo flipped to left edge. |
| 2026-04-10 | Video collection viewer: stop-before-play between videos | Old iframe held audio during React remount, causing overlapping playback. Fix: blank iframe src to `about:blank` before switching, remount on next animation frame. All videos autoplay on switch. |
| 2026-04-10 | Branch strategy confirmed | Frontend: `dev` branch = integration (183 ahead of main), deployed via Vercel. Backend: `feature/decomp-sprint` = integration (140+ ahead of main), deployed via SSH + deploy-dev.sh. Both merge to `main` for production. Vercel env vars (not .env.production) control deployment config. |
| 2026-04-14 | SES production access approved — external email restored | 50,000/day quota, 14/sec rate. `MAIL_DRIVER=ses` on both prod and dev. Dev also switched from `noreply-test@globalgalashow.com` (unverified domain, was silently failing) to `noreply@globalgala.com`. Queue worker on dev restarted with cron keep-alive (PID file was stale — cron thought worker was alive when it wasn't). |
| 2026-04-14 | Reservation flow: V2 canvas with tier colors + admin override | Swapped V1 `VenueCanvas` to V2 `VenueCanvasReadOnly` in SeatSelectionStep. Eliminated 576 lines of V1 nodeToSeatData mapping. Selection now stores `seat_number` directly (backend-ready, no UUID translation). Tier colors rendered via `seatOverrides` prop (passthrough added to VenueCanvasReadOnly → VenueCanvasCore). Admin "Override seat status" toggle allows booking blocked/booked seats. |
| 2026-04-14 | Reservation list: exclude blocked/shadow_sold from results | `AdminReservationService::getReservations()` was returning all `reservation_type='admin'` seat_reservations including blocked seats (121 entries cluttering the list). Added `whereNotIn('status', ['blocked', 'shadow_sold'])` to filter to real reservations only. |
| 2026-04-14 | PHP 8.3 typed constants removed for 8.2 compat | `FeeConfigVO` and `GalaPaymentConfigVO` used `public const float` / `public const array` (PHP 8.3+). Server runs 8.2 → 500 on reservation endpoint. Replaced with untyped constants + `@var` docblocks. |
| 2026-04-14 | EventSelectionStep: admin endpoint + status filter fix | Reservation event picker was hitting public `/events` endpoint (no auth, different response). Switched to `/admin/events` with JWT. Status filter now matches string statuses (`"published"`, `"on_sale"`) not just numeric `1`. |
| 2026-04-14 | White-label architecture scoped | Platform supports independent deployments per client (separate DB + .env + domain). `clubs` table can repurpose as workspace entity. DDD domains make future multi-tenant consolidation tractable. Current recommendation: independent deployments for 1-5 clients, multi-tenant when 5+. |
| 2026-04-14 | Revenue unified across all endpoints — orders-based, comp-excluded | Cleaned up other agent's CASE WHEN seat_reservations hack in AdminController + fixed their OrderController syntax error (mismatched braces). OrderController `getOrderStats` (/admin/orders/stats) also switched from seat snapshots to orders table. FinancialReportingController comp filter made case-insensitive. All revenue queries now: `orders WHERE status='paid' AND payment_status='paid' AND payment_method NOT IN (comp, complimentary)`. |
| 2026-04-14 | Customer totals: exclude failed/comp orders | CustomerController list + show + OrderRepository getHighValueGuestCustomers all filtered to paid non-comp orders. Previously counted all orders regardless of payment outcome. |
| 2026-04-14 | Placeholder quick actions removed from customer list | 6 fake buttons (Reset Password, Check Status, Sign In As, Suspend, Login History, Send Support) were showing toast-only with no backend. Removed to avoid demo confusion. |
| 2026-04-14 | Prod CMS assets synced to dev | 27/32 images + artist profiles + venue backgrounds + sponsor logos rsync'd from prod storage. 5 orphan records (files deleted from prod disk). Storage symlink verified. Mo will see real imagery in admin. |
| 2026-04-14 | Permissions confirmed: legacy role-based, functional | `Admin::hasPermission()` works via role→permissions lookup. `admin.permission:*` middleware enforces on routes. IAM domain has contracts only (5 files). PermissionOverride layer deferred (IAM-2.4 = last decomp task). super_admin has all 55 permissions. |
| 2026-04-14 | EGP, SAR, AED currencies added | Frontend dropdown + backend `config/payments.php` symbol map. No exchange — event currency = payment currency. Revolut handles multi-currency natively. TODO: multi-currency revenue aggregation on reporting pages (currently sums across currencies). |
| 2026-04-14 | Revolut sandbox configured, client ID missing | `REVOLUT_BASE_URL=sandbox-merchant.revolut.com`, API key + webhook secret set. `REVOLUT_CLIENT_ID` empty — locked out of dashboard, to resolve tomorrow. Comp bookings work without Revolut. |
| 2026-04-14 | Auto-fork confirmed working on event creation | Event 5 created via admin with venue_id=1 → `GalaManagementService::linkVenueTemplate()` auto-forked published template 191 → 1222 seats provisioned immediately. This is the happy path — no manual fork step needed when venue has a published template. |
