Skip to content

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

Open
YuriSouzaDev wants to merge 7 commits into
he4rt:feat/eventsfrom
YuriSouzaDev:feat/events-qr-code-check-in
Open

feat(events): implement qr code check-in for event enrollments (#237)#298
YuriSouzaDev wants to merge 7 commits into
he4rt:feat/eventsfrom
YuriSouzaDev:feat/events-qr-code-check-in

Conversation

@YuriSouzaDev
Copy link
Copy Markdown
Contributor

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: new Scan QR header action with a continuous-scan modal. After each scan the modal auto-reopens for rapid sequential check-ins. The reopen is triggered via a Livewire event (dispatch + #[On]) so it runs in a separate request, after Filament's own unmountAction lifecycle completes — calling mountAction directly inside ->after() was being silently undone by that cleanup. Field has autofocus so the cursor lands on the token input on every reopen.

App panel — app-modules/panel-app

  • EventDetail Livewire component: renders the enrollment's QR code as inline SVG when status is confirmed or checked_in. Includes copy-token and download-SVG buttons. Adds computed check-in history list and a "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: generate token, 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

All 74 tests pass. 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

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.

3 participants