From 3b002096b0c86f4b182734dac198d6e0878dbf0a Mon Sep 17 00:00:00 2001 From: Guilherme Saliba Date: Sun, 26 Jul 2026 18:00:03 -0300 Subject: [PATCH] feat(onboarding): add public completion gate --- .../Contracts/OnboardingCompletionGate.php | 16 +++ .../src/OnboardingServiceProvider.php | 7 ++ .../EloquentOnboardingCompletionGate.php | 34 +++++++ .../Feature/OnboardingCompletionGateTest.php | 98 +++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 app-modules/onboarding/src/Contracts/OnboardingCompletionGate.php create mode 100644 app-modules/onboarding/src/Services/EloquentOnboardingCompletionGate.php create mode 100644 app-modules/onboarding/tests/Feature/OnboardingCompletionGateTest.php diff --git a/app-modules/onboarding/src/Contracts/OnboardingCompletionGate.php b/app-modules/onboarding/src/Contracts/OnboardingCompletionGate.php new file mode 100644 index 00000000..21ae3f68 --- /dev/null +++ b/app-modules/onboarding/src/Contracts/OnboardingCompletionGate.php @@ -0,0 +1,16 @@ +app->singleton(OnboardingCompletionGate::class, EloquentOnboardingCompletionGate::class); + } + public function boot(): void { $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); diff --git a/app-modules/onboarding/src/Services/EloquentOnboardingCompletionGate.php b/app-modules/onboarding/src/Services/EloquentOnboardingCompletionGate.php new file mode 100644 index 00000000..61f72d74 --- /dev/null +++ b/app-modules/onboarding/src/Services/EloquentOnboardingCompletionGate.php @@ -0,0 +1,34 @@ +whereBelongsTo($user) + ->where('type', $type) + ->where('status', OnboardingStatus::Completed) + ->exists(); + } + + public function completedAt(User $user, OnboardingType $type): ?CarbonInterface + { + return Onboarding::query() + ->whereBelongsTo($user) + ->where('type', $type) + ->where('status', OnboardingStatus::Completed) + ->first(['completed_at']) + ?->completed_at; + } +} diff --git a/app-modules/onboarding/tests/Feature/OnboardingCompletionGateTest.php b/app-modules/onboarding/tests/Feature/OnboardingCompletionGateTest.php new file mode 100644 index 00000000..e6d7dc06 --- /dev/null +++ b/app-modules/onboarding/tests/Feature/OnboardingCompletionGateTest.php @@ -0,0 +1,98 @@ +create(); + + Onboarding::factory()->create([ + 'user_id' => $user, + 'type' => OnboardingType::Welcome, + 'status' => OnboardingStatus::Completed, + 'completed_at' => now(), + ]); + + expect(resolve(OnboardingCompletionGate::class)->isCompleted($user, OnboardingType::Welcome)) + ->toBeTrue(); +}); + +test('a non-completed onboarding keeps its completion gate closed', function (OnboardingStatus $status): void { + $user = User::factory()->create(); + + Onboarding::factory()->create([ + 'user_id' => $user, + 'type' => OnboardingType::Welcome, + 'status' => $status, + ]); + + expect(resolve(OnboardingCompletionGate::class)->isCompleted($user, OnboardingType::Welcome)) + ->toBeFalse(); +})->with([ + OnboardingStatus::InProgress, + OnboardingStatus::Paused, + OnboardingStatus::Rejected, +]); + +test('a missing onboarding keeps its completion gate closed', function (): void { + $user = User::factory()->create(); + + expect(resolve(OnboardingCompletionGate::class)->isCompleted($user, OnboardingType::Welcome)) + ->toBeFalse(); +}); + +test('completion of another onboarding type does not open the requested gate', function (): void { + $user = User::factory()->create(); + + Onboarding::factory()->create([ + 'user_id' => $user, + 'type' => OnboardingType::Squads, + 'status' => OnboardingStatus::Completed, + 'completed_at' => now(), + ]); + + expect(resolve(OnboardingCompletionGate::class)->isCompleted($user, OnboardingType::Welcome)) + ->toBeFalse(); +}); + +test('the completion gate exposes when the onboarding was completed', function (): void { + $user = User::factory()->create(); + $completedAt = Date::parse('2026-07-26 12:34:56 UTC'); + + Onboarding::factory()->create([ + 'user_id' => $user, + 'type' => OnboardingType::Welcome, + 'status' => OnboardingStatus::Completed, + 'completed_at' => $completedAt, + ]); + + expect(resolve(OnboardingCompletionGate::class)->completedAt($user, OnboardingType::Welcome)) + ->toEqual($completedAt); +}); + +test('the completion gate does not expose a timestamp for a non-completed onboarding', function (): void { + $user = User::factory()->create(); + + Onboarding::factory()->create([ + 'user_id' => $user, + 'type' => OnboardingType::Welcome, + 'status' => OnboardingStatus::InProgress, + 'completed_at' => now(), + ]); + + expect(resolve(OnboardingCompletionGate::class)->completedAt($user, OnboardingType::Welcome)) + ->toBeNull(); +}); + +test('the completion gate has no timestamp when the onboarding is missing', function (): void { + $user = User::factory()->create(); + + expect(resolve(OnboardingCompletionGate::class)->completedAt($user, OnboardingType::Welcome)) + ->toBeNull(); +});