Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0ec3d42
docs(events): adrs and context map
danielhe4rt May 17, 2026
a2e0a80
feat(events): module structure (#249)
GabrielFVDev May 19, 2026
779f759
feat(events): rsvp enrollment end-to-end (#275)
BrunaDomingues May 22, 2026
0febd14
feat(events): implement manual check-in (#278)
davicbtoliveira May 23, 2026
f631ea4
feat(events): waitlist and capacity enforcement (#294)
BrunaDomingues Jun 4, 2026
a51f4b7
feat(events): implement numeric code check-in (#297)
davicbtoliveira Jun 4, 2026
8b3d3c4
feat(events): implement qr code check-in for event enrollments (#237…
YuriSouzaDev Jun 4, 2026
c08f504
pint + deps
danielhe4rt Jun 4, 2026
30ef42b
fix(events): resolve phpstan errors in check-in UI
danielhe4rt Jun 4, 2026
d9a7f09
fix(events): correct tenant_id FK to UUID and unblock test suite
danielhe4rt Jun 4, 2026
c2fa725
feat(events): event closure job and admin status override (#312)
davicbtoliveira Jun 7, 2026
98342e9
feat(events): bot check-in integration (#326)
GabrielFVDev Jun 14, 2026
c8a1cc4
chore(events): enum tricks
danielhe4rt Jun 21, 2026
7b9f85b
POC: i18n no admin de eventos (locale switcher + traduções) (#384)
BrunaDomingues Jul 7, 2026
162c4de
feat(events): application enrollment with form builder (#383)
AdryanneKelly Jul 7, 2026
06844b2
Merge remote-tracking branch 'origin/4.x' into feat/events
danielhe4rt Jul 7, 2026
f3acdc2
style(events): apply pint formatting and phpstan type fixes post-merge
danielhe4rt Jul 7, 2026
be200a8
Merge branch '4.x' into feat/events
AdryanneKelly Jul 8, 2026
c45e252
fix(events): key application answers by a stable field id (#396)
AdryanneKelly Jul 9, 2026
3f5bf89
chore(events): sync 4.x into feat/events (#403)
gvieira18 Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions CONTEXT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ This is a modular monorepo (`internachi/modular`). Each bounded context lives un
| Bot Discord | `app-modules/bot-discord/` | Discord bot runtime (Laracord websocket, slash commands, event handlers) |
| Integration Discord | `app-modules/integration-discord/` | Discord platform transport (REST API via Saloon), OAuth, ETL |
| Identity | `app-modules/identity/` | Users, tenants, external identities, authentication |
| Events | `app-modules/events/` | Event participation lifecycle — enrollment, check-in, attendance, XP dispatch |
| Gamification | `app-modules/gamification/` | Character progression — XP, levels, badges, seasons, daily bonuses |
| Panel Admin | `app-modules/panel-admin/` | Filament admin panel — dashboards, resources, moderation UI, marketing |
| Integration Twitch | `app-modules/integration-twitch/` | Twitch platform transport (Helix API via Saloon), OAuth, EventSub webhooks |
| Integration GitHub | `app-modules/integration-github/` | GitHub transport (REST via Saloon), OAuth, community contribution ingestion (backfill + webhooks) + event lake |
Expand All @@ -27,10 +29,24 @@ This is a modular monorepo (`internachi/modular`). Each bounded context lives un
┌─────────────────┐ ┌──────────────────────┐
│ Bot Discord │ │ Integration Discord │
│ (runtime/ws) │────────▶│ (transport/rest) │
└────────┬────────┘ └──────────┬───────────┘
│ │
│ listens to events │ provides DiscordConnector
▼ │
└───┬─────────┬───┘ └──────────┬───────────┘
│ │ │
│ │ dispatches │ provides DiscordConnector
│ │ CheckInRequested │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Events │ │
│ │ (participation) │────────────┼───── publishes domain events
│ └────────┬────────┘ │ │
│ │ │ ▼
│ │ reads users │ ┌─────────────────┐
│ │ │ │ Gamification │
│ │ │ │ (XP/levels) │
│ │ │ └────────┬────────┘
│ │ │ │
│ listens │ │ │ reads users
│ to events │ │ │
▼ ▼ │ ▼
┌─────────────────┐ │
│ Moderation │◀───────────────────┘
│ (domain core) │
Expand All @@ -46,10 +62,12 @@ This is a modular monorepo (`internachi/modular`). Each bounded context lives un
### Dependency rules

- **Moderation** is platform-agnostic. It never imports from `bot-discord`, `integration-discord`, or `integration-twitch`.
- **Bot Discord** depends on Moderation (listens to domain events) and Integration Discord (uses transport).
- **Bot Discord** depends on Moderation (listens to domain events) and Integration Discord (uses transport) and Events (dispatches check-in domain events)..
- **Integration Discord** depends on Identity (OAuth user resolution). It never imports from Moderation.
- **Integration Twitch** depends on Identity (OAuth user resolution, ExternalIdentity for tenant linking). It never imports from Moderation, Integration Discord, or Bot Discord.
- **Integration GitHub** depends on Identity (OAuth user resolution; future `Character` seam via `ExternalIdentity`). It never imports from Activity, Economy, Moderation or any Bot/runtime module — it only emits the `GithubContributionRecorded` domain event. The community presentation (in `portal`) and the allowlist admin UI (in `panel-admin`) depend on it, never the reverse.
- **Identity** has no upstream dependencies on other contexts listed here.
- **Events** depends on Identity (reads Users and Tenants). Publishes domain events consumed by Gamification.
- **Gamification** depends on Identity (Character belongs to User). Listens to Events domain events for XP.
- **Onboarding** depends on Identity (User, tenant scoping, GitHub `ExternalIdentity` link) and listens to `integration-github`'s `GithubPullRequestApproved` domain event (reads the `challenge` repos in the allowlist). It never imports from `squads` — `squads` is a consumer of its completion gate, never the reverse.
- **Squads** depends on Onboarding (reads the `Squads`-completion gate, "APTO") and Identity (users/tenants). It never imports from presentation; the panels depend on it.
74 changes: 74 additions & 0 deletions app-modules/events/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Events — Bounded Context

## Purpose

Manages event participation lifecycle: enrollment, check-in, attendance tracking, and XP reward dispatching. Covers in-person meetups, workshops, and multi-day conferences.

## Glossary

| Term | Definition | Not to be confused with |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| **Event** | A scheduled gathering (meetup, workshop, or conference) owned by a tenant. Defined by type, date range, location, and an enrollment policy. | "Event" as in Laravel Event (domain event) — use "domain event" for those. |
| **Enrollment** | A single record representing one user's relationship with one event. Progresses through a strict state machine from entry to terminal state. One enrollment per (user, event). | "Registration" — we don't use this term. |
| **Enrollment Policy** | A 1:1 configuration record attached to an event. Defines enrollment method, capacity, check-in method, waitlist behavior, cancellation deadline, XP rewards, and application form schema. | "Event settings" — policy specifically governs participation rules. |
| **Enrollment Method** | How a user enters an event. One of: `rsvp` (1-click), `rsvp_checkin` (1-click + mandatory presence verification), `application` (form submission + organizer approval). | |
| **Check-in** | Verification of physical/virtual presence at an event. One record per (enrollment, date). Methods: `manual`, `numeric_code`, `qr_code`. | "Enrollment" — check-in proves presence, enrollment proves intent. |
| **Numeric Code** | A short-lived code announced by the organizer (projected on screen or spoken in stream). Bound to a specific event date. Has expiration window and optional max uses. Also used by Discord/Twitch bots as check-in trigger. | |
| **QR Token** | A unique token generated per enrollment. Encoded in a QR code on the participant's badge/screen. Reusable across event days — each scan creates a new check-in record for that day. | |
| **Waitlist** | Ordered queue of enrollments when event capacity is full. FIFO promotion when a confirmed participant cancels. | |
| **Attendance Requirement** | Policy rule defining how many days of check-in are needed to achieve `attended` status. Values: `all_days`, `any_day`, `minimum_days(N)`. | |
| **Transition** | An auditable state change on an enrollment. Every transition is recorded with actor, timestamp, and reason. Written by application code (Actions), never by database triggers. | |
| **No-show** | Terminal state for a participant who confirmed but never checked in. Assigned automatically by a scheduled job after the event ends. No systemic consequences in MVP (future: may affect eligibility). | |
| **Application** | An enrollment method where the participant submits a dynamic form (JSONB schema defined in policy) and waits for organizer approval. Results in `pending → confirmed` or `pending → rejected`. | "Submission" (CFP) — application is for participation, submission is for presenting (out of MVP scope). |

## State Machine — Enrollment

```
[entry]
├─ application → pending
│ ├─ approve() → confirmed
│ ├─ reject() → rejected (TERMINAL)
│ └─ cancel() → cancelled (TERMINAL)
├─ rsvp/rsvp_checkin + capacity available → confirmed
└─ rsvp/rsvp_checkin + full → waitlisted
├─ slot_opens() → confirmed
└─ cancel() → cancelled (TERMINAL)

confirmed
├─ check_in() → checked_in
├─ cancel(pre-deadline) → cancelled (TERMINAL)
└─ [job] event_ended → no_show (TERMINAL)

checked_in
└─ [job] event_ended + attendance_requirement met → attended (TERMINAL · SUCCESS)
```

**States:** `pending`, `confirmed`, `waitlisted`, `checked_in`, `attended`, `cancelled`, `rejected`, `no_show`

**Terminal states:** `attended` (success), `cancelled`, `rejected`, `no_show`

## Actors

| Actor | Panel | Capabilities |
| --------------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
| **Organizer** | Admin (`/admin`) | Creates events, configures policies, approves/rejects applications, manual check-in, status override |
| **Participant** | App (`/app`) | Enrolls (RSVP/application), checks in (code/QR), cancels, views own events |

## Module Boundaries

- **Events → Gamification**: Events dispatches domain events (`EnrollmentConfirmed`, `ParticipantCheckedIn`, `ParticipantAttended`). Gamification listens and awards XP. Events does not know how XP works.
- **Bot Discord → Events**: Bot dispatches domain events (e.g., `CheckInRequested`). Events module listens and processes. Bot is transport, Events owns the rules.
- **Events → Identity**: Events reads User and Tenant models. No writes to Identity.

## Out of Scope (MVP)

- Networking between participants
- Referral / invite links
- Magic link and geolocation check-in
- Sponsors association
- Timeline / activity feed (listeners ready, no consumer yet)
- Call for Papers / Submissions (CFP)
- Agenda / schedule display
- Paid events / payment integration
- No-show penalties
32 changes: 32 additions & 0 deletions app-modules/events/database/factories/CheckInCodeFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace He4rt\Events\Database\Factories;

use He4rt\Events\CheckIn\Models\CheckInCode;
use He4rt\Events\Event\Models\Event;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Date;

/** @extends Factory<CheckInCode> */
final class CheckInCodeFactory extends Factory
{
protected $model = CheckInCode::class;

public function definition(): array
{
$startsAt = Date::now();

return [
'event_id' => Event::factory(),
'event_date' => Date::today(),
'code' => fake()->numerify('######'),
'starts_at' => $startsAt,
'expires_at' => $startsAt->clone()->addHours(2),
'max_uses' => null,
'uses_count' => 0,
'revoked_at' => null,
];
}
}
28 changes: 28 additions & 0 deletions app-modules/events/database/factories/CheckInFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace He4rt\Events\Database\Factories;

use He4rt\Events\CheckIn\Enums\CheckInMethod;
use He4rt\Events\CheckIn\Models\CheckIn;
use He4rt\Events\Enrollment\Models\Enrollment;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Date;

/** @extends Factory<CheckIn> */
final class CheckInFactory extends Factory
{
protected $model = CheckIn::class;

public function definition(): array
{
return [
'enrollment_id' => Enrollment::factory(),
'event_date' => Date::today(),
'method' => fake()->randomElement(CheckInMethod::cases()),
'payload' => null,
'checked_in_at' => Date::now(),
];
}
}
35 changes: 35 additions & 0 deletions app-modules/events/database/factories/EnrollmentFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace He4rt\Events\Database\Factories;

use He4rt\Events\Enrollment\Enums\EnrollmentStatus;
use He4rt\Events\Enrollment\Models\Enrollment;
use He4rt\Events\Event\Models\Event;
use He4rt\Identity\User\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/** @extends Factory<Enrollment> */
final class EnrollmentFactory extends Factory
{
protected $model = Enrollment::class;

public function definition(): array
{
return [
'event_id' => Event::factory(),
'user_id' => User::factory(),
'status' => EnrollmentStatus::Pending,
'is_public' => true,
'waitlist_position' => null,
'application_data' => null,
'rejection_reason' => null,
'enrolled_at' => null,
'confirmed_at' => null,
'checked_in_at' => null,
'attended_at' => null,
'cancelled_at' => null,
];
}
}
53 changes: 53 additions & 0 deletions app-modules/events/database/factories/EnrollmentPolicyFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace He4rt\Events\Database\Factories;

use He4rt\Events\CheckIn\Enums\CheckInMethod;
use He4rt\Events\Enrollment\Enums\AttendanceRequirement;
use He4rt\Events\Enrollment\Enums\EnrollmentMethod;
use He4rt\Events\Enrollment\Models\EnrollmentPolicy;
use He4rt\Events\Event\Models\Event;
use Illuminate\Database\Eloquent\Factories\Factory;

/** @extends Factory<EnrollmentPolicy> */
final class EnrollmentPolicyFactory extends Factory
{
protected $model = EnrollmentPolicy::class;

public function definition(): array
{
$requirement = fake()->randomElement(AttendanceRequirement::cases());

return [
'event_id' => Event::factory(),
'enrollment_method' => fake()->randomElement(EnrollmentMethod::cases()),
'check_in_method' => fake()->randomElement(CheckInMethod::cases()),
'capacity' => fake()->optional()->numberBetween(10, 200),
'has_waitlist' => false,
'attendance_requirement' => $requirement,
'minimum_days' => $requirement === AttendanceRequirement::MinimumDays ? 1 : null,
'cancellation_deadline_hours' => null,
'xp_on_confirmed' => 0,
'xp_on_checked_in' => 0,
'xp_on_attended' => 0,
'application_schema' => null,
];
}

public function rsvp(): static
{
return $this->state(fn (): array => [
'enrollment_method' => EnrollmentMethod::Rsvp,
]);
}

public function application(?array $schema = null): static
{
return $this->state(fn (): array => [
'enrollment_method' => EnrollmentMethod::Application,
'application_schema' => $schema,
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace He4rt\Events\Database\Factories;

use He4rt\Events\Enrollment\Enums\EnrollmentStatus;
use He4rt\Events\Enrollment\Enums\TriggeredBy;
use He4rt\Events\Enrollment\Models\Enrollment;
use He4rt\Events\Enrollment\Models\EnrollmentTransition;
use Illuminate\Database\Eloquent\Factories\Factory;

/** @extends Factory<EnrollmentTransition> */
final class EnrollmentTransitionFactory extends Factory
{
protected $model = EnrollmentTransition::class;

public function definition(): array
{
return [
'enrollment_id' => Enrollment::factory(),
'from_status' => null,
'to_status' => EnrollmentStatus::Pending,
'actor_id' => null,
'triggered_by' => TriggeredBy::User,
'reason' => null,
'metadata' => null,
];
}
}
Loading