Skip to content

feat: events module#309

Open
danielhe4rt wants to merge 20 commits into
4.xfrom
feat/events
Open

feat: events module#309
danielhe4rt wants to merge 20 commits into
4.xfrom
feat/events

Conversation

@danielhe4rt

@danielhe4rt danielhe4rt commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces the events participation module (app-modules/events/) with the full lifecycle: enrollment → check-in → attendance → closure. Ships three enrollment methods (RSVP, RSVP + check-in, Application with dynamic form builder), three check-in methods (manual, numeric code, QR), waitlist + capacity enforcement, an event-closure job, bot check-in integration, and full Admin/App panel UI with i18n (en + pt_BR).

Sub-domains (src/)

  • Event — core aggregate (Event, EventType, EventStatus)
  • Enrollment — RSVP + Application flow, policies, transitions, waitlist/capacity
  • CheckIn — manual / numeric code / QR strategies, bot integration
  • Closure — closure job, attended/no-show, admin status override

Closes

Umbrella:

Sub-issues:

danielhe4rt and others added 7 commits June 4, 2026 18:00
- Events domain: `Event` + `EventType` com regras de enrollment e enums
de status/attendance
- Enrollment flow: `Enrollment`, `EnrollmentPolicy`,
`EnrollmentTransition` com factories e testes
- Check-in flow: `CheckIn`, `CheckInCode`, `QrToken` + `CheckInMethod`
- Admin panel: `EventResource` com pages de create/edit/list, relation
manager de enrollments, melhorias em form/infolist/table
- Cleanup/docs: ajuste de legado, ADRs e atualizacao de context map

Events module
|-- Event domain (EventType + Event)
|-- Enrollment domain (EnrollmentMethod/Status/AttendanceRequirement +
Enrollment/Policy/Transition)
`-- Check-in domain (CheckInMethod + CheckIn/CheckInCode/QrToken)

Admin panel
`-- Event management (EventResource + pages + relation manager +
form/infolist/table)

- 8 migrations (inclui drop do legado)
- 7 models e 5 enums
- 7 factories
- 3 testes (feature + unit)
- Recursos do painel admin para eventos

- php artisan migrate
- php artisan test --filter=EventFactoriesTest
- php artisan test --filter=EventResourceTest
- php artisan test --filter=EnrollmentStatusTest

---------

Co-authored-by: danielhe4rt <danielhe4rt@gmail.com>
Implements the **Events** bounded context (foundation) and the **RSVP
enrollment** flow end-to-end: participant confirms presence → enrollment
created as `confirmed` or `waitlisted` (when at capacity with waitlist)
→ transition recorded in audit trail → enrollment visible in App and
Admin panels.

- **`EnrollUserAction`**: validates event (published, upcoming,
RSVP/rsvp_checkin), resolves initial status (`confirmed` vs `waitlisted`
vs rejected when full), creates enrollment, writes
`EnrollmentTransition` (`from_status=null`, `to_status=<initial>`,
`triggered_by=user`), dispatches `EnrollmentConfirmed` only when status
is `confirmed` — all inside `DB::transaction` with `lockForUpdate` on
policy and duplicate handling via `UniqueConstraintViolationException`
- **`EnrollUserDTO`**: input object built from `Event` + `User` (status,
dates, policy capacity/waitlist, XP reward)
- **Domain event**: `EnrollmentConfirmed` implements
`ShouldDispatchAfterCommit` (payload: `enrollment_id`, `event_id`,
`user_id`, `xpRewardOnConfirmed`) — no listener in this slice
- **`EnrollmentException`**: `alreadyEnrolled`, `eventPast`,
`eventNotActive`, `invalidEnrollmentMethod`, `eventFull`

- When `enrollment_policy.capacity` is set and confirmed count ≥
capacity:
- **`has_waitlist=true`** → enrollment `waitlisted` with
`waitlist_position` (no `EnrollmentConfirmed`)
  - **`has_waitlist=false`** → `EnrollmentException::eventFull()`
- App UI: **Confirm Presence** hidden when full without waitlist;
success notification differs for confirmed vs waitlisted; **My Events**
shows status badges (including waitlisted)

- Events listing (`EventsPage` / `EventsList`) — published upcoming
events
- Event detail (`EventPage` / `EventDetail`) — **Confirm Presence**
button
- My Events (`MyEventsPage` / `MyEventsList`) — enrollments with status
badges; enrolled users can open detail for `completed` events

- **`EventResource`**: form/infolist/table aligned with `status` column
(`EventStatus` enum) instead of legacy `active` toggle
- Unique event **slug per tenant** validation on create/edit
- Read-only **Enrollments** relation manager

Schema, models (`Event`, `Enrollment`, `EnrollmentPolicy`,
`EnrollmentTransition`, check-in models), enums, factories, migrations,
ADRs, `CONTEXT.md`, admin resource scaffolding, unit tests for enums.

Events module ├── Enrollment domain │ ├── EnrollUserAction │ ├──
EnrollUserDTO │ ├── EnrollmentConfirmed (domain event) │ └──
EnrollmentException ├── Event domain (scopes: published, upcoming,
viewableByParticipant) └── Check-in models (schema only — no flow in
this PR)

App panel ├── EventsPage / EventsList ├── EventPage / EventDetail (+
Confirm Presence) └── MyEventsPage / MyEventsList

Admin panel └── EventResource (+ EnrollmentsRelationManager)

| Area | Count / notes |
|------|----------------|
| Migrations | 7 (+ drop legacy tables) |
| Actions | `EnrollUserAction` |
| DTOs | `EnrollUserDTO` |
| Domain events | `EnrollmentConfirmed` |
| Exceptions + lang | `EnrollmentException`, en/pt_BR exceptions + pages
+ enums |
| App panel | 3 Filament pages, 3 Livewire components, 7 Blade views
(incl. partial) |
| Admin | `EventResource` schemas + relation manager |
| Tests | `EnrollUserActionTest`, `RsvpEnrollmentTest`,
`EventResourceTest`, `EventFactoriesTest`, enum unit tests |
| Docs | 5 ADRs, `CONTEXT.md`, `CONTEXT-MAP.md` |

- Gamification listener for XP (`EnrollmentConfirmed` subscriber)
- Application enrollment flow (approve/reject)
- Enrollment management actions in admin (promote from waitlist, cancel,
etc.)
- Check-in / QR flows
- Bot integration

Closes #240
Parent: #237
Related foundation PR: #249 (included in this branch when targeting
`4.x`)

---

- [ ] `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`

- [ ] Create event: **Published**, future `starts_at`, enrollment method
**RSVP**
- [ ] Set `capacity` and toggle `has_waitlist` on enrollment policy
- [ ] Edit event → **Enrollments** tab shows participants

- [ ] `/app/{tenant}/events` — event appears in list
- [ ] Open event → **Confirm Presence** → success notification
- [ ] `/app/{tenant}/my-events` — enrollment shows **Confirmed** badge

- [ ] Fill event to capacity **without** waitlist → **Confirm Presence**
hidden; direct enroll returns `event_full`
- [ ] Fill event to capacity **with** waitlist → button still visible;
enroll creates **Waitlisted** badge and waitlist notification

- [ ] Duplicate RSVP → error notification (`already_enrolled`)
- [ ] Past event → no **Confirm Presence** button
- [ ] Draft / non-RSVP event → not enrollable via RSVP
- [ ] Enrolled user on **completed** event → detail page still
accessible

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: GabrielFV <gabrielvasco906@gmail.com>
Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
## Summary
### Solve: #244 

Implements manual check-in: organizers mark participants as present via
the Admin panel. Creates a check-in record and transitions enrollment
status. Covers single-day and multi-day events with duplicate
prevention, date range validation, audit trail, and bulk check-in.

## What was implemented

### `CheckInAction` — Core action

- Accepts enrollment, method (enum), payload (array), event_date (Carbon
date)
- Validates enrollment status is `confirmed` or `checked_in`
- Validates event_date is within the event's date range
- Validates no existing check-in for the same enrollment + date
- Creates `events_check_ins` record with `method=manual`,
`payload={actor_user_id}`, `event_date`, `checked_in_at=now`
- First check-in transitions enrollment from `confirmed` → `checked_in`
- Subsequent check-ins (multi-day) only create the check-in record
- Records audit trail (`triggered_by=admin`, `actor_user_id=organizer`)
- Dispatches `ParticipantCheckedIn` domain event
(`ShouldDispatchAfterCommit`)

### Admin panel — `EnrollmentsRelationManager`

- "Check In" action on enrollment records (visible when status is
`confirmed` or `checked_in`)
- Date picker defaulting to today, constrained to event date range
- Bulk "Check In Selected" action for multiple participants
- Check-in history column per enrollment

### `TransitionEnrollmentAction`

Extracted reusable action for enrollment status transitions, used by the
check-in flow and available for future transitions (`attended`,
`cancelled`).

### `CheckInException`

Check-in validation errors moved to the CheckIn module boundary
(`CheckIn/Exceptions/`) instead of leaking into `EnrollmentException`.

## Acceptance criteria

- [x] Organizer can mark a participant as checked in from Admin panel
- [x] Check-in record created in `events_check_ins` with method=manual
and correct date
- [x] First check-in transitions enrollment from `confirmed` →
`checked_in`
- [x] Subsequent check-ins (multi-day) create records without
re-transitioning
- [x] Duplicate check-in on same date is rejected
- [x] Check-in outside event date range is rejected
- [x] Bulk check-in action works for multiple participants
- [x] `ParticipantCheckedIn` domain event is dispatched
- [x] Transition audit trail records the organizer
- [x] Feature tests: single, multi-day, duplicate, date range, status
validation
- [x] Pint passes

## Files changed

### New (8 files)

| File | Purpose |
|---|---|
| `app-modules/events/src/CheckIn/Actions/CheckInAction.php` | Core
check-in action (validate, create record, transition, dispatch) |
| `app-modules/events/src/CheckIn/Exceptions/CheckInException.php` |
Check-in specific exceptions (invalid status, outside range, duplicate,
invalid actor) |
| `app-modules/events/src/CheckIn/Events/ParticipantCheckedIn.php` |
Domain event dispatched after check-in (`ShouldDispatchAfterCommit`) |
|
`app-modules/events/src/Enrollment/Actions/TransitionEnrollmentAction.php`
| Reusable action for enrollment status transitions with audit trail |
| `app-modules/events/lang/en/check_in.php` | English check-in exception
messages |
| `app-modules/events/lang/pt_BR/check_in.php` | Brazilian Portuguese
check-in exception messages |
| `app-modules/events/database/factories/CheckInFactory.php` | Factory
for CheckIn model |
| `app-modules/events/tests/Feature/CheckInActionTest.php` | 6 feature
tests (single, multi-day, duplicate, date range, status, actor) |

### Modified (11 files)

| File | What changed |
|---|---|
| `app-modules/events/src/CheckIn/Models/CheckIn.php` | Added
`checked_in_at` to fillable, casts, phpdocs |
|
`app-modules/events/database/migrations/2026_05_22_000001_add_checked_in_at_to_events_check_ins_table.php`
| Added `checked_in_at` column to `events_check_ins` |
| `app-modules/events/src/Enrollment/Models/Enrollment.php` | Added
`checkIns()` hasMany relationship |
| `app-modules/events/src/Enrollment/Enums/EnrollmentStatus.php` | Added
`CheckedIn` case with transition rules |
| `app-modules/events/src/Enrollment/Exceptions/EnrollmentException.php`
| Added `invalidTransition()`; removed check-in exceptions (moved to
CheckInException) |
| `app-modules/events/src/Enrollment/Actions/EnrollUserAction.php` |
Import adjustment |
| `app-modules/events/src/Event/Models/Event.php` | Relationship
additions for enrollment policy |
| `app-modules/events/lang/en/exceptions.php` | Added
`invalid_transition` key; removed check-in keys |
| `app-modules/events/lang/pt_BR/exceptions.php` | Added
`invalid_transition` key; removed check-in keys |
| `app-modules/events/tests/Feature/EventResourceTest.php` | 5 tests:
Filament CRUD, single + bulk check-in, check-in history |
|
`app-modules/panel-admin/src/Filament/Resources/Events/RelationManagers/EnrollmentsRelationManager.php`
| "Check In" action, bulk "Check In Selected", check-in history column,
date picker |
## Summary

Completes **atomic capacity enforcement** and **waitlist (FIFO)** on top
of the RSVP flow (#275): when an event is full, participants are
waitlisted if the policy allows, or rejected with `422` otherwise.
Occupied seats count `confirmed`, `checked_in`, and `attended`
enrollments; audit trail and domain events are emitted on enroll.

### Capacity & waitlist (`EnrollUserAction`)

- **`scopeActive()`** on `Enrollment`: `confirmed` + `checked_in` +
`attended` occupy capacity (replaces counting only `confirmed` in
capacity resolution)
- Inside **`DB::transaction`**, `lockForUpdate()` on **event** and
**enrollment policy** (unchanged from #275; ensures fresh state under
concurrent enrollments)
- If `capacity` is `null` → always **`confirmed`**
- If `active` count `< capacity` → **`confirmed`** +
`EnrollmentConfirmed`
- If `active` count `>= capacity` and **`has_waitlist=true`** →
**`waitlisted`** with `waitlist_position = max(position) + 1` +
`EnrollmentWaitlisted` (no `EnrollmentConfirmed`)
- If `active` count `>= capacity` and **`has_waitlist=false`** →
**`EnrollmentException::eventFull()`** (HTTP 422)
- **`EnrollmentTransition`** recorded for every enroll
(`from_status=null`, `to_status=<initial>`, `triggered_by=user`)

### Domain event

- **`EnrollmentWaitlisted`**: implements `ShouldDispatchAfterCommit`
(payload: `enrollment_id`, `event_id`, `user_id`, `waitlist_position`) —
no listener in this slice

### App panel (participant)

- **Event detail**: persistent copy **“You are on the waitlist (position
X)”** when `waitlisted`
- **Event detail**: **“This event is full”** when at capacity without
waitlist (no **Confirm Presence** button)
- Success notification uses waitlist message **with position** after
RSVP
- **`canConfirmPresence` / `isEventFull`** use `active()` scope (aligned
with backend)

### Admin

- **Enrollments** relation manager: **`waitlist_position`** column
(toggleable)
- Status filter unchanged (confirmed, waitlisted, etc.)

### Scopes (`Enrollment` model)

- **`scopeConfirmed()`**, **`scopeWaitlisted()`**, **`scopeActive()`** —
`active` = capacity-occupying statuses only

### Architecture

Events module └── Enrollment domain ├── EnrollUserAction (capacity via
active() + lockForUpdate) ├── EnrollmentWaitlisted (domain event) └──
Enrollment ├── scopeConfirmed / scopeWaitlisted / scopeActive

App panel └── EventDetail (+ waitlist copy, event full state)

Admin panel └── EnrollmentsRelationManager (+ waitlist_position column)


### Files (high level)

| Area | Count / notes |
|------|----------------|
| Actions | `EnrollUserAction` (active count, dispatch
`EnrollmentWaitlisted`) |
| Domain events | `EnrollmentWaitlisted` |
| Models | `Enrollment` (`scopeActive` fix) |
| Enums | `EnrollmentStatus::getResponseMessage(?waitlistPosition)` |
| Lang | `en` / `pt_BR` `pages` (waitlist position, event full) |
| App panel | `EventDetail`, `event-detail.blade.php` |
| Admin | `EnrollmentsRelationManager` |
| Tests | `EnrollmentScopeTest`, `EnrollUserActionTest` (+
capacity/FIFO/422/unlimited), `RsvpEnrollmentTest` (+ UI) |

### Out of scope (future slices)

- Promote from waitlist on cancellation (FIFO promotion job/action)
- Notifications listener for `EnrollmentWaitlisted`
- True parallel-process concurrency test (race on last seat)
- Gamification listener for `EnrollmentConfirmed`

Closes #241  
Parent: #237  
Blocked by / builds on: #240, #275

---

## Test plan

- `php artisan test --filter=EnrollmentScopeTest`
- `php artisan test --filter=EnrollUserActionTest`
- `php artisan test --filter=RsvpEnrollmentTest`
- `./vendor/bin/pint --test`

### Admin

- Edit event with RSVP policy → set `capacity` and `has_waitlist`
- Open **Enrollments** tab → confirm **Waitlist** column and filter by
`waitlisted`

### App — capacity / waitlist

- Event **without** capacity limit → **Confirm Presence** →
**Confirmed**
- Fill to capacity **with** waitlist → next enroll → **Waitlisted**
badge + **“position X”** on detail + success notification with position
- Fill to capacity **without** waitlist → **“This event is full”**, no
**Confirm Presence** button
- Existing enrollment in **`checked_in`** occupies last seat → new user
→ **waitlisted** (validates `active()` scope)

### App — API / action edge cases

- Enroll when full without waitlist → `EnrollmentException` / 422
(`event_full`)
- Multiple enrollments beyond capacity with waitlist → positions **1, 2,
…** (FIFO)
Closes #245

Implements self-service check-in via short-lived numeric codes announced
by the organizer. Participants enter the code on the event detail page
to check themselves in without organizer intervention.

**Domain layer**
- Added `revoked_at` column to `events_check_in_codes` (soft revoke, per
ADR-0003)
- Created `NumericCodeCheckInDTO` carrying enrollment, code, and
eventDate
- Created `NumericCodeCheckInAction` with 5-step validation pipeline:
existence → date binding → expiry/revoked → max uses → atomic increment
+ delegation to core `CheckInAction`
- Added 4 new `CheckInException` factory methods: `invalidCheckInCode`,
`checkInCodeExpired`, `checkInCodeExhausted`, `checkInCodeWrongDate`
- Translated error messages in en/pt_BR

**Admin panel**
- Added `CheckInCodesRelationManager` on the Event edit page
- Generate codes with 4 or 6 digit length selector, auto-generated
read-only code
- `event_date` defaults to the event's starts_at, constrained to the
event date range
- Revoke action with confirmation (sets `revoked_at`)

**App panel**
- Created `NumericCodeCheckIn` Livewire component embedded in the event
detail page
- Code input visible when enrollment is `confirmed` or `checked_in`
- Validation errors surface as inline messages below the input

**Tests**
- 8 feature tests: valid check-in, invalid code, wrong date, expired,
revoked, max uses exhausted, atomic increment, outside event range

- Pint: passed
- PHPStan: passed (0 errors)
- All events tests: 86/86 passed (0 regressions)

- ADR-0006 documents all design decisions

---------

Co-authored-by: davi.oliveira <davi.oliveira@ernestoborges.com.br>
Co-authored-by: danielhe4rt <danielhe4rt@gmail.com>
…#298)

Implements end-to-end QR code check-in for community events.

When an enrollment reaches `confirmed` status, the system automatically
generates a unique QR token. Organizers scan tokens via the admin panel
to check participants in; participants view and share their QR code from
the app panel.

Closes #237 — blocked on #240 (RSVP enrollment, already merged).

---

- `bacon/bacon-qr-code` — SVG QR generation via `BaconQrCode\Writer` +
`SvgRenderer`.

Chosen over `simplesoftwareio/simple-qrcode` due to PHP 8.4 GD extension
incompatibility and Filament auto-discovery conflicts.

---

| File | Role |
| --- | --- |
| `database/migrations/…create_events_qr_tokens_table.php` |
`events_qr_tokens` table: `enrollment_id`, `token` (unique),
`expires_at` (nullable) |
| `src/CheckIn/Actions/GenerateQrTokenAction.php` | Creates token
(64-char URL-safe hex via `bin2hex(random_bytes(32))`). Idempotent —
skips if token already exists. |
| `src/CheckIn/Actions/QrCheckInAction.php` | Validates token: exists,
belongs to event enrollment, enrollment is `confirmed`/`checked_in`, not
expired, not duplicate for today. Delegates to existing `CheckInAction`
with `method=qr_code`. |
| `src/CheckIn/DTOs/QrCheckInDTO.php` | Input DTO: `token`, `event_date`
|
| `src/CheckIn/Listeners/GenerateQrTokenOnConfirmed.php` | Listens to
`EnrollmentConfirmed` domain event; dispatches `GenerateQrTokenAction`.
Implements `ShouldDispatchAfterCommit` for transaction safety. |
| `src/EventsServiceProvider.php` | Registers `EnrollmentConfirmed →
GenerateQrTokenOnConfirmed` listener |
| `src/CheckIn/Exceptions/CheckInException.php` | Adds
`qrTokenNotFound`, `qrTokenExpired` factory methods |
| `lang/en/check_in.php`, `lang/pt_BR/check_in.php` | i18n strings for
new error states |

---

- `EditEvent` resource:
  - Adds **Scan QR** header action with continuous-scan modal.
- After each scan, the modal auto-reopens for rapid sequential
check-ins.
- Reopen is triggered via a Livewire event (`dispatch` + `#[On]`) so it
runs in a separate request after Filament's `unmountAction` lifecycle
completes.
- Avoids calling `mountAction` directly inside `->after()` because
Filament cleanup silently resets it.
- Field uses `autofocus` so cursor lands on the token input every
reopen.

---

- `EventDetail` Livewire component:
  - Renders enrollment QR code as inline SVG when status is:
    - `confirmed`
    - `checked_in`
  - Adds:
    - copy-token button
    - download-SVG button
    - computed check-in history list
    - "present today" badge based on today's check-in records

---

10 feature tests covering all acceptance criteria:

| Test | Scenario |
| --- | --- |
| `generates_qr_token_on_confirmed_enrollment` | Token created on
`EnrollmentConfirmed` event |
| `does_not_regenerate_existing_token` | Idempotency: second
confirmation doesn't overwrite |
| `valid_qr_scan_creates_check_in` | Happy path, `method=qr_code`,
payload has token |
| `dispatches_participant_checked_in_event` | Domain event fired on
successful scan |
| `rejects_unknown_token` | `qrTokenNotFound` exception |
| `rejects_expired_token` | `expires_at < now()` → `qrTokenExpired` |
| `rejects_duplicate_scan_same_day` | Already checked in today →
`alreadyCheckedIn` |
| `rejects_cancelled_enrollment_token` | Status check gates QR scan |
| `same_token_creates_new_check_in_each_day` | Multi-day reuse creates
separate records |
| `listener_generates_token_after_commit` | `ShouldDispatchAfterCommit`
respected in test |

---

- [x] QR token is auto-generated when enrollment reaches `confirmed`
status
- [x] Token is unique and URL-safe
- [x] Organizer can scan/input token and check participant in
- [x] Same token works across multiple event days (creates new check-in
per day)
- [x] Expired token is rejected
- [x] Duplicate scan on same day is rejected
- [x] Cancelled enrollment's token is rejected on scan (status check)
- [x] Participant sees their QR code in App panel
- [x] Check-in record stores `method=qr_code` with token in payload
- [x] `ParticipantCheckedIn` domain event dispatched
- [x] Feature tests covering:
  - token generation
  - valid scan
  - expired token
  - duplicate scan
  - cancelled enrollment scan
  - multi-day reuse
- [x] Pint passes

---

```bash
make test

vendor/bin/pest app-modules/events/tests/Feature/CheckIn/QrCheckInActionTest.php

make pint
```

Results:

- 74 tests passing
- Pint clean

---

| SHA | Message |
| --- | --- |
| `920f0c8` | `dep(events): adiciona biblioteca bacon/bacon-qr-code para
geração de QR SVG` |
| `1db9d28` | `feat(events/check-in): implementa geração e validação de
token QR por inscrição` |
| `1bd4eb1` | `feat(events): registra listener
GenerateQrTokenOnConfirmed no EventsServiceProvider` |
| `22b920f` | `feat(panel-admin): adiciona ação de scan QR contínuo na
página de edição de evento` |
| `d6be299` | `feat(panel-app): exibe QR code, histórico de check-ins e
badge de presença hoje` |
| `ca9da88` | `test(events/check-in): adiciona suite de testes para
geração de token QR e check-in` |
| `1607145` | `fix(panel-admin): corrige reabertura contínua do modal de
scan QR` |

---------

Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
danielhe4rt and others added 6 commits June 4, 2026 18:05
- EditEvent: drop unnecessary nullsafe on enrollment->user->name (relation is non-null).
- EventDetail/NumericCodeCheckIn: add @property-read annotations so PHPStan
  understands the #[Computed] Livewire properties (matches ProfilePage).
The events table declared tenant_id as foreignId() (bigint) while tenants.id
is a uuid, so migrate:fresh failed with a datatype-mismatch on the FK
constraint. Switch it to foreignUuid() to match the uuid primary key the
Event model already expects.

- CheckIn: @Property checked_in_at is nullable -> Carbon|null (phpdoc sync).
- EnrollUserActionTest: drop redundant uses(RefreshDatabase) that collided
  with the project-wide LazilyRefreshDatabase and fataled the whole suite.
- EventResourceTest: recycle the acting tenant and loadTable() on deferred
  tables so factory events are visible to the tenant-scoped list.
## Summary

Implements automated event closure and admin enrollment status override.
After an event's `ends_at`, a scheduled job resolves all non-terminal
enrollments to `attended` or `no_show`. Organizers can manually override
`no_show → attended` or `confirmed → checked_in` via the admin panel
with a required reason.

Closes #247

---

## What changed

### Event closure pipeline

| File | Role |
|---|---|
| `src/Closure/Actions/CloseEventAction.php` | Core loop: per-enrollment
`DB::transaction` with `lockForUpdate()`. Resolves checked-in
enrollments via `EnrollmentPolicy::resolveAttendance()`, confirmed →
`no_show`. Dispatches `ParticipantAttended` on success. Idempotent —
skips terminal enrollments. |
| `src/Closure/Jobs/ProcessEventClosureJob.php` | `ShouldBeUnique` per
`eventId` (30min lock), 4 tries, backoff `[1,5,10]`. Calls
`CloseEventAction`. |
| `src/Console/Commands/ClosePendingEventsCommand.php` |
`events:close-pending` — queries past events with non-terminal
enrollments, dispatches one job per event. |
| `src/EventsServiceProvider.php` | Schedules command every 15 min with
`withoutOverlapping()`. |
| `src/Closure/Events/ParticipantAttended.php` | Domain event consumed
by gamification. `ShouldDispatchAfterCommit`. Payload: enrollmentId,
eventId, userId, xpRewardOnAttended. |

### Admin status override

| File | Role |
|---|---|
| `src/Closure/Actions/OverrideEnrollmentStatusAction.php` | Separate
action from `TransitionEnrollmentAction`. Allowlist: `no_show→attended`,
`confirmed→checked_in`. Reason required. `triggered_by=admin` in audit
trail. No domain event re-dispatch. |
| `src/Closure/DTOs/OverrideEnrollmentStatusDTO.php` | Input DTO:
enrollment, toStatus, actorId, reason. |
| `src/Closure/Exceptions/OverrideEnrollmentStatusException.php` |
`reasonRequired()`, `overrideNotAllowed(from, to)`. |
| `panel-admin/…/EnrollmentsRelationManager.php` |
`overrideStatusAction()` — warning-colored action visible for
no_show/confirmed. Modal with to_status select (dynamically populated
from the Action's allowlist) + reason textarea. |

### EnrollmentPolicy improvements

| File | Role |
|---|---|
| `src/Enrollment/Models/EnrollmentPolicy.php` |
`resolveAttendance(Enrollment): EnrollmentStatus` — counts check-in days
against `attendance_requirement` (all_days, any_day, minimum_days).
Returns `Attended` or `NoShow`. |
| `src/Enrollment/Enums/EnrollmentStatus.php` | Removed unused
`getIcon()` + `HasIcon` interface — dead code. |

### EventForm (panel-admin)

| File | Role |
|---|---|
| `panel-admin/…/EventForm.php` | Attendance requirement fields: radio
for requirement type, conditional minimum_days slider. Hidden for
single-day events. |

### Architecture improvements

- Override allowlist now exposed as
`OverrideEnrollmentStatusAction::allowedTargetsFor(EnrollmentStatus):
list<EnrollmentStatus>` — single source of truth consumed by both
validation and UI. `EnrollmentsRelationManager` no longer hardcodes
allowed transitions.

---

## ADRs

- `app-modules/events/docs/adr/0007-event-closure-as-scheduled-job.md`
-
`app-modules/events/docs/adr/0008-admin-status-override-as-separate-action.md`

---

## Test plan
- **CloseEventActionTest** (6 tests): all_days requirement, any_day,
no-show, idempotency, multi-enrollment
- **ClosePendingEventsCommandTest** (4 tests): past/future event job
dispatch, multiple events
- **ProcessEventClosureJobTest** (5 tests): closure, config
(tries/backoff/uniqueFor via reflection), non-existent event, failure
logging
- **OverrideEnrollmentStatusActionTest** (8 tests): allowed transitions,
reason required, invalid pairs, `allowedTargetsFor()`
- **EnrollmentPolicyTest** (5 tests): all_days, any_day, minimum_days
- **EventResourceTest**: EventForm attendance requirement fields
- [x] PHPStan: 0 errors
- [x] Pint: clean

---

## Commits

| SHA | Message |
|---|---|
| `0ec3d42b` | `docs(events): adrs and context map` |
| `a2e0a803` | `feat(events): module structure (#249)` |
| `779f7598` | `feat(events): rsvp enrollment end-to-end (#275)` |
| `f631ea47` | `feat(events): waitlist and capacity enforcement (#294)`
|
| `a51f4b7b` | `feat(events): numeric code check-in (#297)` |
| `8b3d3c41` | `feat(events): qr code check-in (#298)` |
| `c08f5042` | `pint + deps` |
| `30ef42b3` | `fix(events): resolve phpstan errors in check-in UI` |
| `d9a7f098` | `fix(events): correct tenant_id FK to UUID` |
| `022f471d` | `feat(enrollment): resolveAttendance()` |
| `da47e5c1` | `feat(panel_admin): override EnrollmentStatus` |
| `747dc31d` | `feat(events): schedule ClosePendingEventsCommand` |
| `4f6c111f` | `chore(exceptions): translations` |
| `d47fe97a` | `docs(adr): ADRs for issue #247` |
| `36ca23b4` | `feat(events): close-pending command` |
| `325d2840` | `feat(panel-admin): attendance requirements on EventForm`
|
| `5d389511` | `feat(events): EnrollmentPolicyFactory fields` |
| `0fdf456e` | `feat(events): CloseEventAction` |
| `87e959a6` | `feat(events): OverrideEnrollmentStatusAction` |
| `821ef139` | `feat(events): OverrideEnrollmentStatusDTO` |
| `534a2840` | `feat(events): ProcessEventClosureJob` |
| `ed4b5b60` | `feat(events): OverrideEnrollment exceptions` |
| `f9dfa7fd` | `feat(events): ParticipantAttended event + tests` |
| `601efcca` | `chore(deps): package-lock.json` |
| `2e5f05fb` | `test(events): job config via reflection` |
| `4000f2ee` | `refactor(events): expose allowlist as queryable method`
|
| `06703c7b` | `chore(events): remove unused getIcon()` |
## Summary

Implements the **Events-module side** of bot-triggered check-in
(Discord/Twitch). The bot dispatches a `CheckInRequested` domain event;
Events listens (`HandleBotCheckIn`), resolves the user and enrollment,
processes the check-in by reusing the numeric-code validation, and
dispatches a `CheckInProcessed` response so the bot can reply to the
member in chat. The bot is pure transport; Events owns the rules
(ADR-0005).

**Flow:** `CheckInRequested` → `HandleBotCheckIn` →
`NumericCodeCheckInAction` → `CheckInProcessed`

  Closes #248

  ---
  
  ## What changed — `app-modules/events`

  | File | Role |
  |---|---|
| `src/CheckIn/Events/CheckInRequested.php` | Inbound domain event
dispatched by the bot. Payload: `provider`, `externalUserId`, `code`,
`tenantId`, `eventId?` (reserved, see below), `channelContext`. |
| `src/CheckIn/Events/CheckInProcessed.php` | Outbound response event
for the bot to reply in chat. Payload: `provider`, `externalUserId`,
`status`, `message`, `channelContext`, `enrollmentId?`. |
| `src/CheckIn/Enums/BotCheckInStatus.php` | `Success` / `Error` status
for the response event. |
| `src/CheckIn/Listeners/HandleBotCheckIn.php` | Resolves the user via
Identity (`FindExternalIdentity`), resolves the enrollment, delegates to
`NumericCodeCheckInAction`, dispatches `CheckInProcessed`. Catches
`ExternalIdentityException` / `CheckInException` → error response. |
| `src/CheckIn/Exceptions/CheckInException.php` | Adds
`botNoActiveEnrollment()` and `botMultipleActiveEvents()` factories. |
| `lang/en/check_in.php`, `lang/pt_BR/check_in.php` | Bot-facing
messages (user not linked, no active enrollment, success) + reworded
multiple-events message. |
| `src/EventsServiceProvider.php` | Registers `CheckInRequested →
HandleBotCheckIn`. |

  ### Event resolution (the important bit)

The listener finds the user's check-in-able enrollments
(`confirmed`/`checked_in`) for a published event happening today, scoped
to the tenant. When the user has **more than one** active event the same
day (e.g. a morning and an evening online event), the **typed code
identifies which event** — each `CheckInCode` belongs to a single event.

This replaces an earlier **date-only** resolution that errored on *any*
2-same-day case, which would have wrongly blocked the legitimate
morning+evening scenario. The single-event flow is unchanged; only a
genuine collision (same code value on two same-day events the user is
enrolled in) returns an ambiguity error pointing to the panel.

  ---

  ## Acceptance criteria

  - [x] `CheckInRequested` defined with proper payload
  - [x] Events listener resolves the user and processes the check-in
  - [x] `CheckInProcessed` response dispatched with success/error status
- [x] Numeric code validation applies the same rules as the web flow
(delegates to `NumericCodeCheckInAction`)
- [x] Error cases handled: user not linked, not enrolled, invalid code,
already checked in, ambiguous code
- [x] Listener registered via `EventsServiceProvider` — **manual
registration** (Laravel event auto-discovery is disabled app-wide;
matches the existing `GenerateQrTokenOnConfirmed` pattern)
  - [x] Feature tests (see Test plan)
  - [x] Pint passes

  ---

  ## Test plan

  ```bash
  # Bot check-in tests
vendor/bin/pest
app-modules/events/tests/Feature/HandleBotCheckInTest.php

  # Full events module (must stay green)
  vendor/bin/pest app-modules/events/tests
  
  # Style + static analysis
  make pint && make phpstan
  ```

  Results:
  - `HandleBotCheckInTest`: **11/11**
  - Events module: **133/133** (0 regressions)
  - Pint: clean · PHPStan: **0 errors** · Rector (dry-run): clean

Scenarios covered: success + success response · unlinked user · invalid
code · no active enrollment · event not today · multiple events → typed
code selects the right one · multiple events + non-matching code →
invalid · same code on two events → ambiguity error · already checked in
today · waitlisted enrollment ignored.

  ---

  ## Out of scope (separate issues)

- The **bot-side command** that dispatches `CheckInRequested`
(`app-modules/bot-discord/`) — separate concern, per the issue.
- **Rate limiting** on the bot path — deferred to the bot command (the
web flow already throttles 5/60s).
- **Explicit event selection** (`eventId` / "path A") — the field stays
reserved in the payload but is not honored; the typed code already
resolves the event.
@BrunaDomingues BrunaDomingues self-requested a review June 30, 2026 03:05
BrunaDomingues and others added 3 commits July 7, 2026 13:16
## Summary

POC para validar traduções no admin de eventos, com commits separados
por escopo.

- Adiciona seletor de idioma EN/PT-BR no menu do usuário do Filament
(estilo theme switcher), com persistência em sessão
- Formata datas do Filament conforme o locale ativo (ex.: `29/06/2026`
em PT-BR)
- Traduz labels do resource de eventos no `panel-admin` (tabelas,
formulários, relation managers e notificações)

## Commits

1. `feat(i18n): add Filament user menu locale switcher` — menu +
infraestrutura de locale (revertível isoladamente)
2. `feat(i18n): format Filament dates by application locale` — Carbon +
formatos no Filament
3. `feat(panel-admin): translate events Filament resource labels` —
arquivos `lang/events.php` e resources

## Notas

- O commit do locale switcher foi isolado de propósito: se não for para
produção, dá para reverter só ele com `git revert <hash>`.
- Enum values (ex.: status da inscrição) já usavam `events::enums.*`;
este PR cobre principalmente strings hardcoded do admin.
- Requer assets do Vite atualizados (`npm run dev` ou `npm run build`)
para o estilo do seletor de idioma.

## Test plan

- [ ] Trocar idioma no menu do usuário (admin e app) e recarregar a
página
- [ ] Verificar labels do resource de eventos em PT-BR (Inscrições,
Participante, etc.)
- [ ] Verificar formato de datas em PT-BR (`d/m/Y H:i:s`) vs EN (`M j, Y
H:i:s`)
- [ ] `php artisan test tests/Feature/LocaleSwitcherTest.php`
- [ ] `php artisan test
app-modules/events/tests/Feature/EventResourceTest.php`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary

Implements the Application enrollment method: the participant fills a
dynamic form defined by the organizer, the enrollment lands as
`pending`, and the organizer approves or rejects it from the Admin
panel. Reuses `EnrollUserAction` as the shared entrypoint (RSVP path
untouched) and adds two new domain actions for the approval/rejection
decision.

Flow: participant submits form → `EnrollUserAction` (pending, no
capacity check) → Admin approves/rejects → `ApproveApplicationAction`
(capacity check + confirm/waitlist) or `RejectApplicationAction` (reason
+ rejected)

Closes #237

## What changed — app-modules/events

| File | Role |
|---|---|
| `src/Enrollment/Actions/EnrollUserAction.php` | Application path:
creates `pending` enrollment, validates `application_data` against the
policy's schema (per-type: `required`, `select`/`checkbox` options),
skips capacity check on submission. |
| `src/Enrollment/Actions/ApproveApplicationAction.php` | Validates
status is `pending`, capacity check with `lockForUpdate`, transitions to
`confirmed`/`waitlisted` (or throws if full with no waitlist),
dispatches `EnrollmentConfirmed`/`EnrollmentWaitlisted`, audit trail
(`triggered_by=admin`, `actor_id`). |
| `src/Enrollment/Actions/RejectApplicationAction.php` | Validates
status is `pending`, stores `rejection_reason`, transitions to
`rejected`, audit trail with reason. |
| `src/Enrollment/DTOs/ApproveApplicationDTO.php`,
`RejectApplicationDTO.php` | New DTOs for the two admin actions. |
| `src/Enrollment/DTOs/EnrollUserDTO.php` | Adds `applicationData`
payload. |
| `src/Enrollment/Exceptions/EnrollmentException.php` | Adds
`enrollmentNotPending()` and `applicationDataInvalid()` factories. |
| `src/Enrollment/Models/Enrollment.php`, `EnrollmentPolicy.php` |
Corrects `@property` typing for `application_data` /
`application_schema` (list, not associative array) — removes a PHPStan
type-mismatch cascade in the new validation code. |
| `database/factories/EnrollmentPolicyFactory.php`,
`database/seeders/EventsSeeder.php` | `application()` factory state +
seed data covering all 4 field types
(`text`/`textarea`/`select`/`checkbox`) across two Application-method
events. |
| `lang/{en,pt_BR}/{exceptions,pages}.php` | New user-facing strings. |

## What changed — app-modules/panel-admin

| File | Role |
|---|---|
| `Filament/Resources/Events/Schemas/EventForm.php` | Repeater-based
form builder for `application_schema` (type/label/required/options),
visible only when `enrollment_method = Application`. |
|
`Filament/Resources/Events/RelationManagers/Actions/ApproveApplicationAction.php`,
`RejectApplicationAction.php` | Table row actions — approve
(confirmation modal) / reject (reason textarea) — call into the domain
actions. |
|
`Filament/Resources/Events/RelationManagers/EnrollmentsRelationManager.php`
| Wires the two actions plus a "View Application" modal on
pending/decided rows. |
| `resources/views/enrollments/application-data.blade.php` | Read-only
rendering of submitted answers (boolean, array, and scalar aware). |

## What changed — app-modules/panel-app

| File | Role |
|---|---|
| `Livewire/Events/EventDetail.php` | `canApply` computed prop,
`apply()` action, pre-initializes checkbox-group state as arrays in
`mount()`. |
| `resources/views/livewire/events/event-detail.blade.php` | Dynamic
form rendered per field type; `pending`/`rejected` status views showing
the submitted answers / rejection reason. |

## Design notes (the non-obvious bits)

**`checkbox` = multi-select group, not a single boolean.** The issue's
schema (`{type, label, required, options?}`) doesn't spell this out, so
it was ambiguous whether `checkbox` meant a single yes/no toggle or a
group of checkboxes sharing one `options` list (like `select`, but
multi-value). Went with the multi-select reading — `checkbox` now shows
the same `options` builder as `select` in the Admin, and stores an array
of selected values. Validation treats `required` as "at least one
selected" and checks each selection against `options`.

**Livewire needs the array pre-seeded.** Multiple `<input
type="checkbox" wire:model="applicationFormData.{index}" value="...">`
bound to the same array path only get grouped into an array by
Livewire's front end if that path is *already* an array before the first
interaction — otherwise every checkbox in the group ends up sharing one
boolean and checking one checks them all. `EventDetail::mount()` now
seeds `applicationFormData[$index] = []` for every `checkbox` field up
front.

**Filament enum-cast gotcha.**
`Select::make('enrollment_method')->options(EnrollmentMethod::class)`
auto-applies an `EnumStateCast`, so `$get('enrollment_method')` inside
`visible()` closures returns the enum *instance*, not `->value`. The
Repeater's visibility check was comparing against the string value and
silently never showed — fixed by comparing against the enum case
directly.

## Acceptance criteria

- [x] Organizer can define a dynamic form schema via Filament form
builder in Admin
- [x] Participant sees and fills the dynamic form in App panel
- [x] Submission creates enrollment with status `pending` and
`application_data` populated
- [x] Organizer can approve → enrollment transitions to `confirmed` (or
`waitlisted` if full)
- [x] Organizer can reject with reason → enrollment transitions to
`rejected` with reason stored
- [x] Capacity is checked on approval, not on submission
- [x] Application data is displayed in Admin enrollment detail
- [x] Transition audit trail records approver/rejector identity
- [x] Feature tests: submit application, approve, reject, approve when
full (waitlist), approve when full (no waitlist)
- [x] Pint passes

## Test plan

```
# Application enrollment tests
vendor/bin/pest app-modules/events/tests/Feature/EnrollUserActionTest.php \
  app-modules/events/tests/Feature/Enrollment/ApproveApplicationActionTest.php \
  app-modules/events/tests/Feature/Enrollment/RejectApplicationActionTest.php \
  app-modules/panel-app/tests/Feature/Events/ApplicationEnrollmentTest.php

# Full events + panel-app suite (must stay green)
vendor/bin/pest app-modules/events/tests app-modules/panel-app/tests

# Style
vendor/bin/pint --test app-modules/events app-modules/panel-admin app-modules/panel-app
```

Results:
- Application enrollment tests: 34/34
- events + panel-app suite: 181/181 (0 regressions)
- Pint: clean

Scenarios covered: submit → pending + audit trail · missing
`applicationData` → exception · missing required field → exception · no
schema defined → pending · event past → rejected · approve → confirmed +
audit trail · approve at capacity + waitlist → waitlisted · approve at
capacity, no waitlist → event full exception · approve non-pending →
exception · no capacity limit → always confirms · reject with reason →
rejected · reject non-pending → exception · reject preserves
`application_data` · apply button rendering ·
`canApply`/`canConfirmPresence` flags · pending status shows submitted
answers · rejected shows reason · already applied → no apply button ·
missing required field → error notification.

**Note on PHPStan:** the module already carried ~172 pre-existing errors
before this branch (untyped test-helper array params, Pest fluent-call
false positives from Larastan) — out of scope here. This PR's own new
type mismatch (`EnrollUserAction::validateApplicationData` against the
`application_schema`/`application_data` docblocks) was fixed; remaining
new findings are the same pre-existing categories, consistent with every
other test file in the suite.

## Out of scope (separate issues)

- Admin bulk actions on pending applications (filter/view exist; bulk
approve/reject not implemented).
- Editing a submitted application before it's reviewed.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: danielhe4rt <danielhe4rt@gmail.com>
# Conflicts:
#	CONTEXT-MAP.md
#	app-modules/events/database/factories/EventFactory.php
#	composer.json
#	composer.lock
#	package-lock.json
@danielhe4rt danielhe4rt marked this pull request as ready for review July 7, 2026 17:43
@danielhe4rt danielhe4rt requested a review from a team July 7, 2026 17:43
The 4.x merge brought stricter Pint config and PHPStan level 7. Adapt the
events module code accordingly:

- Fix 12 PHPStan errors: typed form-data extraction via Arr::string(),
  list<EnrollmentStatus> return, redundant nullsafe removal, typed
  check-in date, aligned application data key types.
- Apply Pint fixes carried over from the merge hook (static closures,
  named boolean args, numeric separators, missing @param/@return docs).
AdryanneKelly and others added 2 commits July 9, 2026 11:16
## Summary

**The bug:** reordering questions in the Admin's application-form
Repeater silently corrupted already-submitted answers, showing them
under the wrong question.

**Root cause:** an answer in `application_data` was matched to its
question in `application_schema` by array position —
`application_data[0]` was "whatever the answer to schema position 0
was." The Repeater re-indexes `application_schema` to the new visual
order on every save. So if a participant answered while the schema was
`[A, B]` (answer stored at index `0` = "answer to A") and the organizer
later reordered to `[B, A]`, index `0` now points to B — that
participant's answer to A renders as if it were the answer to B, in both
the Admin's "View Application" modal and the App panel's "Your Answers"
section.

**Fix:** each question in the schema now carries a hidden, immutable
`key` (UUID), generated once when the question is created. Filament's
Repeater moves the whole item (including this hidden field) as a unit
when reordering, so the key never changes — only its position does.
Submission, validation, and both answer displays now match answer to
question by that `key` instead of array position.

**Follow-up fix (second commit):** enrollment policies saved before the
`key` field existed (or edited outside the Admin form builder) can still
have schema entries with no key. Instead of crashing with "Undefined
array key" when that happens, those fields are now silently skipped —
consistent with how the answer-display side already treated a missing
key as "no answer."

## What changed

| File | Change |
|---|---|
| `EventForm.php` | Added a hidden `key` (UUID) field to each Repeater
item in the application-schema builder, generated once on creation. |
| `EnrollUserAction.php` | `validateApplicationData()` now matches
submitted answers to schema fields by `key` instead of position;
`application_data`/`applicationData` typed as `array<string, mixed>`. |
| `EnrollUserDTO.php`, `Enrollment.php` | Docblock types updated to
match (`array<string, mixed>` keyed by field id, not array index). |
| `EventDetail.php` | The apply form's `wire:model` bindings and the
checkbox-group state pre-seed in `mount()` now key off `$field['key']`;
fields missing a key are skipped instead of crashing. |
| `event-detail.blade.php` | Same re-keying for the "Your Answers"
pending-status display and the apply form; skips fields with no key. |
| `EnrollmentsRelationManager.php` | Admin's "View Application" modal
now looks up each answer by `$field['key']`. |
| `EventsSeeder.php` | Seed schemas and fake-answer generation updated
to the key-based shape. |
| `EnrollUserActionTest.php`, `ApproveApplicationActionTest.php`,
`RejectApplicationActionTest.php`, `ApplicationEnrollmentTest.php` |
Updated fixtures/assertions to key-based `application_data`. |

## Test plan

```
vendor/bin/pest app-modules/events/tests app-modules/panel-app/tests app-modules/panel-admin/tests
vendor/bin/pint --test app-modules/events app-modules/panel-admin app-modules/panel-app
```

Results: 230/230 passing, Pint clean.

Manual verification: submitted an application, reordered the questions
in Admin and saved, confirmed the original answers still show under the
correct question in both the App panel ("Your Answers") and Admin ("View
Application").

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
## Why

`#402` landed on `4.x` (conventions, tooling, tests). This brings the
`feat/events` long-lived integration branch up to date so sub-work PR'd
into it inherits the current standards.

## What

- Merge `origin/4.x` into `feat/events`.
- **composer.json**: auto-merged to `4.x` (correct `^1.0.0` module
  constraints + package bumps), keeping events' `bacon/bacon-qr-code`.
- **composer.lock**: regenerated from `4.x`'s lock, re-adding
  `bacon/bacon-qr-code` (+ `dasprid/enum`) — the only events-specific
  external dependency.

## Notes

Only conflict was `composer.lock` (alphabetical reordering from a new
`4.x` package). `composer validate` passes. CI runs on this PR.

---------

Co-authored-by: Bruna Domingues Leite <brunadomingues.dev@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: danielhe4rt <danielhe4rt@gmail.com>
Co-authored-by: BrunaDomingues <bruna@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

8 participants