Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 29 additions & 11 deletions CONTEXT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ This is a modular monorepo (`internachi/modular`). Each bounded context lives un

## Contexts

| Context | Path | Description |
| ------------------- | ---------------------------------- | --------------------------------------------------------------------------- |
| Moderation | `app-modules/moderation/` | Content moderation pipeline — classification, routing, enforcement, appeals |
| 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 |
| Context | Path | Description |
| ------------------- | ---------------------------------- | ----------------------------------------------------------------------------- |
| Moderation | `app-modules/moderation/` | Content moderation pipeline — classification, routing, enforcement, appeals |
| 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 |

Expand All @@ -19,10 +21,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 @@ -38,7 +54,9 @@ 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.
- **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.
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,
];
}
}
43 changes: 43 additions & 0 deletions app-modules/events/database/factories/EnrollmentPolicyFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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
{
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' => AttendanceRequirement::AllDays,
'minimum_days' => 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,
]);
}
}
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,
];
}
}
49 changes: 0 additions & 49 deletions app-modules/events/database/factories/EventAgendaFactory.php

This file was deleted.

Loading