Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 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
22 changes: 13 additions & 9 deletions CONTEXT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ 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 |
| 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 |
| 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 |
| 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 |
| Onboarding | `app-modules/onboarding/` | Universal, mandatory entry layer — polymorphic onboarding state machines by type; owns the per-type completion gate (APTO) |
| Squads | `app-modules/squads/` | Squad lifecycle, membership and governance (captain/sub-captain, elections, removal, reallocation) |

## Relationships

Expand Down Expand Up @@ -49,3 +51,5 @@ This is a modular monorepo (`internachi/modular`). Each bounded context lives un
- **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.
- **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.
75 changes: 75 additions & 0 deletions app-modules/onboarding/CONTEXT.md
Comment thread
danielhe4rt marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Onboarding Context

The universal, mandatory entry layer of the ecosystem. Owns the polymorphic onboarding state
machines that a person walks through, and the per-type **completion** status that other contexts
consume as an access gate. Today it powers community entry (`Welcome`) and squad entry (`Squads`),
but it is designed so new onboarding types are added without touching consumers.

## Glossary

| Term | Definition | Not to be confused with |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| **Onboarding** | One person's journey through one typed flow, scoped to a tenant. One row per `(tenant, user, type)`. Holds lifecycle `status`, not the per-step detail. | The UI wizard (presentation) — the Onboarding is the persisted state machine |
| **OnboardingType** | The discriminator enum (`Welcome`, `Squads`, …). Resolves the polymorphic behaviour via `handler(): OnboardingFlow` — same idiom as `IdentityProvider::getClient`. | A step — a type _has_ steps |
Comment thread
danielhe4rt marked this conversation as resolved.
| **OnboardingFlow** | The per-type handler (a class behind the `OnboardingFlow` contract). Declares `steps()`, `prerequisites()`, `advance()`, `isComplete()`. All type-specific rules live here. | The model — the Flow is stateless behaviour; the Onboarding is the state |
| **OnboardingStep** | One auditable stage of a flow, one row in `onboarding_steps`. Carries its own `status` + `data` (jsonb) + `completed_at`. Enables pause/resume and history. | A prerequisite (another _type_ that must be complete first) |
| **Prerequisite** | Another `OnboardingType` that must be **completed** before this one can start (e.g. `Squads` requires `Welcome`). Declared by the flow, enforced on start. | A step (intra-type) — a prerequisite is inter-type |
| **APTO** | Domain shorthand for "completed the `Squads` onboarding". The condition that unlocks squad candidacy and squad creation. It is _not_ a global flag — it is `Squads` completion. | A generic "active member" — APTO is specifically squad-eligible |
| **Challenge** | The Git step of the `Squads` flow: open a PR (with the mandatory template) on a `challenge` repo; a human reviewer approves it on GitHub. Curation happens entirely on GitHub. | A contribution (the gamification record) — challenge repos do **not** award XP |
| **Gate** | A pre-condition checked at a transition without being a step of its own. The `Squads` flow gates `git_challenge` on having a linked GitHub `ExternalIdentity`. | A step — a gate produces no `onboarding_steps` row |

## State machine (shape)

`status` is a generic lifecycle: `in_progress` · `paused` · `completed` · `rejected`. The _steps_ are
type-specific and defined by the flow. The `Squads` flow:

```
[prereq: Welcome completed?]
│ yes
step: form ──(submit, auto-advance, no curation)──► [gate: GitHub linked?]
│ no -> blocked + CTA "link GitHub"
│ yes
step: git_challenge ──(PR approved on challenge repo)──► completed = APTO
```

Pause/resume is orthogonal: `status = paused` + `paused_at`, resumable from the current step.

## Structure (proposed)

```
src/
├── Models/ ← Onboarding · OnboardingStep
├── Enums/ ← OnboardingType · OnboardingStatus · OnboardingStepStatus
├── Contracts/ ← OnboardingFlow
├── Flows/ ← WelcomeOnboardingFlow · SquadsOnboardingFlow
├── Actions/ ← StartOnboarding · AdvanceStep · PauseOnboarding · ResumeOnboarding
├── DTOs/ ← per-step payload contracts (validated by the flow)
└── Listeners/ ← GithubPullRequestApproved -> advance the challenge step
```

## Module Boundaries

### This module owns:

- The onboarding state machines (one polymorphic model + steps) and their lifecycle.
- The per-type **completion** status other modules read as a gate (`Onboarding::isCompleted(user, tenant, type)`).
- The inter-type prerequisite chain.

### This module does NOT own:

- Squad lifecycle, membership, governance — belongs to `squads` (a consumer of the gate).
- Any HTTP communication with GitHub or the raw event lake — belongs to `integration-github`. This
module **listens to** `GithubPullRequestApproved` and reads `GithubRepository` (`purpose = challenge`).
- GitHub account linking — belongs to `identity` (`ExternalIdentity`, provider `github`).

## Dependencies

- **Identity** — `User`, tenant scoping, and the GitHub `ExternalIdentity` link (the `git_challenge` gate).
- **Integration GitHub** — consumes the `GithubPullRequestApproved` domain event and the `challenge`
repo allowlist. Never the reverse.
- Presentation (`panel-app`) drives the UI and calls the module's Actions.

See `docs/adr/0001-onboarding-polimorfico-por-tipo.md` and
`docs/adr/0002-sinal-de-pr-aprovado-via-evento-de-dominio.md`.
27 changes: 27 additions & 0 deletions app-modules/onboarding/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "he4rt/onboarding",
"description": "",
"type": "library",
"version": "1.0.0",
"license": "proprietary",
"autoload": {
"psr-4": {
"He4rt\\Onboarding\\": "src/",
"He4rt\\Onboarding\\Database\\Factories\\": "database/factories/",
"He4rt\\Onboarding\\Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"He4rt\\Onboarding\\Tests\\": "tests/"
}
},
"minimum-stability": "stable",
"extra": {
"laravel": {
"providers": [
"He4rt\\Onboarding\\Providers\\OnboardingServiceProvider"
]
}
}
}
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# ADR-0001: Onboarding polimórfico por tipo, com etapas auditáveis

**Status:** Accepted
**Date:** 2026-06-15
**Deciders:** danielhe4rt

## Contexto

Os Squads da He4rt rodam hoje no informal (grupos de WhatsApp, sem liderança formal). A primeira
entrega de software ataca **governança** (capitão/subcapitão, eleição, etc.) e, antes dela, uma
camada de **Entrada** que filtra quem realmente quer integrar a comunidade e contribuir.

Essa Entrada — chamada no documento da P.O. de "pré-triagem" — é **universal e obrigatória**:
ninguém se candidata a um squad nem propõe um squad novo sem concluí-la. Duas perguntas de fronteira
apareceram:

1. **Onde mora?** A pré-triagem é sobre pertencer à comunidade, não a um squad específico, e reusa
pesado o `identity` (vínculo GitHub via `ExternalIdentity`). Colocá-la dentro de `squads`
amarraria um conceito universal a um consumidor específico.
Comment thread
danielhe4rt marked this conversation as resolved.
Outdated
2. **Quantos formatos?** O time já enxerga **mais de um tipo de entrada** — `Welcome` (entrada na
comunidade) e `Squads` (entrada no programa) — e quer outros no futuro, cada um com seu próprio
contrato de payload e processamento.

Modelar a pré-triagem como uma máquina de estados fixa (form → desafio) resolveria o `Squads` de
hoje, mas não comportaria novos tipos sem refator.

## Decisão

**Criar um módulo de domínio novo, `onboarding`, dono de máquinas de onboarding polimórficas por
tipo.** `squads` (e futuros consumidores) apenas leem o gate de conclusão.

### Polimorfismo (enum → handler)

- `OnboardingType` (enum) discrimina o tipo e resolve o comportamento via `handler(): OnboardingFlow`
— mesmo idioma que `IdentityProvider::getClient()` já usa no `identity`.
- `OnboardingFlow` (contrato) declara `steps()`, `prerequisites()`, `advance()`, `isComplete()`.
Toda regra específica do tipo vive no handler; nenhum consumidor conhece os tipos concretos.

### Persistência (modelo + etapas)

Modelo único discriminado por `type` + tabela de etapas (opção "C" avaliada):

`onboardings` — uma linha por `(tenant_id, user_id, type)`:

| Coluna | Tipo | Notas |
| -------------- | ---------------------------- | --------------------------------------------------------------------- |
| `id` | uuid (PK) | `HasUuids` |
| `tenant_id` | uuid (FK) | tenant-scoped (convenção do repo, multi-tenant-ready) |
| `user_id` | uuid (FK) | |
| `type` | string | `OnboardingType` (`welcome` \| `squads` \| …) |
| `status` | string | ciclo de vida genérico: `in_progress`/`paused`/`completed`/`rejected` |
| `completed_at` | timestamptz? | |
| `paused_at` | timestamptz? | |
| timestamps | tz | |
| UNIQUE | `(tenant_id, user_id, type)` | |

`onboarding_steps` — uma linha por etapa do fluxo:

| Coluna | Tipo | Notas |
| --------------- | --------------------------- | ------------------------------------------------- |
| `id` | uuid (PK) | |
| `onboarding_id` | uuid (FK) | |
| `step_key` | string | semântica do handler (`form`, `git_challenge`, …) |
| `status` | string | `pending`/`done`/… |
| `data` | jsonb | payload da etapa, validado pelo DTO do tipo |
| `completed_at` | timestamptz? | |
| timestamps | tz | |
| UNIQUE | `(onboarding_id, step_key)` | |

A tabela de etapas (em vez de só um `payload` JSON no modelo) foi escolhida por dar **auditoria e
histórico por etapa de graça** — relevante pro desafio Git, que tem reenvio com evolução e
pausa/retoma.

### Cadeia entre tipos

`prerequisites()` declara dependências **inter-tipo**: `Squads` exige `Welcome` concluído para poder
iniciar. O gate que o `squads` consome é `Onboarding::isCompleted(user, tenant, Squads)` — apelidado
de **APTO** no domínio.

## Alternativas consideradas

- **Pré-triagem dentro de `identity`** (membership): conceitualmente limpo, mas mistura onboarding
evolutivo com o núcleo de autenticação e força `identity` a depender de `integration-github`.
- **Pré-triagem dentro de `squads`**: entrega rápida, mas amarra um conceito universal a um consumidor
e exigiria migração quando outro módulo (eventos, etc.) quiser o mesmo gate.
- **STI / modelo por tipo**: Laravel não tem STI nativo (exige pacote/boilerplate), foge do idioma
`enum→resolve` do repo e incha o schema com colunas nuláveis por tipo. Descartado.
Comment thread
danielhe4rt marked this conversation as resolved.
- **Modelo único só com `payload` JSON (sem tabela de etapas)**: mais simples, mas perde auditoria
por etapa. É o passo anterior natural; promovido para a tabela de etapas por causa do desafio Git.

## Consequências

### Positivas

- Somar um tipo novo = +1 case no enum + 1 classe `Flow`. Consumidores intactos.
- Auditoria etapa-a-etapa nativa (início/fim de cada etapa, tentativas de reenvio).
- Pausa/retoma natural (estado vive na etapa + `status=paused`).
- Núcleo do jogo (futuro) pode virar mais um tipo, ou consumir o gate, sem acoplar.

### Negativas / diferidas

- `status`/`step_key` são strings genéricas — a disciplina de transição fica no handler, não no banco.
- Dois lugares de verdade (`onboardings` + `onboarding_steps`); o `isComplete()` precisa ser a única
fonte que decide conclusão para não divergirem.
- Validação do payload é só de aplicação (DTO), não de schema.

## Review trigger

Revisitar quando (a) o Núcleo do jogo for refinado e a gente decidir se ele é um `OnboardingType` ou
um consumidor do gate; ou (b) surgir um tipo cujo estado de etapa não caiba no par modelo+steps.
Loading