feat(events): rsvp enrollment end-to-end#263
Conversation
📝 WalkthroughWalkthroughThis PR implements the Events bounded context MVP, delivering the RSVP enrollment slice end-to-end. It adds database schema (8 tables), Eloquent models with relationships, state-machine enums with transition validation, an Possibly related issues
Possibly related PRs
Suggested reviewers
✨ Finishing Touches⚔️ Resolve merge conflicts
|
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app-modules/events/CONTEXT.md`:
- Line 26: The fenced code block in CONTEXT.md that begins with the line
containing "[entry]" lacks a language token (causing markdownlint MD040); update
that opening triple-backtick fence to include a language identifier (e.g.,
"text") so the block reads ```text before the "[entry]" line to satisfy the
linter while leaving the block content unchanged.
In
`@app-modules/events/database/migrations/2026_05_16_195205_drop_events_module_tables.php`:
- Around line 21-24: The migration's down() method currently no-ops; replace it
with a loud failure by throwing an exception to prevent silent successful
rollbacks — inside the down() method of this migration class, remove the comment
and throw a RuntimeException (or
Illuminate\Contracts\Filesystem\FileNotFoundException if preferred) with a clear
message like "Irreversible migration: cannot recreate dropped events module
tables" so any attempt to rollback fails loudly and documents irreversibility;
keep the method signature public function down(): void and ensure the exception
type is imported or fully-qualified.
In `@app-modules/events/docs/adr/0001-state-machine-as-enum-not-package.md`:
- Line 13: The ADR text names the enum `EnrollmentStatusEnum` but the code
defines `EnrollmentStatus`; update the ADR (or rename the enum type) so the
terminology matches: either change all ADR references from
`EnrollmentStatusEnum` to `EnrollmentStatus`, or rename the enum type in code to
`EnrollmentStatusEnum` and update usages like canTransitionTo to match; ensure
references to the Action classes and the canTransitionTo(self $target): bool
method use the exact enum identifier `EnrollmentStatus` (or the chosen
replacement) consistently.
In `@app-modules/events/src/Enrollment/Actions/EnrollUserAction.php`:
- Around line 25-47: The existence check using
Enrollment::query()->where(...)->exists() is not concurrency-safe; wrap the
persistence inside DB::transaction and catch database unique-constraint
violations thrown by Enrollment::save() (or the underlying query) and rethrow
EnrollmentException::alreadyEnrolled() so parallel RSVP requests map DB
unique-constraint errors to the domain error; specifically modify the
EnrollUserAction transaction block around the new Enrollment creation/save to
rescue the DB unique-constraint exception type your DB layer throws and throw
EnrollmentException::alreadyEnrolled() instead, leaving other exceptions to
bubble up.
In `@app-modules/events/src/Event/Models/Event.php`:
- Around line 90-93: totalDays() currently calls startOfDay() directly on the
Carbon properties and mutates the model timestamps; update totalDays() to
operate on copies by calling copy() on both $this->starts_at and $this->ends_at
before startOfDay(), e.g. use $this->starts_at->copy()->startOfDay() and
$this->ends_at->copy()->startOfDay() so the original starts_at/ends_at are not
mutated while preserving the same diffInDays + 1 calculation.
In `@app-modules/events/tests/Feature/EnrollUserActionTest.php`:
- Around line 112-119: The test currently only creates one enrollment so it
doesn't exercise duplicate handling; update the test to call
resolve(EnrollUserAction::class)->handle($event, $user) a second time and assert
that it throws the expected EnrollmentException, then keep the existing
Enrollment::query()->where('event_id', $event->id)->where('user_id',
$user->id)->count() assertion to remain 1; this targets the duplicate-case
behavior in EnrollUserAction::handle and validates the Enrollment model still
contains only a single record.
In `@app-modules/panel-app/resources/views/livewire/events/events-list.blade.php`:
- Line 24: Replace the hardcoded $event->starts_at->format('d/m/Y H:i') with a
locale-aware Carbon formatter (e.g. $event->starts_at->isoFormat('L LT')) so
dates render per the user's locale; update the view expression where
$event->starts_at->format(...) appears and ensure your app/Carbon locale is
configured elsewhere so isoFormat uses the correct locale.
In
`@app-modules/panel-app/resources/views/livewire/events/my-events-list.blade.php`:
- Line 18: The hard-coded format('d/m/Y H:i') is not locale-aware; replace
usages of $enrollment->event->starts_at->format(...) (and the other timestamp at
the similar location) with Carbon's locale-aware formatting, e.g. call
->locale(app()->getLocale())->isoFormat('L LT') (or ->translatedFormat(...) if
you prefer a custom pattern) so the displayed date/time follows the user's
locale; update both occurrences (the starts_at formatting and the other
enrollment timestamp) to use this locale-aware method.
In `@app-modules/panel-app/src/Livewire/Events/MyEventsList.php`:
- Around line 9-23: The whereHas callback in
MyEventsList::getEnrollmentsProperty is type-hinted with
Illuminate\Contracts\Database\Query\Builder but whereHas supplies an
Illuminate\Database\Eloquent\Builder; update the import/use so the callback uses
Illuminate\Database\Eloquent\Builder (or remove the incorrect contract import
and reference Eloquent\Builder) to avoid the runtime TypeError when invoking
whereHas.
In `@CONTEXT-MAP.md`:
- Line 18: The fenced ASCII diagram block (the triple-backtick fenced code block
containing the context map diagram) is missing a language tag and triggers
markdownlint MD040; update the opening fence from ``` to include a language tag
such as ```text (i.e., add "text" after the opening backticks) so the diagram
block is explicitly marked as plain text while leaving the closing ```
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 06d97f9a-bb8c-407f-a5a8-a38c7a4567fb
📒 Files selected for processing (79)
CONTEXT-MAP.mdapp-modules/events/CONTEXT.mdapp-modules/events/database/factories/CheckInCodeFactory.phpapp-modules/events/database/factories/CheckInFactory.phpapp-modules/events/database/factories/EnrollmentFactory.phpapp-modules/events/database/factories/EnrollmentPolicyFactory.phpapp-modules/events/database/factories/EnrollmentTransitionFactory.phpapp-modules/events/database/factories/EventAgendaFactory.phpapp-modules/events/database/factories/EventFactory.phpapp-modules/events/database/factories/EventSubmissionFactory.phpapp-modules/events/database/factories/EventSubmissionSpeakerFactory.phpapp-modules/events/database/factories/QrTokenFactory.phpapp-modules/events/database/factories/SponsorFactory.phpapp-modules/events/database/migrations/2026_05_16_195205_drop_events_module_tables.phpapp-modules/events/database/migrations/2026_05_16_200001_create_events_table.phpapp-modules/events/database/migrations/2026_05_16_200002_create_events_enrollment_policies_table.phpapp-modules/events/database/migrations/2026_05_16_200003_create_events_enrollments_table.phpapp-modules/events/database/migrations/2026_05_16_200004_create_events_enrollment_transitions_table.phpapp-modules/events/database/migrations/2026_05_16_200005_create_events_check_ins_table.phpapp-modules/events/database/migrations/2026_05_16_200006_create_events_check_in_codes_table.phpapp-modules/events/database/migrations/2026_05_16_200007_create_events_qr_tokens_table.phpapp-modules/events/docs/adr/0001-state-machine-as-enum-not-package.mdapp-modules/events/docs/adr/0002-xp-via-domain-events-not-ledger.mdapp-modules/events/docs/adr/0003-no-database-triggers-all-logic-in-actions.mdapp-modules/events/docs/adr/0004-check-in-as-separate-table-one-to-many.mdapp-modules/events/docs/adr/0005-bot-communicates-via-domain-events-not-api.mdapp-modules/events/lang/en/enums.phpapp-modules/events/lang/en/exceptions.phpapp-modules/events/lang/en/pages.phpapp-modules/events/lang/pt_BR/enums.phpapp-modules/events/lang/pt_BR/exceptions.phpapp-modules/events/lang/pt_BR/pages.phpapp-modules/events/src/CheckIn/Enums/CheckInMethod.phpapp-modules/events/src/CheckIn/Models/CheckIn.phpapp-modules/events/src/CheckIn/Models/CheckInCode.phpapp-modules/events/src/CheckIn/Models/QrToken.phpapp-modules/events/src/Enrollment/Actions/EnrollUserAction.phpapp-modules/events/src/Enrollment/Enums/AttendanceRequirement.phpapp-modules/events/src/Enrollment/Enums/EnrollmentMethod.phpapp-modules/events/src/Enrollment/Enums/EnrollmentStatus.phpapp-modules/events/src/Enrollment/Enums/TriggeredBy.phpapp-modules/events/src/Enrollment/Events/EnrollmentConfirmed.phpapp-modules/events/src/Enrollment/Exceptions/EnrollmentException.phpapp-modules/events/src/Enrollment/Models/Enrollment.phpapp-modules/events/src/Enrollment/Models/EnrollmentPolicy.phpapp-modules/events/src/Enrollment/Models/EnrollmentTransition.phpapp-modules/events/src/Event/Enums/EventStatus.phpapp-modules/events/src/Event/Enums/EventType.phpapp-modules/events/src/Event/Models/Event.phpapp-modules/events/src/EventsServiceProvider.phpapp-modules/events/tests/Feature/EnrollUserActionTest.phpapp-modules/events/tests/Feature/EventFactoriesTest.phpapp-modules/events/tests/Feature/EventResourceTest.phpapp-modules/events/tests/Unit/EnrollmentStatusTest.phpapp-modules/events/tests/Unit/EventStatusTest.phpapp-modules/panel-admin/src/Filament/Resources/Events/EventResource.phpapp-modules/panel-admin/src/Filament/Resources/Events/Pages/CreateEvent.phpapp-modules/panel-admin/src/Filament/Resources/Events/Pages/EditEvent.phpapp-modules/panel-admin/src/Filament/Resources/Events/Pages/ListEvents.phpapp-modules/panel-admin/src/Filament/Resources/Events/RelationManagers/EnrollmentsRelationManager.phpapp-modules/panel-admin/src/Filament/Resources/Events/Schemas/EventForm.phpapp-modules/panel-admin/src/Filament/Resources/Events/Schemas/EventInfolist.phpapp-modules/panel-admin/src/Filament/Resources/Events/Tables/EventsTable.phpapp-modules/panel-admin/src/PanelAdminServiceProvider.phpapp-modules/panel-app/resources/views/livewire/events/event-detail.blade.phpapp-modules/panel-app/resources/views/livewire/events/events-list.blade.phpapp-modules/panel-app/resources/views/livewire/events/my-events-list.blade.phpapp-modules/panel-app/resources/views/pages/event.blade.phpapp-modules/panel-app/resources/views/pages/events.blade.phpapp-modules/panel-app/resources/views/pages/my-events.blade.phpapp-modules/panel-app/src/Livewire/Events/EventDetail.phpapp-modules/panel-app/src/Livewire/Events/EventsList.phpapp-modules/panel-app/src/Livewire/Events/MyEventsList.phpapp-modules/panel-app/src/Pages/EventPage.phpapp-modules/panel-app/src/Pages/EventsPage.phpapp-modules/panel-app/src/Pages/MyEventsPage.phpapp-modules/panel-app/src/PanelAppServiceProvider.phpapp-modules/panel-app/tests/Feature/Events/RsvpEnrollmentTest.phpapp/Providers/Filament/AppPanelProvider.php
💤 Files with no reviewable changes (4)
- app-modules/events/database/factories/EventAgendaFactory.php
- app-modules/events/database/factories/EventSubmissionSpeakerFactory.php
- app-modules/events/database/factories/EventSubmissionFactory.php
- app-modules/events/database/factories/SponsorFactory.php
|
|
||
| ## State Machine — Enrollment | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Specify a language for the state-machine fenced block.
Line 26 should include a language token to satisfy markdownlint MD040.
Suggested patch
-```
+```text
[entry]
...
-```
+```📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 26-26: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/events/CONTEXT.md` at line 26, The fenced code block in
CONTEXT.md that begins with the line containing "[entry]" lacks a language token
(causing markdownlint MD040); update that opening triple-backtick fence to
include a language identifier (e.g., "text") so the block reads ```text before
the "[entry]" line to satisfy the linter while leaving the block content
unchanged.
| public function down(): void | ||
| { | ||
| // Irreversible: removes legacy events module tables replaced by the new schema. | ||
| } |
There was a problem hiding this comment.
Do not silently no-op in down() for a destructive migration.
Line 21–24 currently reports success on rollback without restoring dropped tables, which can produce inconsistent rollback state. If true reversal isn’t feasible, fail loudly.
Suggested patch
public function down(): void
{
- // Irreversible: removes legacy events module tables replaced by the new schema.
+ throw new RuntimeException(
+ 'Irreversible migration: legacy events tables were dropped and cannot be recreated automatically.'
+ );
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app-modules/events/database/migrations/2026_05_16_195205_drop_events_module_tables.php`
around lines 21 - 24, The migration's down() method currently no-ops; replace it
with a loud failure by throwing an exception to prevent silent successful
rollbacks — inside the down() method of this migration class, remove the comment
and throw a RuntimeException (or
Illuminate\Contracts\Filesystem\FileNotFoundException if preferred) with a clear
message like "Irreversible migration: cannot recreate dropped events module
tables" so any attempt to rollback fails loudly and documents irreversibility;
keep the method signature public function down(): void and ensure the exception
type is imported or fully-qualified.
|
|
||
| ## Decision | ||
|
|
||
| Use a backed PHP enum (`EnrollmentStatusEnum`) with an explicit `canTransitionTo(self $target): bool` method. Transition side-effects (XP dispatch, waitlist promotion, audit trail) live in Action classes. |
There was a problem hiding this comment.
Align enum name with the implemented type.
Line 13 references EnrollmentStatusEnum, but the module’s enum appears to be EnrollmentStatus. Keep ADR terminology consistent with code to avoid drift.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/events/docs/adr/0001-state-machine-as-enum-not-package.md` at
line 13, The ADR text names the enum `EnrollmentStatusEnum` but the code defines
`EnrollmentStatus`; update the ADR (or rename the enum type) so the terminology
matches: either change all ADR references from `EnrollmentStatusEnum` to
`EnrollmentStatus`, or rename the enum type in code to `EnrollmentStatusEnum`
and update usages like canTransitionTo to match; ensure references to the Action
classes and the canTransitionTo(self $target): bool method use the exact enum
identifier `EnrollmentStatus` (or the chosen replacement) consistently.
| return DB::transaction(function () use ($event, $user): Enrollment { | ||
| throw_if( | ||
| Enrollment::query() | ||
| ->where('event_id', $event->id) | ||
| ->where('user_id', $user->id) | ||
| ->exists(), | ||
| EnrollmentException::alreadyEnrolled(), | ||
| ); | ||
|
|
||
| $event->loadMissing('enrollmentPolicy'); | ||
| $policy = $event->enrollmentPolicy; | ||
|
|
||
| $now = now(); | ||
|
|
||
| $enrollment = new Enrollment([ | ||
| 'event_id' => $event->id, | ||
| 'user_id' => $user->id, | ||
| 'enrolled_at' => $now, | ||
| 'confirmed_at' => $now, | ||
| ]); | ||
| $enrollment->status = EnrollmentStatus::Confirmed; | ||
| $enrollment->save(); | ||
|
|
There was a problem hiding this comment.
Handle unique-constraint races and map them to alreadyEnrolled().
exists() checks are not concurrency-safe by themselves. Under parallel RSVP requests, one write can hit the DB unique constraint and currently bubble up as an unhandled DB exception instead of a domain error.
Proposed fix
use Illuminate\Support\Facades\DB;
+use Illuminate\Database\QueryException;
final readonly class EnrollUserAction
{
public function handle(Event $event, User $user): Enrollment
{
$this->validate($event, $user);
- return DB::transaction(function () use ($event, $user): Enrollment {
+ try {
+ return DB::transaction(function () use ($event, $user): Enrollment {
throw_if(
Enrollment::query()
->where('event_id', $event->id)
->where('user_id', $user->id)
->exists(),
EnrollmentException::alreadyEnrolled(),
);
@@
- return $enrollment->fresh(['event.enrollmentPolicy']);
- });
+ return $enrollment->fresh(['event.enrollmentPolicy']);
+ });
+ } catch (QueryException $e) {
+ // map unique-constraint violation to domain exception
+ throw EnrollmentException::alreadyEnrolled();
+ }
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/events/src/Enrollment/Actions/EnrollUserAction.php` around lines
25 - 47, The existence check using Enrollment::query()->where(...)->exists() is
not concurrency-safe; wrap the persistence inside DB::transaction and catch
database unique-constraint violations thrown by Enrollment::save() (or the
underlying query) and rethrow EnrollmentException::alreadyEnrolled() so parallel
RSVP requests map DB unique-constraint errors to the domain error; specifically
modify the EnrollUserAction transaction block around the new Enrollment
creation/save to rescue the DB unique-constraint exception type your DB layer
throws and throw EnrollmentException::alreadyEnrolled() instead, leaving other
exceptions to bubble up.
| public function totalDays(): int | ||
| { | ||
| return (int) $this->starts_at->startOfDay()->diffInDays($this->ends_at->startOfDay()) + 1; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -path "*app-modules/events/src/Event/Models/Event.php" -type fRepository: he4rt/heartdevs.com
Length of output: 111
🌐 Web query:
Carbon startOfDay() mutation behavior PHP
💡 Result:
In the Carbon PHP library, the startOfDay method is mutable [1][2]. This means that when you call startOfDay on a Carbon instance, it modifies the original object in place and returns the same instance [1][2]. If you wish to obtain the start of the day without changing the original object, you must explicitly clone it using the copy method before applying the modifier [3][4]: $date = Carbon::now; $start = $date->copy->startOfDay; // $date remains unchanged [3][4] Alternatively, you can use CarbonImmutable if your application requires immutable date objects by default, where such modifiers will return a new instance instead of modifying the existing one [4].
Citations:
- 1: https://carbon.nesbot.com/guide/advanced-features/modifiers.html
- 2: startOfDay and endOf* are mutable, could it be immutable? briannesbitt/Carbon#560
- 3: https://stackoverflow.com/questions/45825163/how-to-check-if-a-carbon-date-object-is-at-the-start-of-day
- 4: https://stackoverflow.com/questions/42406461/how-to-get-start-and-or-end-of-year-of-a-carbon-instance-without-modifying-it-in
🏁 Script executed:
cat -n ./app-modules/events/src/Event/Models/Event.php | sed -n '85,100p'Repository: he4rt/heartdevs.com
Length of output: 579
🏁 Script executed:
# Also check if there are other similar patterns in the file
rg "startOfDay\(\)" ./app-modules/events/src/Event/Models/Event.php -nRepository: he4rt/heartdevs.com
Length of output: 165
Use copy() to avoid mutating starts_at/ends_at in totalDays().
Carbon's startOfDay() mutates the original instance. Calling totalDays() modifies the model's in-memory timestamps, breaking downstream logic that relies on the original time-of-day values.
Fix
public function totalDays(): int
{
- return (int) $this->starts_at->startOfDay()->diffInDays($this->ends_at->startOfDay()) + 1;
+ $start = $this->starts_at->copy()->startOfDay();
+ $end = $this->ends_at->copy()->startOfDay();
+
+ return (int) $start->diffInDays($end) + 1;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public function totalDays(): int | |
| { | |
| return (int) $this->starts_at->startOfDay()->diffInDays($this->ends_at->startOfDay()) + 1; | |
| } | |
| public function totalDays(): int | |
| { | |
| $start = $this->starts_at->copy()->startOfDay(); | |
| $end = $this->ends_at->copy()->startOfDay(); | |
| return (int) $start->diffInDays($end) + 1; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/events/src/Event/Models/Event.php` around lines 90 - 93,
totalDays() currently calls startOfDay() directly on the Carbon properties and
mutates the model timestamps; update totalDays() to operate on copies by calling
copy() on both $this->starts_at and $this->ends_at before startOfDay(), e.g. use
$this->starts_at->copy()->startOfDay() and $this->ends_at->copy()->startOfDay()
so the original starts_at/ends_at are not mutated while preserving the same
diffInDays + 1 calculation.
| test('when duplicate enrollment exists in database, then only one enrollment record is kept', function (): void { | ||
| $user = User::factory()->create(); | ||
| $tenant = Tenant::factory()->create(); | ||
| $event = createRsvpEvent($tenant); | ||
|
|
||
| resolve(EnrollUserAction::class)->handle($event, $user); | ||
|
|
||
| expect(Enrollment::query()->where('event_id', $event->id)->where('user_id', $user->id)->count())->toBe(1); |
There was a problem hiding this comment.
Test intent and assertions are mismatched for duplicate handling.
Line 117 creates only one enrollment, so this test does not actually validate duplicate behavior despite the test name. Add a second handle() call (expecting EnrollmentException) and keep the count assertion at 1.
Suggested adjustment
test('when duplicate enrollment exists in database, then only one enrollment record is kept', function (): void {
$user = User::factory()->create();
$tenant = Tenant::factory()->create();
$event = createRsvpEvent($tenant);
resolve(EnrollUserAction::class)->handle($event, $user);
+ expect(fn () => resolve(EnrollUserAction::class)->handle($event, $user))
+ ->toThrow(EnrollmentException::class);
expect(Enrollment::query()->where('event_id', $event->id)->where('user_id', $user->id)->count())->toBe(1);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test('when duplicate enrollment exists in database, then only one enrollment record is kept', function (): void { | |
| $user = User::factory()->create(); | |
| $tenant = Tenant::factory()->create(); | |
| $event = createRsvpEvent($tenant); | |
| resolve(EnrollUserAction::class)->handle($event, $user); | |
| expect(Enrollment::query()->where('event_id', $event->id)->where('user_id', $user->id)->count())->toBe(1); | |
| test('when duplicate enrollment exists in database, then only one enrollment record is kept', function (): void { | |
| $user = User::factory()->create(); | |
| $tenant = Tenant::factory()->create(); | |
| $event = createRsvpEvent($tenant); | |
| resolve(EnrollUserAction::class)->handle($event, $user); | |
| expect(fn () => resolve(EnrollUserAction::class)->handle($event, $user)) | |
| ->toThrow(EnrollmentException::class); | |
| expect(Enrollment::query()->where('event_id', $event->id)->where('user_id', $user->id)->count())->toBe(1); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/events/tests/Feature/EnrollUserActionTest.php` around lines 112 -
119, The test currently only creates one enrollment so it doesn't exercise
duplicate handling; update the test to call
resolve(EnrollUserAction::class)->handle($event, $user) a second time and assert
that it throws the expected EnrollmentException, then keep the existing
Enrollment::query()->where('event_id', $event->id)->where('user_id',
$user->id)->count() assertion to remain 1; this targets the duplicate-case
behavior in EnrollUserAction::handle and validates the Enrollment model still
contains only a single record.
| <div | ||
| class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 dark:text-gray-400" | ||
| > | ||
| <span>{{ $event->starts_at->format('d/m/Y H:i') }}</span> |
There was a problem hiding this comment.
Use locale-aware datetime formatting instead of a hardcoded format.
format('d/m/Y H:i') forces one regional format and bypasses localization; this will render incorrectly for non-BR locales.
Suggested change
- <span>{{ $event->starts_at->format('d/m/Y H:i') }}</span>
+ <span>{{ $event->starts_at->locale(app()->getLocale())->isoFormat('L LT') }}</span>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span>{{ $event->starts_at->format('d/m/Y H:i') }}</span> | |
| <span>{{ $event->starts_at->locale(app()->getLocale())->isoFormat('L LT') }}</span> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/panel-app/resources/views/livewire/events/events-list.blade.php`
at line 24, Replace the hardcoded $event->starts_at->format('d/m/Y H:i') with a
locale-aware Carbon formatter (e.g. $event->starts_at->isoFormat('L LT')) so
dates render per the user's locale; update the view expression where
$event->starts_at->format(...) appears and ensure your app/Carbon locale is
configured elsewhere so isoFormat uses the correct locale.
| <div | ||
| class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 dark:text-gray-400" | ||
| > | ||
| <span>{{ $enrollment->event->starts_at->format('d/m/Y H:i') }}</span> |
There was a problem hiding this comment.
Localize date rendering for event and enrollment timestamps.
Both timestamps use format('d/m/Y H:i'), which is not locale-aware and can produce the wrong format for users in other locales.
Suggested change
- <span>{{ $enrollment->event->starts_at->format('d/m/Y H:i') }}</span>
+ <span>{{ $enrollment->event->starts_at->locale(app()->getLocale())->isoFormat('L LT') }}</span>
- 'date' => $enrollment->enrolled_at->format('d/m/Y H:i'),
+ 'date' => $enrollment->enrolled_at->locale(app()->getLocale())->isoFormat('L LT'),Also applies to: 23-23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app-modules/panel-app/resources/views/livewire/events/my-events-list.blade.php`
at line 18, The hard-coded format('d/m/Y H:i') is not locale-aware; replace
usages of $enrollment->event->starts_at->format(...) (and the other timestamp at
the similar location) with Carbon's locale-aware formatting, e.g. call
->locale(app()->getLocale())->isoFormat('L LT') (or ->translatedFormat(...) if
you prefer a custom pattern) so the displayed date/time follows the user's
locale; update both occurrences (the starts_at formatting and the other
enrollment timestamp) to use this locale-aware method.
| use Illuminate\Contracts\Database\Query\Builder; | ||
| use Illuminate\Contracts\View\View; | ||
| use Illuminate\Database\Eloquent\Collection; | ||
| use Livewire\Component; | ||
|
|
||
| final class MyEventsList extends Component | ||
| { | ||
| /** @return Collection<int, Enrollment> */ | ||
| public function getEnrollmentsProperty(): Collection | ||
| { | ||
| return Enrollment::query() | ||
| ->with(['event']) | ||
| ->where('user_id', auth()->id()) | ||
| ->whereHas('event', fn (Builder $query) => $query->where('tenant_id', filament()->getTenant()->getKey())) | ||
| ->latest('enrolled_at') |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Laravel Eloquent (latest 12.x docs/source), what exact builder type is passed to the closure callback of whereHas()? Is Illuminate\Contracts\Database\Query\Builder a valid typehint there, or should it be Illuminate\Database\Eloquent\Builder?
💡 Result:
In Laravel 12.x, the closure passed to whereHas receives an instance of Illuminate\Database\Eloquent\Builder. The framework’s PHPDoc for whereHas specifies the callback as (Closure(Illuminate\Database\Eloquent\Builder): mixed)|null [1]. The official 12.x API docs also show the whereHas signature that includes a Closure|null $callback parameter [2]. So: - Using Illuminate\Database\Eloquent\Builder as the closure callback typehint is the correct choice. - Illuminate\Contracts\Database\Query\Builder is not the right type for that callback (that interface is for the lower-level query builder; whereHas supplies an Eloquent builder instance to the callback, as indicated by the Eloquent\Builder generic in the whereHas docblock) [1].
Citations:
- 1: https://github.com/illuminate/database/blob/master/Eloquent/Concerns/QueriesRelationships.php
- 2: https://api.laravel.com/docs/12.x/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.html
🏁 Script executed:
cat -n app-modules/panel-app/src/Livewire/Events/MyEventsList.phpRepository: he4rt/heartdevs.com
Length of output: 1336
🏁 Script executed:
#!/bin/bash
# Check if Eloquent\Builder implements Query\Builder interface
rg "class Builder" -A 5 -B 2 | head -50Repository: he4rt/heartdevs.com
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Search for Eloquent Builder class in vendor or documentation
find . -type f -name "Builder.php" | grep -E "(Eloquent|Database)" | head -10Repository: he4rt/heartdevs.com
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Search for other whereHas usages in the codebase to see what pattern is used elsewhere
rg "whereHas\(" -A 2 -B 1 | head -100Repository: he4rt/heartdevs.com
Length of output: 3228
🏁 Script executed:
#!/bin/bash
# Check imports in the files that use whereHas with Builder typehint
echo "=== ImportDiscordMembers.php imports ==="
head -20 app/Console/Commands/ImportDiscordMembers.php | grep -E "^use"
echo ""
echo "=== HistoryBasedPenaltyAdvisor.php imports ==="
head -30 app-modules/moderation/src/Classification/Actions/Advisors/HistoryBasedPenaltyAdvisor.php | grep -E "^use"Repository: he4rt/heartdevs.com
Length of output: 971
🏁 Script executed:
#!/bin/bash
# Check if there are any issues, tests, or error reports mentioning type errors in these whereHas calls
rg "TypeError|whereHas" -i | head -30Repository: he4rt/heartdevs.com
Length of output: 1276
🏁 Script executed:
#!/bin/bash
# Let's check the Laravel docs/source to verify if Eloquent\Builder implements Query\Builder
# Search for any documentation or comments about the relationship between these types
rg "Eloquent.*Builder.*Query.*Builder|Query.*Builder.*Eloquent" -i | head -20Repository: he4rt/heartdevs.com
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Check if there are any version constraints for Laravel in composer.json
cat composer.json | grep -A 5 -B 5 "laravel\|illuminate"Repository: he4rt/heartdevs.com
Length of output: 3114
🏁 Script executed:
#!/bin/bash
# Check Laravel 13.x documentation or source to verify the exact builder type passed to whereHas
# Let's also see if there are any IDE helper or type stubs that might show the correct signature
find . -type f -name "*.php" -path "*vendor*" | head -5Repository: he4rt/heartdevs.com
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Search for documentation or comments about the relationship between Builder types
# Also check if there are any type checking configuration files
cat phpstan.neon 2>/dev/null || cat phpstan.neon.dist 2>/dev/null || echo "No phpstan config found"Repository: he4rt/heartdevs.com
Length of output: 219
🏁 Script executed:
#!/bin/bash
# Let's check if there are any type checking errors reported in the codebase
# Run phpstan on the specific file to see if it reports a type issue
echo "Looking for phpstan output or errors related to Builder types..."
rg "Builder" -t php app/Console/Commands/ImportDiscordMembers.php -B 3 -A 3 | head -30Repository: he4rt/heartdevs.com
Length of output: 659
🌐 Web query:
Does Illuminate\Database\Eloquent\Builder implement Illuminate\Contracts\Database\Query\Builder interface in Laravel 13.x? Can using the interface typehint in whereHas callback cause a runtime TypeError with declare(strict_types=1)?
💡 Result:
In Laravel 13.x, Illuminate\Database\Eloquent\Builder does not implement the Illuminate\Contracts\Database\Query\Builder interface [1][2]. These are distinct classes designed for different purposes, and while the Eloquent builder composes the database query builder, it is not a drop-in replacement that adheres to that specific interface contract [2]. Using a typehint for Illuminate\Contracts\Database\Query\Builder in a whereHas callback will likely cause a TypeError if the runtime actually passes an instance of Illuminate\Database\Eloquent\Builder, regardless of whether declare(strict_types=1) is enabled [3]. While declare(strict_types=1) enforces strict type checking for function calls made from within the file where it is declared [4], the TypeError in this scenario occurs because of a mismatch between the declared parameter type and the actual object provided by the Laravel framework during execution [3]. When Laravel invokes your callback, it passes an instance of Illuminate\Database\Eloquent\Builder [2]. If your callback explicitly typehints the Query Builder interface, PHP will detect that the Eloquent Builder instance does not satisfy that type constraint, triggering a TypeError [3]. To avoid this, you should either remove the typehint, use the Eloquent Builder class typehint (Illuminate\Database\Eloquent\Builder), or use no typehint at all, allowing PHP to accept the object as passed [3].
Citations:
- 1: https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Query/Builder.php
- 2: https://github.com/illuminate/database/blob/master/Eloquent/Builder.php
- 3: https://stackoverflow.com/questions/73957689/laravel-wherein-argument-1-must-be-of-type-illuminate-database-eloquent-builder
- 4: https://stackoverflow.com/questions/67889115/why-does-php-strict-typing-allow-function-arguments-of-wrong-type
Change Builder typehint to use Illuminate\Database\Eloquent\Builder.
On line 22, the callback is typed as Illuminate\Contracts\Database\Query\Builder, but whereHas passes an Illuminate\Database\Eloquent\Builder instance. Since Eloquent\Builder does not implement the Query\Builder interface, this will trigger a runtime TypeError when the method executes, breaking the My Events page.
🔧 Proposed fix
-use Illuminate\Contracts\Database\Query\Builder;
+use Illuminate\Database\Eloquent\Builder;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/panel-app/src/Livewire/Events/MyEventsList.php` around lines 9 -
23, The whereHas callback in MyEventsList::getEnrollmentsProperty is type-hinted
with Illuminate\Contracts\Database\Query\Builder but whereHas supplies an
Illuminate\Database\Eloquent\Builder; update the import/use so the callback uses
Illuminate\Database\Eloquent\Builder (or remove the incorrect contract import
and reference Eloquent\Builder) to avoid the runtime TypeError when invoking
whereHas.
|
|
||
| ## Relationships | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Add a language tag to the fenced diagram block.
Line 18 uses a fenced code block without language, which triggers markdownlint MD040.
Suggested patch
-```
+```text
┌─────────────────┐ ┌──────────────────────┐
...
-```
+```📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 18-18: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CONTEXT-MAP.md` at line 18, The fenced ASCII diagram block (the
triple-backtick fenced code block containing the context map diagram) is missing
a language tag and triggers markdownlint MD040; update the opening fence from
``` to include a language tag such as ```text (i.e., add "text" after the
opening backticks) so the diagram block is explicitly marked as plain text while
leaving the closing ``` unchanged.
| private function validate(Event $event, User $user): void | ||
| { | ||
| throw_unless( | ||
| $event->status === EventStatus::Published, | ||
| EnrollmentException::eventNotActive(), | ||
| ); | ||
|
|
||
| throw_if( | ||
| $event->starts_at->lte(now()), | ||
| EnrollmentException::eventPast(), | ||
| ); | ||
|
|
||
| $event->loadMissing('enrollmentPolicy'); | ||
|
|
||
| throw_unless( | ||
| in_array( | ||
| $event->enrollmentPolicy?->enrollment_method, | ||
| [EnrollmentMethod::Rsvp, EnrollmentMethod::RsvpCheckin], | ||
| strict: true, | ||
| ), | ||
| EnrollmentException::invalidEnrollmentMethod(), | ||
| ); | ||
|
|
||
| throw_if( | ||
| Enrollment::query() | ||
| ->where('event_id', $event->id) | ||
| ->where('user_id', $user->id) | ||
| ->exists(), | ||
| EnrollmentException::alreadyEnrolled(), | ||
| ); |
There was a problem hiding this comment.
Fico na dúvida se isso deveria ser parte da lógica do Filament Form ou qualquer outra camada anterior de request... Mas de qualquer forma tá dahora.
Pass only enrollment inputs into EnrollUserAction instead of Event and User models, mapping at the UI and test boundaries. Co-authored-by: Cursor <cursoragent@cursor.com>
GabrielFVDev
left a comment
There was a problem hiding this comment.
Entendo quase nd de php, os comentarios que to deixando aqui é de acordo com o claudinho.
Se o que ele disse é sem sentido, só dalé pra aprove
Summary
confirmedstatus → transition recorded in audit trail → enrollment visible in App panelEnrollUserAction: validates event (published, upcoming, RSVP/rsvp_checkin), creates enrollment, writesEnrollmentTransition(from_status=null,to_status=confirmed,triggered_by=user), dispatchesEnrollmentConfirmedinsideDB::transactionEnrollmentConfirmedimplementsShouldDispatchAfterCommit(payload:enrollment_id,event_id,user_id,xpRewardRsvp) — no listener in this sliceEventResourceform/infolist/table withstatuscolumn (EventStatusenum) instead of legacyactivetoggleevent_date(renamed in foundation)Already delivered in #249 (foundation — not repeated here):
Event,Enrollment,EnrollmentPolicy,EnrollmentTransition), enums, factoriesEventResource+ read-onlyEnrollmentsRelationManagerArchitecture
Events module |-- Enrollment domain (existing — #249) | +-- EnrollUserAction | +-- EnrollmentConfirmed (domain event) | +-- EnrollmentException | App panel |-- EventsPage (discover published upcoming events) |-- EventPage (detail + Confirm Presence) `-- MyEventsPage (participant enrollments)
Admin panel `-- EventResource (status field fix)
Files
EnrollUserAction)EnrollmentConfirmed)EnrollmentException)EnrollUserActionTest,RsvpEnrollmentTest)published,upcoming,past,rsvp)Out of scope (future slices)
Closes #240
Parent: #237
Test plan
php artisan migratephp artisan test --filter=EnrollUserActionTestphp artisan test --filter=RsvpEnrollmentTestphp artisan test --filter=EventResourceTestphp artisan test --filter=EventFactoriesTest./vendor/bin/pint --teststarts_at, enrollment method RSVP/app/{tenant}/events— event appears in list/app/{tenant}/my-events— enrollment shows Confirmed badgePull Request Summary
Description
Implements end-to-end RSVP enrollment flow for events with database schema, domain models, and UI components. Users can confirm presence at events through the app panel. Admins manage events and view enrollments. Includes state machine for enrollment lifecycle, transition auditing, and event domain dispatch for gamification integration.
References
#240— RSVP enrollment end-to-end implementation#237Dependencies & Requirements
No new external dependencies added. Uses existing Laravel Eloquent, Filament, and Livewire frameworks. Configuration:
EventsServiceProvider::boot()Contributor Summary
Changes Summary
app-modules/events/database/migrations/app-modules/events/src/Enrollment/Models/app-modules/events/src/Event/Models/Event.phpapp-modules/events/src/CheckIn/Models/app-modules/events/src/Enrollment/Enums/app-modules/events/src/Event/Enums/app-modules/events/src/CheckIn/Enums/CheckInMethod.phpapp-modules/events/src/Enrollment/Actions/EnrollUserAction.phpapp-modules/events/src/Enrollment/Events/EnrollmentConfirmed.phpapp-modules/events/src/Enrollment/Exceptions/EnrollmentException.phpapp-modules/events/database/factories/app-modules/events/lang/app-modules/panel-admin/src/Filament/Resources/Events/app-modules/panel-admin/src/Filament/Resources/Events/Pages/app-modules/panel-app/src/Pages/app-modules/panel-app/src/Livewire/Events/app-modules/panel-app/resources/views/livewire/events/app-modules/panel-app/resources/views/pages/app-modules/events/tests/Feature/app-modules/events/tests/Unit/app-modules/events/docs/adr/app-modules/events/CONTEXT.mdCONTEXT-MAP.md