@@ -5,12 +5,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
55## Development Commands
66
77### Environment Setup
8+
89The project supports two development environments:
910
1011** Nix / devenv (primary)** — ` .envrc ` uses ` direnv ` + ` use flake ` . With direnv allowed, the
1112toolchain (PHP 8.3, Node) is provided automatically and a ` sail ` alias is exported.
1213
1314** Laravel Sail (Docker)** — the classic path:
15+
1416``` bash
1517# Initial setup
1618cp .env.example .env # or: mv .env.example .env
@@ -31,6 +33,7 @@ npm run build
3133> (and the ` DB_* ` host vars) if you run against the Sail database.
3234
3335### Development Commands
36+
3437``` bash
3538# Frontend development
3639npm run dev # Start Vite dev server with hot reload
@@ -54,6 +57,7 @@ npm run build # Build production assets
5457```
5558
5659### Event State Management
60+
5761``` bash
5862# Useful for development/testing - sets up an event in a given state.
5963# Accepted states: pre-order | order | event-order | closed (open = legacy alias)
@@ -62,6 +66,7 @@ php artisan event:state closed # Orders closed
6266```
6367
6468### Other Useful Artisan Commands
69+
6570``` bash
6671php artisan badges:print # Print all unprinted badges
6772php artisan badges:unprint # Revert badges to unprinted
@@ -79,6 +84,7 @@ php artisan tse:update-state # Fiskaly TSE state management (also t
7984## Architecture Overview
8085
8186### Core System
87+
8288This is a ** Laravel 11 + Inertia.js + Vue 3** application for managing fursuit badge
8389registration at the Eurofurence convention. It covers the full lifecycle from badge creation
8490through on-site pickup, with integrated payment processing (wallet + SumUp), German fiscal
@@ -93,6 +99,7 @@ determine when badges can be ordered. Event state is computed dynamically (see
9399
94100** Badge Lifecycle** : Badges use Spatie Model States with two parallel state machines
95101(in ` app/Models/Badge/ ` ):
102+
96103- Payment states (` State_Payment/ ` ): ` Unpaid ` → ` Paid `
97104- Fulfillment states (` State_Fulfillment/ ` ): ` Pending ` → ` Processing ` → ` ReadyForPickup ` → ` PickedUp `
98105 (a ` Printed ` state also exists; transitions live in the ` Transitions/ ` subfolders, and POS
@@ -109,6 +116,7 @@ wraps this with its own models, services, and states.
109116### Application Structure
110117
111118** Multi-Interface Design** :
119+
112120- ` / ` — Public fursuit badge registration interface (Vue/Inertia)
113121- ` /admin ` — Filament admin panel for staff
114122- ` /pos ` — Point-of-sale system for on-site operations (machine + staff PIN auth)
@@ -120,6 +128,7 @@ wraps this with its own models, services, and states.
120128cert/signing), ` catch-em-all.php ` , ` gallery.php ` , ` api.php ` , ` channels.php ` , ` console.php ` .
121129
122130** Key Directories** :
131+
123132- ` app/Models/Badge/ ` — Badge model with the two state machines
124133- ` app/Models/Fursuit/ ` — Fursuit management with approval workflow
125134- ` app/Models/FCEA/ ` — Catch-Em-All catch/log/ranking models
@@ -136,11 +145,13 @@ cert/signing), `catch-em-all.php`, `gallery.php`, `api.php`, `channels.php`, `co
136145 ` CatchEmAll/ ` , ` FCEA/ ` , ` Gallery/ ` , ` Statistics/ ` , ` Auth/ `
137146
138147### State Management Pattern
148+
139149The system heavily uses ** Spatie Model States** for complex entity lifecycles (Badges,
140150Fursuits, Checkouts). When working with these entities, always consider the current state and
141151the available transitions rather than mutating state properties directly.
142152
143153### Event-Driven Architecture
154+
144155- Badge creation triggers notifications
145156- State transitions are logged via ` spatie/laravel-activitylog `
146157- Background jobs handle printing, ranking updates, and receipt generation
@@ -149,44 +160,51 @@ the available transitions rather than mutating state properties directly.
149160 (every 15 min), and stuck-print-job checks (every 3 min)
150161
151162### Badge Generation System
163+
152164- Badges are rendered as PDFs using custom badge classes in ` app/Badges/ `
153165- Each badge type (e.g. ` EF28_Badge ` , ` EF29_Badge ` ) extends ` BadgeBase_V1 ` (in ` Bases/ ` ) and
154166 defines positioning/fonts; reusable field/layout helpers live in ` app/Badges/Components/ `
155167- PDF generation uses ` mpdf/mpdf ` ; images are processed with Intervention/Imagine and stored on S3
156168- QR codes are generated for the Catch-Em-All game integration
157169
158170### Printing System
171+
159172- On-site printing is driven through ** QZ Tray** (browser-to-printer bridge); the POS exposes
160173 certificate and signing endpoints (` pos-auth.php ` ) for QZ
161174- Print jobs and printer state are modeled in ` app/Domain/Printing/ ` with their own enums
162175 (` PrintJobStatusEnum ` , ` PrinterStatusEnum ` , etc.) and queued jobs (` PrintBadgeJob ` , ` BatchPrintJob ` )
163176- See ` PRINTING_SYSTEM.md ` and the ` PRINTING_SYSTEM_IMPROVEMENTS*.md ` docs for design notes
164177
165178### Fiscal Compliance (German market)
179+
166180- SumUp card readers handle on-site card payments (` SumUpReader ` model)
167181- A Fiskaly ** TSE** (Technical Security System) signs transactions; managed via the ` tse:* `
168182 commands. See ` TSE.md `
169183- DSFinV-K exports are produced by ` dsfin:generate-direct-export ` . See ` DSFinV_K_2_4.pdf `
170184
171185### Database Design Notes
186+
172187- Default connection is SQLite (` .env.example ` ); Sail provisions MySQL
173188- Events use computed state (no ` state ` column) based on date comparisons
174189- Badges have ` custom_id ` for human-readable identification
175190- Soft deletes are used throughout for audit trails
176191- Activity logging tracks all important changes
177192
178193### Testing Approach
194+
179195- Uses ** Pest PHP** testing framework (` tests/Feature ` , ` tests/Unit ` )
180196- Feature tests cover critical user journeys (badge flow, checkout, printing, notifications,
181197 event order state)
182198- Database factory patterns for test data creation
183199- Event state can be manipulated via the ` event:state ` command for testing
184200
185201### Observability
202+
186203- ** Sentry** (` sentry/sentry-laravel ` ) for error tracking
187204- ** Clockwork** (` itsgoingd/clockwork ` ) for local request profiling/debugging
188205
189206### Development Environment
207+
190208- ** Nix / devenv** (` .envrc ` + flake) or ** Laravel Sail** for Docker-based development
191209- ** Laravel Octane** (Swoole) is available as the production app server
192210- ** Laravel Reverb** provides WebSockets; the frontend uses ` laravel-echo `
@@ -196,30 +214,39 @@ the available transitions rather than mutating state properties directly.
196214
197215### Domain Gotchas
198216
199- ** Prepaid badges: "can create" vs. "free badges left" are two different things.** There are two
200- intentionally different prepaid calculations — do ** not** merge their values:
201- - ` App\Policies\BadgePolicy::create() ` uses ` prepaid_badges − ordered ` (raw allowance) to decide
202- whether a user may create a badge ** at all** (the badge may end up ** paid** ). A prepaid
203- allowance bypasses the closed order-window restriction.
204- - ` App\Models\User::getPrepaidBadgesLeft() ` subtracts an additional ` 1 ` after ` order_starts_at `
205- (the included free badge is "no longer honored" / becomes paid) and answers ** how many free
206- badges remain** ; it drives badge ** pricing** in ` BadgeController@store ` .
207-
208- Because of this, a user can have ` getPrepaidBadgesLeft() == 0 ` yet still be allowed to order an
209- additional ** paid** badge. The public Welcome page (` Welcome.vue ` ) therefore gates its
210- create/customize button on the authoritative ` canCreate ` (` Gate::allows('create', Badge::class) ` )
211- passed by ` WelcomeController ` , not on ` prepaidBadgesLeft ` . ` PrepaidBadgePriceConsistencyTest `
212- locks in that paid additional badges stay orderable once the free allowance is used.
213- See ` docs/bugfix-01-result.md ` and ` docs/handoff.md ` .
217+ ** Prepaid badges: "can create" vs. "free badges left" are two different things.** Two related
218+ prepaid calculations — do ** not** merge their values:
219+
220+ - ` App\Policies\BadgePolicy::create() ` uses ` prepaid_badges − ordered ` to decide whether a user
221+ may create a badge ** at all** (the badge may end up ** paid** ). A prepaid allowance bypasses the
222+ closed order-window restriction.
223+ - ` App\Models\User::getPrepaidBadgesLeft() ` = ` prepaid_badges − orderedMainBadges ` (only main
224+ badges count; spare copies — ` extra_copy_of != null ` — are always separately paid and never
225+ consume the allowance). It answers ** how many free badges remain** and drives badge ** pricing**
226+ in ` BadgeController@store ` .
227+
228+ The ** full** ` prepaid_badges ` entitlement is honored as free. (Until bugfix-03 this method also
229+ deducted an extra ` 1 ` after ` order_starts_at ` — "the included badge is no longer honored" — which
230+ wrongly ** charged** the user's last prepaid badge; that ` −1 ` is gone. See
231+ ` docs/bugfix-03-fix.md ` .)
232+
233+ A user with ` getPrepaidBadgesLeft() == 0 ` can still order an additional ** paid** badge while the
234+ order window is open. The public Welcome page (` Welcome.vue ` ) therefore gates its create/customize
235+ button on the authoritative ` canCreate ` (` Gate::allows('create', Badge::class) ` ) passed by
236+ ` WelcomeController ` , not on ` prepaidBadgesLeft ` . ` PrepaidBadgePriceConsistencyTest ` locks in the
237+ pricing; ` FreeBadgeRepairService ` (admin → Maintenance → DB Service) repairs already-wrongly-charged
238+ badges. See ` docs/bugfix-01-result.md ` , ` docs/bugfix-03-fix.md ` , and ` docs/handoff.md ` .
214239
215240### Migrations must be idempotent
241+
216242Migrations run as an ArgoCD PreSync ** Job** (` php artisan migrate --force ` ) against MySQL, and
217243** MySQL DDL is not transactional** — a migration that fails partway leaves its applied steps in
218244place but is never recorded, so the next run hits "Duplicate column / key / table" and blocks every
219245later migration (this caused a dev outage; see ` docs/bugfix-02-*.md ` ).
220246
221247Every migration must therefore be safe to re-run. Guard each operation with
222248` App\Support\Migrations\SchemaGuard ` :
249+
223250- ` Schema::create('t', …) ` → wrap in ` if (SchemaGuard::missingTable('t')) { … } ` (and use
224251 ` Schema::dropIfExists ` in ` down() ` ).
225252- add/drop column → ` SchemaGuard::missingColumn(...) ` / ` SchemaGuard::hasColumn(...) ` .
@@ -232,6 +259,7 @@ Every migration must therefore be safe to re-run. Guard each operation with
232259` tests/Feature/MigrationIdempotencyTest.php ` locks in the helper's behaviour.
233260
234261### Additional Documentation
262+
235263The repo root contains focused design docs worth consulting: ` CATCH.md ` (Catch-Em-All game),
236264` PRINTING_SYSTEM*.md ` (printing), ` TSE.md ` + ` zebra.md ` (fiscal/printer hardware),
237265` openapi.yml ` (API spec), and ` README.md ` (setup). Bugfix write-ups live in ` docs/ ` .
0 commit comments