Skip to content

feat(events): rsvp enrollment end-to-end#263

Closed
BrunaDomingues wants to merge 7 commits into
feat/eventsfrom
feat/events-rsvp-enrollment
Closed

feat(events): rsvp enrollment end-to-end#263
BrunaDomingues wants to merge 7 commits into
feat/eventsfrom
feat/events-rsvp-enrollment

Conversation

@BrunaDomingues
Copy link
Copy Markdown

@BrunaDomingues BrunaDomingues commented May 20, 2026

Summary

  • RSVP enrollment flow: participant confirms presence → enrollment created with confirmed status → transition recorded in audit trail → enrollment visible in App panel
  • EnrollUserAction: validates event (published, upcoming, RSVP/rsvp_checkin), creates enrollment, writes EnrollmentTransition (from_status=null, to_status=confirmed, triggered_by=user), dispatches EnrollmentConfirmed inside DB::transaction
  • Domain event: EnrollmentConfirmed implements ShouldDispatchAfterCommit (payload: enrollment_id, event_id, user_id, xpRewardRsvp) — no listener in this slice
  • App panel (participant): Events listing, event detail with Confirm Presence button, My Events page with status badges
  • Admin fixes: align EventResource form/infolist/table with status column (EventStatus enum) instead of legacy active toggle
  • Test fix: update check-in factory test to assert event_date (renamed in foundation)

Already delivered in #249 (foundation — not repeated here):

  • Schema, models (Event, Enrollment, EnrollmentPolicy, EnrollmentTransition), enums, factories
  • Admin: EventResource + read-only EnrollmentsRelationManager

Architecture

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

  • 1 Action (EnrollUserAction)
  • 1 domain event (EnrollmentConfirmed)
  • 1 exception class (EnrollmentException)
  • 4 lang files (en/pt_BR — exceptions + pages)
  • 3 Filament pages + 3 Livewire components + 6 Blade views
  • 2 feature test files (EnrollUserActionTest, RsvpEnrollmentTest)
  • Factory states (published, upcoming, past, rsvp)
  • Admin schema fixes (form, infolist, table)

Out of scope (future slices)

  • Capacity / waitlist validation
  • Gamification listener for XP
  • Application enrollment flow (approve/reject)
  • Enrollment management actions in admin

Closes #240
Parent: #237

Test plan

  • php artisan migrate
  • php artisan test --filter=EnrollUserActionTest
  • php artisan test --filter=RsvpEnrollmentTest
  • php artisan test --filter=EventResourceTest
  • php artisan test --filter=EventFactoriesTest
  • ./vendor/bin/pint --test
  • Admin: create event with Published status, future starts_at, enrollment method RSVP
  • App: /app/{tenant}/events — event appears in list
  • App: open event → Confirm Presence → success notification
  • App: /app/{tenant}/my-events — enrollment shows Confirmed badge
  • Admin: event edit → Enrollments tab shows participant
  • Duplicate RSVP attempt returns error
  • Past event does not show Confirm Presence button

Pull 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

  • Closes #240 — RSVP enrollment end-to-end implementation
  • Parent PR: #237
  • Related bounded context: Events with Gamification integration

Dependencies & Requirements

No new external dependencies added. Uses existing Laravel Eloquent, Filament, and Livewire frameworks. Configuration:

  • Translation files loaded via EventsServiceProvider::boot()
  • Filament resources and pages registered in admin and app panels
  • Livewire components bound in service providers

Contributor Summary

Contributor Lines Added Lines Removed Files Changed
Implementation 2,247 157 96

Changes Summary

File Path Change Description
app-modules/events/database/migrations/ 7 migrations: events, enrollment_policies, enrollments, enrollment_transitions, check_ins, check_in_codes, qr_tokens tables
app-modules/events/src/Enrollment/Models/ Enrollment, EnrollmentPolicy, EnrollmentTransition models with relationships
app-modules/events/src/Event/Models/Event.php Event model with scopes and relationships
app-modules/events/src/CheckIn/Models/ CheckIn, CheckInCode, QrToken models
app-modules/events/src/Enrollment/Enums/ EnrollmentStatus, EnrollmentMethod, AttendanceRequirement, TriggeredBy enums
app-modules/events/src/Event/Enums/ EventStatus, EventType enums
app-modules/events/src/CheckIn/Enums/CheckInMethod.php CheckInMethod enum
app-modules/events/src/Enrollment/Actions/EnrollUserAction.php Action for user enrollment with validation and transaction
app-modules/events/src/Enrollment/Events/EnrollmentConfirmed.php Domain event with ShouldDispatchAfterCommit
app-modules/events/src/Enrollment/Exceptions/EnrollmentException.php Validation exception with static constructors
app-modules/events/database/factories/ 6 new factories and EventFactory updates
app-modules/events/lang/ English and Portuguese translations for enums, exceptions, pages
app-modules/panel-admin/src/Filament/Resources/Events/ EventResource with form/infolist/table schemas and EnrollmentsRelationManager
app-modules/panel-admin/src/Filament/Resources/Events/Pages/ CreateEvent, EditEvent, ListEvents pages
app-modules/panel-app/src/Pages/ EventsPage, EventPage, MyEventsPage
app-modules/panel-app/src/Livewire/Events/ EventsList, EventDetail, MyEventsList components
app-modules/panel-app/resources/views/livewire/events/ event-detail, events-list, my-events-list Blade templates
app-modules/panel-app/resources/views/pages/ event, events, my-events page templates
app-modules/events/tests/Feature/ EnrollUserActionTest, RsvpEnrollmentTest, EventFactoriesTest, EventResourceTest
app-modules/events/tests/Unit/ EnrollmentStatusTest, EventStatusTest
app-modules/events/docs/adr/ 5 ADRs: state machine design, XP via events, no DB triggers, check-in model, bot communication
app-modules/events/CONTEXT.md Bounded context documentation
CONTEXT-MAP.md Updated with Events and Gamification contexts

Review Change Stack

@BrunaDomingues BrunaDomingues self-assigned this May 20, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

This 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 EnrollUserAction orchestrating transactional enrollment with audit trail and domain events, Filament admin UI (EventResource, forms, relation managers), app panel pages and Livewire components for participant RSVP, comprehensive feature and unit tests, and service provider wiring. The changes include localization files for English and Portuguese, ADR documentation on architectural decisions (enum-based state machine, domain-event-driven XP, no database triggers, separate check-in tables, event-based bot communication), and removal of legacy event factories.

Possibly related issues

  • #240: This PR fully implements the RSVP enrollment end-to-end slice specified in the issue, including EnrollUserAction, domain event dispatch, audit trail, admin enrollments relation manager, and app panel "My Events" page with feature test coverage.
  • #241: The PR adds enrollment capacity and waitlist fields to EnrollmentPolicy and includes related enums, but EnrollUserAction does not yet implement capacity checks, waitlist positioning, or the EnrollmentWaitlisted event—these are likely deferred to a follow-up slice.
  • #243: The PR adds application_schema field and EnrollmentMethod::Application enum with UI support, but EnrollUserAction explicitly rejects Application enrollments; application approval/rejection logic is deferred.

Possibly related PRs

  • he4rt/heartdevs.com#236: Both PRs are part of the Events module refactor; this PR adds the new schema while #236 removes legacy events infrastructure.
  • he4rt/heartdevs.com#206: This PR depends on the tenant-scoped admin infrastructure and resource discovery introduced in #206 for proper Filament panel integration.

Suggested reviewers

  • Clintonrocha98
  • GabrielFVDev
  • gvieira18
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/events-rsvp-enrollment

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5a45594 and 1cafa8a.

📒 Files selected for processing (79)
  • CONTEXT-MAP.md
  • app-modules/events/CONTEXT.md
  • app-modules/events/database/factories/CheckInCodeFactory.php
  • app-modules/events/database/factories/CheckInFactory.php
  • app-modules/events/database/factories/EnrollmentFactory.php
  • app-modules/events/database/factories/EnrollmentPolicyFactory.php
  • app-modules/events/database/factories/EnrollmentTransitionFactory.php
  • app-modules/events/database/factories/EventAgendaFactory.php
  • app-modules/events/database/factories/EventFactory.php
  • app-modules/events/database/factories/EventSubmissionFactory.php
  • app-modules/events/database/factories/EventSubmissionSpeakerFactory.php
  • app-modules/events/database/factories/QrTokenFactory.php
  • app-modules/events/database/factories/SponsorFactory.php
  • app-modules/events/database/migrations/2026_05_16_195205_drop_events_module_tables.php
  • app-modules/events/database/migrations/2026_05_16_200001_create_events_table.php
  • app-modules/events/database/migrations/2026_05_16_200002_create_events_enrollment_policies_table.php
  • app-modules/events/database/migrations/2026_05_16_200003_create_events_enrollments_table.php
  • app-modules/events/database/migrations/2026_05_16_200004_create_events_enrollment_transitions_table.php
  • app-modules/events/database/migrations/2026_05_16_200005_create_events_check_ins_table.php
  • app-modules/events/database/migrations/2026_05_16_200006_create_events_check_in_codes_table.php
  • app-modules/events/database/migrations/2026_05_16_200007_create_events_qr_tokens_table.php
  • app-modules/events/docs/adr/0001-state-machine-as-enum-not-package.md
  • app-modules/events/docs/adr/0002-xp-via-domain-events-not-ledger.md
  • app-modules/events/docs/adr/0003-no-database-triggers-all-logic-in-actions.md
  • app-modules/events/docs/adr/0004-check-in-as-separate-table-one-to-many.md
  • app-modules/events/docs/adr/0005-bot-communicates-via-domain-events-not-api.md
  • app-modules/events/lang/en/enums.php
  • app-modules/events/lang/en/exceptions.php
  • app-modules/events/lang/en/pages.php
  • app-modules/events/lang/pt_BR/enums.php
  • app-modules/events/lang/pt_BR/exceptions.php
  • app-modules/events/lang/pt_BR/pages.php
  • app-modules/events/src/CheckIn/Enums/CheckInMethod.php
  • app-modules/events/src/CheckIn/Models/CheckIn.php
  • app-modules/events/src/CheckIn/Models/CheckInCode.php
  • app-modules/events/src/CheckIn/Models/QrToken.php
  • app-modules/events/src/Enrollment/Actions/EnrollUserAction.php
  • app-modules/events/src/Enrollment/Enums/AttendanceRequirement.php
  • app-modules/events/src/Enrollment/Enums/EnrollmentMethod.php
  • app-modules/events/src/Enrollment/Enums/EnrollmentStatus.php
  • app-modules/events/src/Enrollment/Enums/TriggeredBy.php
  • app-modules/events/src/Enrollment/Events/EnrollmentConfirmed.php
  • app-modules/events/src/Enrollment/Exceptions/EnrollmentException.php
  • app-modules/events/src/Enrollment/Models/Enrollment.php
  • app-modules/events/src/Enrollment/Models/EnrollmentPolicy.php
  • app-modules/events/src/Enrollment/Models/EnrollmentTransition.php
  • app-modules/events/src/Event/Enums/EventStatus.php
  • app-modules/events/src/Event/Enums/EventType.php
  • app-modules/events/src/Event/Models/Event.php
  • app-modules/events/src/EventsServiceProvider.php
  • app-modules/events/tests/Feature/EnrollUserActionTest.php
  • app-modules/events/tests/Feature/EventFactoriesTest.php
  • app-modules/events/tests/Feature/EventResourceTest.php
  • app-modules/events/tests/Unit/EnrollmentStatusTest.php
  • app-modules/events/tests/Unit/EventStatusTest.php
  • app-modules/panel-admin/src/Filament/Resources/Events/EventResource.php
  • app-modules/panel-admin/src/Filament/Resources/Events/Pages/CreateEvent.php
  • app-modules/panel-admin/src/Filament/Resources/Events/Pages/EditEvent.php
  • app-modules/panel-admin/src/Filament/Resources/Events/Pages/ListEvents.php
  • app-modules/panel-admin/src/Filament/Resources/Events/RelationManagers/EnrollmentsRelationManager.php
  • app-modules/panel-admin/src/Filament/Resources/Events/Schemas/EventForm.php
  • app-modules/panel-admin/src/Filament/Resources/Events/Schemas/EventInfolist.php
  • app-modules/panel-admin/src/Filament/Resources/Events/Tables/EventsTable.php
  • app-modules/panel-admin/src/PanelAdminServiceProvider.php
  • app-modules/panel-app/resources/views/livewire/events/event-detail.blade.php
  • app-modules/panel-app/resources/views/livewire/events/events-list.blade.php
  • app-modules/panel-app/resources/views/livewire/events/my-events-list.blade.php
  • app-modules/panel-app/resources/views/pages/event.blade.php
  • app-modules/panel-app/resources/views/pages/events.blade.php
  • app-modules/panel-app/resources/views/pages/my-events.blade.php
  • app-modules/panel-app/src/Livewire/Events/EventDetail.php
  • app-modules/panel-app/src/Livewire/Events/EventsList.php
  • app-modules/panel-app/src/Livewire/Events/MyEventsList.php
  • app-modules/panel-app/src/Pages/EventPage.php
  • app-modules/panel-app/src/Pages/EventsPage.php
  • app-modules/panel-app/src/Pages/MyEventsPage.php
  • app-modules/panel-app/src/PanelAppServiceProvider.php
  • app-modules/panel-app/tests/Feature/Events/RsvpEnrollmentTest.php
  • app/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

```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
```
🧰 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.

Comment on lines +21 to +24
public function down(): void
{
// Irreversible: removes legacy events module tables replaced by the new schema.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +25 to +47
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +90 to +93
public function totalDays(): int
{
return (int) $this->starts_at->startOfDay()->diffInDays($this->ends_at->startOfDay()) + 1;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -path "*app-modules/events/src/Event/Models/Event.php" -type f

Repository: 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:


🏁 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 -n

Repository: 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.

Suggested change
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.

Comment on lines +112 to +119
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +9 to +23
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')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 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:


🏁 Script executed:

cat -n app-modules/panel-app/src/Livewire/Events/MyEventsList.php

Repository: 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 -50

Repository: 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 -10

Repository: 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 -100

Repository: 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 -30

Repository: 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 -20

Repository: 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 -5

Repository: 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 -30

Repository: 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:


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.

Comment thread CONTEXT-MAP.md

## Relationships

```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
```
🧰 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.

@BrunaDomingues BrunaDomingues changed the base branch from 4.x to feat/events May 20, 2026 14:42
Comment thread app-modules/events/src/Enrollment/Actions/EnrollUserAction.php Outdated
Comment on lines +67 to +96
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(),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Comment thread app-modules/events/src/Enrollment/Actions/EnrollUserAction.php
Comment thread app-modules/panel-app/src/Livewire/Events/EventDetail.php
Copy link
Copy Markdown

@GabrielFVDev GabrielFVDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Events: RSVP Enrollment end-to-end

4 participants