feat(onboarding): fundar módulo + máquina de estados via Welcome (step form)#381
feat(onboarding): fundar módulo + máquina de estados via Welcome (step form)#381sirelves wants to merge 4 commits into
Conversation
…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
|
Warning Review limit reached
Next review available in: 22 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Repository YAML (base), Central YAML (inherited) Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughIntroduces the onboarding bounded context as a new Laravel module. The Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
…lcome # Conflicts: # CONTEXT-MAP.md # app-modules/onboarding/composer.json # composer.json
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
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.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
app-modules/onboarding/tests/Feature/StartOnboardingTest.php (1)
33-43: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueAdd 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
⛔ Files ignored due to path filters (1)
composer.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
app-modules/onboarding/composer.jsonapp-modules/onboarding/database/factories/.gitkeepapp-modules/onboarding/database/factories/OnboardingFactory.phpapp-modules/onboarding/database/factories/OnboardingStepFactory.phpapp-modules/onboarding/database/migrations/.gitkeepapp-modules/onboarding/database/migrations/2026_06_25_161812_create_onboardings_table.phpapp-modules/onboarding/database/migrations/2026_06_25_161813_create_onboarding_steps_table.phpapp-modules/onboarding/src/Actions/StartOnboarding.phpapp-modules/onboarding/src/Contracts/OnboardingFlow.phpapp-modules/onboarding/src/Enums/OnboardingStatus.phpapp-modules/onboarding/src/Enums/OnboardingStepStatus.phpapp-modules/onboarding/src/Enums/OnboardingType.phpapp-modules/onboarding/src/Flows/WelcomeOnboardingFlow.phpapp-modules/onboarding/src/Models/Onboarding.phpapp-modules/onboarding/src/Models/OnboardingStep.phpapp-modules/onboarding/src/OnboardingServiceProvider.phpapp-modules/onboarding/src/Providers/OnboardingServiceProvider.phpapp-modules/onboarding/tests/Feature/.gitkeepapp-modules/onboarding/tests/Feature/StartOnboardingTest.phpapp-modules/onboarding/tests/Feature/WelcomeOnboardingFlowTest.php
💤 Files with no reviewable changes (1)
- app-modules/onboarding/src/Providers/OnboardingServiceProvider.php
| 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', | ||
| ]; | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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.
…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.
Triagem do feedback do CodeRabbitAplicado (commit
Rejeitado — falso-positivo:
Não aplicado — trade-off consciente:
Validação local: Pint ✅ · PHPStan (level 7) 0 erros ✅ · Pest 7/7 ✅ |
|
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:
Detalhes e proposta aqui: #341 (comment) Vale alinhar lá antes de mergear, porque a decisão pode mudar o |
Closes #343
O que foi feito
Estabelece o módulo de domínio
onboardingcom a máquina de estados polimórfica por tipo (enum → handler), exercitada pelo tipo mais simples (Welcome).src/OnboardingServiceProvider.php(carrega as migrations do módulo)Tz):onboardings—tenant_id,user_id,type,status,completed_at,paused_at+UNIQUE (tenant_id, user_id, type)onboarding_steps—onboarding_id,step_key,status,datajsonb,completed_at+UNIQUE (onboarding_id, step_key)Onboarding/OnboardingStepcom@propertyPHPDoc,#[Table]e#[UseFactory]OnboardingType(Welcome,Squads),OnboardingStatus,OnboardingStepStatusOnboardingFlow(steps(),prerequisites(),advance(),isComplete());OnboardingType::handler()resolve o flowWelcomeOnboardingFlowcom step únicoformStartOnboarding(tenant-scoped, idempotente viafirstOrCreate+ transação)CONTEXT.mde registro noCONTEXT-MAP.mdCritérios de aceite
src/Tz, PHPDoc sincronizadoUNIQUE (tenant_id, user_id, type)OnboardingType::Welcome->handler()→WelcomeOnboardingFlowStartOnboardingcria onboardingin_progress+ stepformpendingCenários BDD → testes
form/pending)" →StartOnboardingTestStartOnboardingTestformconclui o onboarding Welcome →WelcomeOnboardingFlowTestValidação local
Rodado contra Postgres real (
docker-compose up):checkImplicitMixed) — 0 errosFora do escopo (issues futuras)
SquadsOnboardingFlow, enforce de prerequisites no start, e o listener deGithubPullRequestApproved— documentados como deferidos na ADR-0001 e noCONTEXT.md.