Skip to content

feat(events): implement qr code check-in for event enrollments (#237)#298

Merged
danielhe4rt merged 9 commits into
he4rt:feat/eventsfrom
YuriSouzaDev:feat/events-qr-code-check-in
Jun 4, 2026
Merged

feat(events): implement qr code check-in for event enrollments (#237)#298
danielhe4rt merged 9 commits into
he4rt:feat/eventsfrom
YuriSouzaDev:feat/events-qr-code-check-in

Conversation

@YuriSouzaDev
Copy link
Copy Markdown
Contributor

@YuriSouzaDev YuriSouzaDev commented May 31, 2026

Summary

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).


What changed

New dependency

  • 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.


Core — app-modules/events

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

Admin panel — app-modules/panel-admin

  • 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.

App panel — app-modules/panel-app

  • 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

Tests — app-modules/events/tests

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

Acceptance criteria

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

Test plan

# Run full suite (must stay green)
make test

# Run only QR check-in tests
vendor/bin/pest app-modules/events/tests/Feature/CheckIn/QrCheckInActionTest.php

# Code style
make pint

Results:

  • 74 tests passing
  • Pint clean

Commits

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

… inscrição

- Adiciona GenerateQrTokenAction: gera token único URL-safe de 64 chars por enrollment,
  idempotente — retorna token existente se já criado
- Adiciona QrCheckInAction: valida token, enrollment e data do evento antes de delegar
  ao CheckInAction existente (method=qr_code, payload com token)
- Adiciona QrCheckInDTO com validações de token e ator no construtor
- Adiciona GenerateQrTokenOnConfirmed listener para o evento EnrollmentConfirmed
- Adiciona exceções qrTokenNotFound e qrTokenExpired em CheckInException
- Adiciona strings de tradução (en/pt_BR) para os novos erros de check-in QR
…erviceProvider

Conecta EnrollmentConfirmed → GenerateQrTokenOnConfirmed para geração automática
do token QR quando a inscrição é confirmada.
…ção de evento

- Botão "Scan QR" no cabeçalho do EditEvent abre modal com campo de token
- Executa QrCheckInAction e exibe notificação de sucesso com nome do participante
- Em caso de erro (CheckInException ou Throwable), exibe notificação de erro
- Modal reabre automaticamente via ->after() para leituras contínuas sem fechar
- Botão de submit renomeado para "Check In" para evitar ambiguidade com confirmações
…sença hoje

- EventDetail: adiciona computed qrToken (visível apenas com método QrCode e status
  confirmed/checked_in), qrCodeSvg via BaconQrCode, checkIns (histórico ordenado) e
  hasCheckedInToday para badge condicional
- View: seção "Meu QR Code" com SVG inline, botões de copiar token e baixar SVG
- Badge "Check-in feito hoje" no cabeçalho do card quando há check-in no dia atual
- Seção de histórico de check-ins lista datas confirmadas abaixo do QR
- Inscrição sem check-in ainda exibe instruções sem histórico
- Adiciona strings de tradução en/pt_BR para histórico e badge
… QR e check-in

Cobre todos os critérios de aceite da task he4rt#237:
- Geração de token único por enrollment
- Idempotência: segunda chamada retorna o mesmo token
- Scan válido: cria CheckIn, atualiza status e dispara ParticipantCheckedIn
- Token inexistente rejeitado
- Token de outro evento rejeitado
- Token expirado rejeitado
- Scan duplicado no mesmo dia rejeitado
- Token de enrollment cancelado rejeitado
- Reutilização em dias diferentes cria check-ins distintos
- Listener gera token automaticamente via EnrollUserAction (integração)
`mountAction` chamado em `->after()` era desfeito pelo `unmountAction`
do lifecycle do Filament logo em seguida. A solução despacha um evento
Livewire (`reopen-scan-qr`) dentro do `finally` da action, que é
processado num request separado — após o lifecycle atual completar —
e reabre o modal via `#[On]` sem conflito.

Também adiciona `autofocus` no campo token para o cursor já estar
posicionado a cada reabertura.
Copy link
Copy Markdown
Member

@davicbtoliveira davicbtoliveira left a comment

Choose a reason for hiding this comment

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

LGTM

GenerateQrTokenAction stored the raw Uuid object, leaving the in-memory
token attribute as a LazyUuidFromString that broke QrCheckInDTO's string
type hint. Persist it via Str::uuid7()->toString() instead.

- Update QrCheckInActionTest to assert a valid UUID rather than a 64-char length.
- Pass a waitlist position in EnrollmentStatusTest to match getResponseMessage().
- Drop unused DetachAction import in EnrollmentsRelationManager (Pint).
@danielhe4rt danielhe4rt merged commit 70d5a53 into he4rt:feat/events Jun 4, 2026
1 check passed
danielhe4rt added a commit that referenced this pull request Jun 4, 2026
…#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>
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.

4 participants