# CLAUDE.md — ShowPrima Backend (DDD Decomposition)

**Branch**: `feature/decomp-sprint`
**What this is**: Near-total DDD refactor of the ShowPrima event ticketing Laravel monolith.
**Status**: See `STATE_OF_PLAY.md` for full assessment.

---

## Quick Orientation

This is a **worktree** of the main ShowPrima repo at `/Users/charlie/code/showprima/`. The domain decomposition lives here. Production (`main`) is still the monolith — this branch merges there when ready.

```
main (production monolith, no domain code)
  └── feature/decomp-sprint (this branch, 133 commits ahead, all DDD code)
```

There is no `architecture-v2` branch. This IS the new architecture.

### Repository Map

| Directory | Purpose |
|-----------|---------|
| `showprima/` | Production backend (main branch) |
| `showprima-user-account-decomp/` | **This repo** — DDD decomposition branch |
| `showprima-frontend/` | Next.js frontend monorepo |

---

## Architecture

7 bounded domains at `app/Domains/`:

| Domain | Files | What it owns |
|--------|-------|-------------|
| **Customer** (46) | Account management, profiles, order history, email management |
| **Gala** (69) | Events, seats, pricing tiers, artists, state machine, availability |
| **Ordering** (62) | Orders, seat reservations, payments, refunds, audit logs |
| **Notifications** (49) | Email templates, newsletter campaigns, delivery tracking |
| **Venue** (19) | Venue templates, seat layouts, template forking |
| **IAM** (5) | Authorization contracts (foundation only) |
| **Shared** (4) | User model, value objects (EmailAddress, PhoneNumber) |

**Legacy code still exists alongside** at `app/Model/` (135 files), `app/Services/` (94 files), `app/Http/Controllers/` (135 files). Most legacy controllers now call domain services. ~119 legacy models have no domain equivalent yet (tickets, payments, CMS, coupons, chargebacks, etc.).

### Domain Rules

- Domains communicate via **contracts** (interfaces) at `app/Domains/*/Contracts/`
- Cross-domain references must be **read-only** — no mutations across boundaries
- Each domain has a **ServiceProvider** registered in `config/app.php`
- Legacy `App\Model\*` alias classes extend domain models for backward compatibility

---

## Commands

### PHP Version

**Use PHP 8.4** for all CLI work. PHP 8.5 floods output with deprecation warnings that crash the test runner.

```bash
/opt/homebrew/opt/php@8.4/bin/php vendor/bin/phpunit          # Full suite
/opt/homebrew/opt/php@8.4/bin/php vendor/bin/phpunit tests/Unit/Domains/  # Domain tests only
/opt/homebrew/opt/php@8.4/bin/php artisan <command>            # Artisan
```

### Testing

```bash
# Domain tests (priority — these prove the DDD architecture works)
/opt/homebrew/opt/php@8.4/bin/php vendor/bin/phpunit tests/Unit/Domains/
/opt/homebrew/opt/php@8.4/bin/php vendor/bin/phpunit tests/Feature/Domains/

# Full suite (has known failures in legacy tests)
/opt/homebrew/opt/php@8.4/bin/php vendor/bin/phpunit

# Specific domain
/opt/homebrew/opt/php@8.4/bin/php vendor/bin/phpunit tests/Unit/Domains/Customer/
/opt/homebrew/opt/php@8.4/bin/php vendor/bin/phpunit tests/Unit/Domains/Gala/

# Gala suite (configured in phpunit.xml)
/opt/homebrew/opt/php@8.4/bin/php vendor/bin/phpunit --testsuite=Gala
```

### Database

Test database runs on MySQL 8.0 in Docker via OrbStack:
- **Container**: `showprima_mysql`
- **Database**: `showprima_test`
- **User**: `globalgala` / `globalgala_dev_pass`
- **Host**: `127.0.0.1:3306`

```bash
# Check MySQL is up
mysql -u globalgala -pglobalgala_dev_pass -h 127.0.0.1 -e "SELECT 1" showprima_test

# If OrbStack/Docker is down
open -a OrbStack
```

Schema is loaded from `database/schema/mysql-schema.sql` (104 tables) plus migrations in `database/migrations/`.

### Database Safety

```bash
./scripts/backup-db.sh                    # Backup before changes
php artisan migrate:safe                  # Safe migration (backs up first)
./scripts/restore-db.sh <backup_file>     # Restore from backup
```

**Never** run `migrate:fresh` or `db:wipe` without explicit user confirmation.

---

## Known Issues

### Test Suite (656E / 366F as of 2026-03-09 — stale; current count higher)

Most failures cascade from a family of **fixture-vs-schema mismatches** where tests insert rows that don't satisfy NOT NULL columns the schema requires:

1. **`is_admin` attribute**: 6 test files pass `'is_admin' => true` to `User::factory()->create()` — the column doesn't exist on `users` table.
2. **`role` attribute on Admin**: 3 test files pass `'role' => 'super_admin'` to `Admin::factory()->create()` — the column is `role_id` (FK), not `role`.
3. **`scanner_name` on ScannerDevice**: `tests/Unit/Controllers/SplitPaymentControllerTest.php:51` calls `ScannerDevice::create([...])` without `scanner_name` — the column has no default value.

These cascade because any test creating a User / Admin / ScannerDevice record hits a SQL error, and downstream tests that expect those records also fail.

Other known-broken legacy tests (not fixture-family):

- **`tests/Feature/API/AuthRefreshTest::test_refresh_with_valid_token`**: response array missing `token` key — auth refresh response shape drift.

The pre-push hook (`pre-push` git hook) runs the full suite and currently blocks pushes on these legacy failures. Use `git push --no-verify` when shipping work that doesn't touch the failing files; investigate and fix during a dedicated legacy-debt session, or add specific tests to an ignore list in the hook config.

### Other Known Issues

- **Picqer barcode v2/v3**: Production code uses v3 API (`TypeCode128::class`) but `composer.lock` has v2.4.2. Causes 11 test failures. Production code bug — not fixable by test changes.
- **Notification test methods moved**: `resolveChannels()`, `getChannelInstances()`, `sendViaChannels()` moved from `NotificationService` to `NotificationDispatchService` during refactor.
- **Customer domain phantom failures**: Pass individually, fail in full suite from test ordering pollution.

---

## Development Patterns

### Factory Resolution for Domain Models

Domain models need `newFactory()` overrides because Laravel expects factories at `Database\Factories\App\Domains\*` which doesn't exist:

```php
// app/Domains/Ordering/Models/Order.php
protected static function newFactory()
{
    return \Database\Factories\App\Model\OrderFactory::new();
}
```

Models WITH overrides: User, Order, Gala, Seat, Artist, PriceTier, CustomerProfile, SeatReservation, PaymentTransaction, OrderRefund, EmailLog.

### Cross-Domain Access

```php
// Correct: use contract interface
use App\Domains\Gala\Contracts\GalaReadInterface;
$gala = app(GalaReadInterface::class)->find($id);

// Wrong: direct model import across domains
use App\Domains\Gala\Models\Gala;
$gala = Gala::find($id);
```

### Error Handling

- 409 for seat conflicts
- 422 for validation errors
- 404 for not found
- Use structured logging with correlation IDs

---

## Git Workflow

- **Never push directly to `main`** unless explicitly instructed
- This branch is `feature/decomp-sprint` — commit here
- Use conventional commits: `feat:`, `fix:`, `test:`, `refactor:`, `docs:`
- Always backup before migrations

---

## File Locations

| What | Where |
|------|-------|
| Domain code | `app/Domains/{Customer,Gala,Ordering,Notifications,Venue,IAM,Shared}/` |
| Legacy models | `app/Model/` |
| Legacy services | `app/Services/` |
| Controllers | `app/Http/Controllers/API/` (legacy), `app/Domains/Customer/Controllers/` (domain) |
| Factories | `database/factories/` and `database/factories/App/Model/` |
| Schema dump | `database/schema/mysql-schema.sql` |
| Domain tests | `tests/Unit/Domains/`, `tests/Feature/Domains/` |
| Legacy tests | `tests/Unit/Services/`, `tests/Feature/` |
| Scripts | `scripts/` |
| Project status | `STATE_OF_PLAY.md` |

---

## Deployment (Production)

Production deployment targets `/Users/charlie/code/showprima/` on `main` branch — NOT this decomp branch.

```bash
ssh globalgalashow@glo.globalgala.com
cd ~/public_html/dev
./scripts/deploy-dev.sh
```

The deploy script handles: backup, pull, composer install, cache clear, migrations, queue restart, health check.

For details see `scripts/deploy-dev.sh` and the deployment atom/molecule/organism libraries in `scripts/`.

---

## Email System

Queue-based email delivery. Queue worker MUST be running for emails to send.

```bash
./scripts/start-queue-worker.sh     # Start worker
./scripts/queue-monitor.sh          # Monitor
php artisan email:check             # Health check
```

All Mailables implement `ShouldQueue`. Use `Mail::to()->queue()`, never `Mail::to()->send()`.
Dev emails captured by Mailpit at `http://localhost:8025`.

---

## Ticket PDF storage

Two distinct PDF artefacts with different retention rules. Don't conflate them.

| Path | Purpose | Lifetime |
|------|---------|----------|
| `storage/app/tickets/order_{id}_{timestamp}.pdf` | **Canonical PDF.** Source of truth for re-sends, customer download, admin preview, support requests. Attached to confirmation/comp emails. | **Permanent.** Never auto-deleted. |
| `storage/app/public/downloads/tickets_{id}_{rand}.pdf` | **Temp public download copy.** Created when admin uses the "generate download URL" path in `TicketRegenerationController` to share a public link. | **1 hour.** Deleted by `CleanupTempDownload` job (see TG-6). |

**Disk planning**: the canonical `tickets` disk grows monotonically — expect ~50–500KB per order. Plan capacity accordingly. The `public/downloads` directory should stay near-empty in steady state; if it doesn't, the cleanup job is failing and queue health needs investigation.

**Never** delete from `storage/app/tickets/` as part of cleanup, retention, or housekeeping work. Treat it like immutable customer records.
