Skip to content

feat(onboarding): fundar módulo + máquina de estados via Welcome (step form)#381

Open
sirelves wants to merge 4 commits into
feat/squadsfrom
feat/343-onboarding-welcome
Open

feat(onboarding): fundar módulo + máquina de estados via Welcome (step form)#381
sirelves wants to merge 4 commits into
feat/squadsfrom
feat/343-onboarding-welcome

Conversation

@sirelves

Copy link
Copy Markdown

Closes #343

O que foi feito

Estabelece o módulo de domínio onboarding com a máquina de estados polimórfica por tipo (enum → handler), exercitada pelo tipo mais simples (Welcome).

  • ServiceProvider em src/OnboardingServiceProvider.php (carrega as migrations do módulo)
  • Migrations (datas Tz):
    • onboardingstenant_id, user_id, type, status, completed_at, paused_at + UNIQUE (tenant_id, user_id, type)
    • onboarding_stepsonboarding_id, step_key, status, data jsonb, completed_at + UNIQUE (onboarding_id, step_key)
  • Models Onboarding/OnboardingStep com @property PHPDoc, #[Table] e #[UseFactory]
  • Enums backed string: OnboardingType (Welcome, Squads), OnboardingStatus, OnboardingStepStatus
  • Contrato OnboardingFlow (steps(), prerequisites(), advance(), isComplete()); OnboardingType::handler() resolve o flow
  • WelcomeOnboardingFlow com step único form
  • Action StartOnboarding (tenant-scoped, idempotente via firstOrCreate + transação)
  • Docs: ADR-0001 (decisão polimórfica), CONTEXT.md e registro no CONTEXT-MAP.md

Critérios de aceite

  • ServiceProvider em src/
  • Migrations via módulo, datas Tz, PHPDoc sincronizado
  • UNIQUE (tenant_id, user_id, type)
  • OnboardingType::Welcome->handler()WelcomeOnboardingFlow
  • StartOnboarding cria onboarding in_progress + step form pending
  • Testes de inicialização e idempotência

Cenários BDD → testes

  • "Início cria o estado persistido (form/pending)" → StartOnboardingTest
  • "Não duplica onboarding do mesmo tipo" → StartOnboardingTest
  • (extra) Avanço do form conclui o onboarding Welcome → WelcomeOnboardingFlowTest

Validação local

Rodado contra Postgres real (docker-compose up):

  • ✅ Pint
  • ✅ PHPStan (level 7, checkImplicitMixed) — 0 erros
  • ✅ Pest — 5/5 (16 assertions)

Fora do escopo (issues futuras)

SquadsOnboardingFlow, enforce de prerequisites no start, e o listener de GithubPullRequestApproved — documentados como deferidos na ADR-0001 e no CONTEXT.md.

…p form)

Estabelece o módulo de domínio `onboarding` com a máquina de estados
polimórfica por tipo, exercitada pelo tipo mais simples (`Welcome`).

- ServiceProvider em `src/` (carrega migrations do módulo)
- Migrations `onboardings` e `onboarding_steps` com datas `Tz` e
  UNIQUE (tenant_id, user_id, type) / (onboarding_id, step_key)
- Models `Onboarding`/`OnboardingStep` com @Property, #[Table] e #[UseFactory]
- Enums backed: OnboardingType (Welcome, Squads), OnboardingStatus, OnboardingStepStatus
- Contrato OnboardingFlow (steps/prerequisites/advance/isComplete);
  OnboardingType::handler() resolve o flow
- WelcomeOnboardingFlow com step único `form`
- Action StartOnboarding (tenant-scoped, idempotente)
- Testes: inicialização, idempotência e avanço/conclusão do flow Welcome
- Docs: ADR-0001, CONTEXT.md e registro no CONTEXT-MAP.md

Closes #343
@sirelves sirelves requested a review from a team June 30, 2026 13:52
@sirelves sirelves added type:feat New feature mod:onboarding Onboarding entry layer (pre-triage, APTO gate) difficulty:easy 1-2 days labels Jun 30, 2026
@sirelves sirelves added this to the Onboarding milestone Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sirelves, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 7a3cab87-e2f3-4ff7-872a-5019154ed73c

📥 Commits

Reviewing files that changed from the base of the PR and between 227e6f5 and 7b505a3.

📒 Files selected for processing (4)
  • app-modules/onboarding/src/Actions/StartOnboarding.php
  • app-modules/onboarding/src/Enums/OnboardingType.php
  • app-modules/onboarding/src/Flows/WelcomeOnboardingFlow.php
  • app-modules/onboarding/tests/Feature/WelcomeOnboardingFlowTest.php
📝 Walkthrough

Walkthrough

Introduces the onboarding bounded context as a new Laravel module. The OnboardingServiceProvider is relocated from src/Providers/ to src/ and composer.json is updated accordingly. Two migrations create onboardings and onboarding_steps tables with UUID PKs, foreign keys, unique constraints, and timezone-aware timestamps. Onboarding and OnboardingStep Eloquent models are added with relationships and enum casts. OnboardingStatus, OnboardingStepStatus, and OnboardingType string-backed enums are defined. The OnboardingFlow interface contract and WelcomeOnboardingFlow implementation are added, along with the StartOnboarding action. Feature tests cover creation, idempotency, and flow advancement.

Possibly related issues

Possibly related PRs

  • he4rt/heartdevs.com#332: Touches onboarding's composer.json and Laravel service provider registration — the same file modified in this PR to relocate the provider class.

Suggested reviewers

  • gvieira18
  • Clintonrocha98
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title names the onboarding module and Welcome state-machine work, matching the PR’s main change.
Description check ✅ Passed The description is directly about the onboarding module, Welcome flow, migrations, models, and tests in this changeset.
Linked Issues check ✅ Passed The summarized code covers the provider move, migrations, models, enums, flow contract, Welcome flow, StartOnboarding, and tests required by #343.
Out of Scope Changes check ✅ Passed No clear unrelated code changes are shown beyond the onboarding module setup, flow, and tests required by the issue.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…lcome

# Conflicts:
#	CONTEXT-MAP.md
#	app-modules/onboarding/composer.json
#	composer.json
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The head commit changed during the review from 7a81fc7 to 227e6f5.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/343-onboarding-welcome

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

O metadado em cache do pacote path no composer.lock ainda apontava
He4rt\Onboarding\Providers\OnboardingServiceProvider (FQN antigo, de
quando #332 criou o provider em src/Providers/). Como a CI roda
composer install (lê do lock), package:discover falhava com
'Class not found'. Atualiza o lock para He4rt\Onboarding\OnboardingServiceProvider.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
app-modules/onboarding/tests/Feature/StartOnboardingTest.php (1)

33-43: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Add a concurrent case for this idempotency test Sequential calls only cover the happy path; they don’t exercise contention on the unique indexes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/onboarding/tests/Feature/StartOnboardingTest.php` around lines 33
- 43, The idempotency test for StartOnboarding only covers sequential calls, so
add a concurrent execution case that exercises contention against the unique
indexes. Update the existing StartOnboardingTest around the
StartOnboarding::handle flow to simulate two overlapping starts for the same
Tenant, User, and OnboardingType::Welcome, then assert only one Onboarding and
one OnboardingStep are created and both calls resolve to the same record.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app-modules/onboarding/src/Actions/StartOnboarding.php`:
- Around line 20-36: Make StartOnboarding idempotent under concurrent requests:
the current DB::transaction wrapper around Onboarding::query()->firstOrCreate()
and $onboarding->steps()->firstOrCreate() still allows a race where two requests
both miss the initial read and one fails on the unique index. Update the
StartOnboarding action to handle duplicate-key races explicitly by reusing the
existing Onboarding/step record after a unique-constraint hit, or by using an
atomic upsert/locking approach around the Onboarding and OnboardingStep creation
path so concurrent starts return the same records instead of throwing.

In `@app-modules/onboarding/src/Enums/OnboardingType.php`:
- Around line 15-23: The OnboardingType enum currently exposes an unsupported
Squads option even though OnboardingType::handler() returns null for it, which
causes StartOnboarding::handle() to create a stuck onboarding. Remove or hide
the unsupported case from OnboardingType so only types with a valid
OnboardingFlow handler are exposed, and ensure handler() only resolves supported
types like Welcome.

In `@app-modules/onboarding/src/Flows/WelcomeOnboardingFlow.php`:
- Around line 24-39: The WelcomeOnboardingFlow::advance method is overwriting
the onboarding completion timestamp on every repeated call once the flow is
already complete. Update the logic so the onboarding record is only marked
Completed and assigned completed_at if it is not already completed, while still
allowing the pending step lookup/update to run as needed. Use the existing
isComplete method and the onboarding->update block as the main place to guard
against rewriting completed_at.

In `@app-modules/onboarding/src/Models/Onboarding.php`:
- Around line 34-67: The Onboarding model is missing mass-assignment
configuration, so `StartOnboarding::handle()` and
`WelcomeOnboardingFlow::advance()` will fail when creating or updating records
through `firstOrCreate()` and `update()`. Add explicit mass-assignable handling
on `Onboarding` by defining either `$fillable` or `$guarded` so the attributes
used by those flows can be safely persisted, and keep the existing `tenant()`,
`user()`, `steps()`, and `casts()` behavior unchanged.

In `@app-modules/onboarding/src/Models/OnboardingStep.php`:
- Around line 29-49: The OnboardingStep model currently has no mass-assignment
rules, so the firstOrCreate and update calls on this model will hit
MassAssignmentException. Add explicit mass-assignable configuration on
OnboardingStep by defining either $fillable or $guarded for the attributes used
by StartOnboarding::handle() and WelcomeOnboardingFlow::advance(), and make sure
the casted fields like status, data, and completed_at are included as needed.

In `@app-modules/onboarding/tests/Feature/WelcomeOnboardingFlowTest.php`:
- Around line 13-27: The Welcome onboarding test only verifies completion after
a single advance and misses the repeated-advance behavior in
WelcomeOnboardingFlow::advance, where calling it again after completion can
rewrite completed_at. Extend the WelcomeOnboardingFlowTest to advance the same
onboarding twice and assert the persisted completed_at remains unchanged after
the second call, using StartOnboarding, WelcomeOnboardingFlow, and the
onboarding refresh checks to locate the flow and the timestamp assertion.

---

Nitpick comments:
In `@app-modules/onboarding/tests/Feature/StartOnboardingTest.php`:
- Around line 33-43: The idempotency test for StartOnboarding only covers
sequential calls, so add a concurrent execution case that exercises contention
against the unique indexes. Update the existing StartOnboardingTest around the
StartOnboarding::handle flow to simulate two overlapping starts for the same
Tenant, User, and OnboardingType::Welcome, then assert only one Onboarding and
one OnboardingStep are created and both calls resolve to the same record.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 1397753b-1d0d-452c-84d4-9c54f96073bd

📥 Commits

Reviewing files that changed from the base of the PR and between 8b53011 and 227e6f5.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • app-modules/onboarding/composer.json
  • app-modules/onboarding/database/factories/.gitkeep
  • app-modules/onboarding/database/factories/OnboardingFactory.php
  • app-modules/onboarding/database/factories/OnboardingStepFactory.php
  • app-modules/onboarding/database/migrations/.gitkeep
  • app-modules/onboarding/database/migrations/2026_06_25_161812_create_onboardings_table.php
  • app-modules/onboarding/database/migrations/2026_06_25_161813_create_onboarding_steps_table.php
  • app-modules/onboarding/src/Actions/StartOnboarding.php
  • app-modules/onboarding/src/Contracts/OnboardingFlow.php
  • app-modules/onboarding/src/Enums/OnboardingStatus.php
  • app-modules/onboarding/src/Enums/OnboardingStepStatus.php
  • app-modules/onboarding/src/Enums/OnboardingType.php
  • app-modules/onboarding/src/Flows/WelcomeOnboardingFlow.php
  • app-modules/onboarding/src/Models/Onboarding.php
  • app-modules/onboarding/src/Models/OnboardingStep.php
  • app-modules/onboarding/src/OnboardingServiceProvider.php
  • app-modules/onboarding/src/Providers/OnboardingServiceProvider.php
  • app-modules/onboarding/tests/Feature/.gitkeep
  • app-modules/onboarding/tests/Feature/StartOnboardingTest.php
  • app-modules/onboarding/tests/Feature/WelcomeOnboardingFlowTest.php
💤 Files with no reviewable changes (1)
  • app-modules/onboarding/src/Providers/OnboardingServiceProvider.php

Comment thread app-modules/onboarding/src/Actions/StartOnboarding.php Outdated
Comment thread app-modules/onboarding/src/Enums/OnboardingType.php
Comment thread app-modules/onboarding/src/Flows/WelcomeOnboardingFlow.php
Comment thread app-modules/onboarding/src/Models/Onboarding.php
Comment on lines +29 to +49
final class OnboardingStep extends Model
{
/** @use HasFactory<OnboardingStepFactory> */
use HasFactory;
use HasUuids;

/** @return BelongsTo<Onboarding, $this> */
public function onboarding(): BelongsTo
{
return $this->belongsTo(Onboarding::class);
}

/** @return array<string, mixed> */
protected function casts(): array
{
return [
'status' => OnboardingStepStatus::class,
'data' => 'array',
'completed_at' => 'datetime',
];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Declare mass-assignable attributes on this model.

StartOnboarding::handle() seeds the first step with firstOrCreate(), and WelcomeOnboardingFlow::advance() completes it with update([...]). This class also extends Illuminate\Database\Eloquent\Model directly without $fillable or $guarded, so both writes will fail with MassAssignmentException.

Diff
 final class OnboardingStep extends Model
 {
     /** `@use` HasFactory<OnboardingStepFactory> */
     use HasFactory;
     use HasUuids;
+
+    protected $fillable = [
+        'onboarding_id',
+        'step_key',
+        'status',
+        'data',
+        'completed_at',
+    ];
 
     /** `@return` BelongsTo<Onboarding, $this> */
     public function onboarding(): BelongsTo
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final class OnboardingStep extends Model
{
/** @use HasFactory<OnboardingStepFactory> */
use HasFactory;
use HasUuids;
/** @return BelongsTo<Onboarding, $this> */
public function onboarding(): BelongsTo
{
return $this->belongsTo(Onboarding::class);
}
/** @return array<string, mixed> */
protected function casts(): array
{
return [
'status' => OnboardingStepStatus::class,
'data' => 'array',
'completed_at' => 'datetime',
];
}
final class OnboardingStep extends Model
{
/** `@use` HasFactory<OnboardingStepFactory> */
use HasFactory;
use HasUuids;
protected $fillable = [
'onboarding_id',
'step_key',
'status',
'data',
'completed_at',
];
/** `@return` BelongsTo<Onboarding, $this> */
public function onboarding(): BelongsTo
{
return $this->belongsTo(Onboarding::class);
}
/** `@return` array<string, mixed> */
protected function casts(): array
{
return [
'status' => OnboardingStepStatus::class,
'data' => 'array',
'completed_at' => 'datetime',
];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/onboarding/src/Models/OnboardingStep.php` around lines 29 - 49,
The OnboardingStep model currently has no mass-assignment rules, so the
firstOrCreate and update calls on this model will hit MassAssignmentException.
Add explicit mass-assignable configuration on OnboardingStep by defining either
$fillable or $guarded for the attributes used by StartOnboarding::handle() and
WelcomeOnboardingFlow::advance(), and make sure the casted fields like status,
data, and completed_at are included as needed.

Comment thread app-modules/onboarding/tests/Feature/WelcomeOnboardingFlowTest.php
…ance idempotente)

- OnboardingType::handler() retorna OnboardingFlow não-nulável e lança
  LogicException para tipos sem implementação (ex.: Squads), evitando
  um onboarding 'in_progress' travado sem steps.
- StartOnboarding resolve o flow antes de gravar (fail-fast) e remove o
  ramo morto de null-check.
- WelcomeOnboardingFlow::advance() não reescreve completed_at em chamadas
  repetidas após a conclusão.
- Testes: advance idempotente preserva completed_at; iniciar tipo sem
  handler falha e não persiste nada.

MassAssignment apontado pelo CodeRabbit é falso-positivo: a app usa
Model::unguard() global (AppServiceProvider) e nenhum model do repo
declara $fillable/$guarded.
@sirelves

Copy link
Copy Markdown
Author

Triagem do feedback do CodeRabbit

Aplicado (commit 7b505a3f):

  • OnboardingType::handler() retornava null para Squads → onboarding travado (Major): agora retorna OnboardingFlow não-nulável e lança LogicException para tipos sem implementação. StartOnboarding resolve o flow antes de gravar (fail-fast), então iniciar um tipo sem handler falha sem persistir nada.
  • WelcomeOnboardingFlow::advance() reescrevia completed_at em chamadas repetidas (Major): adicionado guard ($advanced && status !== Completed && isComplete()).
  • Testes novos cobrindo ambos os casos.

Rejeitado — falso-positivo:

  • MassAssignmentException nos models Onboarding/OnboardingStep (2× Critical): a aplicação chama Model::unguard() globalmente em app/Providers/AppServiceProvider.php:57, e nenhum model do repositório declara $fillable/$guarded. Declarar $fillable aqui iria contra a convenção estabelecida. Os testes criam e atualizam ambos os models contra Postgres real e passam.

Não aplicado — trade-off consciente:

  • firstOrCreate não-atômico sob concorrência: firstOrCreate é o idioma idiomático do Laravel e a constraint UNIQUE (tenant_id, user_id, type) garante integridade (sem duplicatas). Hardening contra corrida (upsert/lock) é fora do escopo da feat(onboarding): fundar módulo + máquina de estados via Welcome (step form) #343 e adicionaria complexidade sem problema medido — pode virar issue futura se a ação for exposta a double-submit real.
  • Teste de idempotência concorrente (nitpick): não-determinístico em Pest; o caso sequencial já cobre o critério de aceite.

Validação local: Pint ✅ · PHPStan (level 7) 0 erros ✅ · Pest 7/7 ✅

@stherzada

Copy link
Copy Markdown
Contributor

@Clintonrocha98 @gvieira18

@gvieira18 gvieira18 changed the base branch from 4.x to feat/squads June 30, 2026 19:08
@Clintonrocha98

Copy link
Copy Markdown
Member

Durante a revisão apareceram dois pontos que são de fundação (afetam as próximas issues, não só este PR), então levei a discussão e uma proposta de direção para a issue #341 em vez de resolver aqui:

  • o flow aceitar qualquer onboarding no advance (nada garante que o flow certo opera sobre o onboarding certo);
  • a ordem entre os steps ser decidida pela data de criação (oldest()), e não pela ordem declarada em steps().

Detalhes e proposta aqui: #341 (comment)

Vale alinhar lá antes de mergear, porque a decisão pode mudar o StartOnboarding e o formato do advance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

difficulty:easy 1-2 days mod:onboarding Onboarding entry layer (pre-triage, APTO gate) type:feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(onboarding): fundar módulo + máquina de estados via Welcome (step form)

3 participants