diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 9968b6e3..5a468ecf 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -51,6 +51,7 @@ class HandleInertiaRequests extends Middleware 'billing' => $billing !== null && $currentOrganization !== null ? [ 'has_subscription' => $billing->hasSubscription($currentOrganization), 'has_trial' => $billing->hasTrial($currentOrganization), + 'trial_until' => $billing->getTrialUntil($currentOrganization)?->toIso8601ZuluString(), 'is_blocked' => $billing->isBlocked($currentOrganization), ] : null, 'flash' => [ diff --git a/app/Service/BillingContract.php b/app/Service/BillingContract.php index 2a43a39f..467ab545 100644 --- a/app/Service/BillingContract.php +++ b/app/Service/BillingContract.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Service; use App\Models\Organization; +use Illuminate\Support\Carbon; /** * This class is a contract for the billing system @@ -33,6 +34,15 @@ class BillingContract return false; } + /** + * Get the date until which the organization's trial subscription is valid + * If the organization does not have a trial subscription, this method should return null + */ + public function getTrialUntil(Organization $organization): ?Carbon + { + return null; + } + /** * Check if the organization is blocked * A blocked organization is an organization that has more than 1 non-placeholder member but no subscription/trial diff --git a/routes/api.php b/routes/api.php index f5ac543e..2cb70dd5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -73,7 +73,7 @@ Route::middleware([ Route::get('/organizations/{organization}/projects/{project}', [ProjectController::class, 'show'])->name('show'); Route::post('/organizations/{organization}/projects', [ProjectController::class, 'store'])->name('store')->middleware('check-organization-blocked'); Route::put('/organizations/{organization}/projects/{project}', [ProjectController::class, 'update'])->name('update')->middleware('check-organization-blocked'); - Route::delete('/organizations/{organization}/projects/{project}', [ProjectController::class, 'destroy'])->name('destroy')->middleware('check-organization-blocked'); + Route::delete('/organizations/{organization}/projects/{project}', [ProjectController::class, 'destroy'])->name('destroy'); }); // Project member routes @@ -81,7 +81,7 @@ Route::middleware([ Route::get('/organizations/{organization}/projects/{project}/project-members', [ProjectMemberController::class, 'index'])->name('index'); Route::post('/organizations/{organization}/projects/{project}/project-members', [ProjectMemberController::class, 'store'])->name('store')->middleware('check-organization-blocked'); Route::put('/organizations/{organization}/project-members/{projectMember}', [ProjectMemberController::class, 'update'])->name('update')->middleware('check-organization-blocked'); - Route::delete('/organizations/{organization}/project-members/{projectMember}', [ProjectMemberController::class, 'destroy'])->name('destroy')->middleware('check-organization-blocked'); + Route::delete('/organizations/{organization}/project-members/{projectMember}', [ProjectMemberController::class, 'destroy'])->name('destroy'); }); // Time entry routes @@ -91,7 +91,7 @@ Route::middleware([ Route::post('/organizations/{organization}/time-entries', [TimeEntryController::class, 'store'])->name('store')->middleware('check-organization-blocked'); Route::put('/organizations/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'update'])->name('update')->middleware('check-organization-blocked'); Route::patch('/organizations/{organization}/time-entries', [TimeEntryController::class, 'updateMultiple'])->name('update-multiple')->middleware('check-organization-blocked'); - Route::delete('/organizations/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'destroy'])->name('destroy')->middleware('check-organization-blocked'); + Route::delete('/organizations/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'destroy'])->name('destroy'); }); Route::name('users.time-entries.')->group(static function () { diff --git a/tests/TestCase.php b/tests/TestCase.php index 73d48ba8..ad6ba8dd 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -28,6 +28,7 @@ abstract class TestCase extends BaseTestCase $this->mock(BillingContract::class, function (MockInterface $mock) { $mock->shouldReceive('hasSubscription')->andReturn(false); $mock->shouldReceive('hasTrial')->andReturn(false); + $mock->shouldReceive('getTrialUntil')->andReturn(null); $mock->shouldReceive('isBlocked')->andReturn(false); }); } diff --git a/tests/Unit/Middleware/HandleInertiaRequestsMiddlewareTest.php b/tests/Unit/Middleware/HandleInertiaRequestsMiddlewareTest.php index 59de2c57..1ae7cb71 100644 --- a/tests/Unit/Middleware/HandleInertiaRequestsMiddlewareTest.php +++ b/tests/Unit/Middleware/HandleInertiaRequestsMiddlewareTest.php @@ -7,6 +7,7 @@ namespace Tests\Unit\Middleware; use App\Http\Middleware\HandleInertiaRequests; use App\Service\BillingContract; use Illuminate\Session\Middleware\StartSession; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Route; use Inertia\Inertia; use Inertia\Testing\AssertableInertia as Assert; @@ -31,9 +32,10 @@ class HandleInertiaRequestsMiddlewareTest extends MiddlewareTestAbstract // Arrange $user = $this->createUserWithPermission(); $route = $this->createTestRoute(); - $this->mock(BillingContract::class, function (MockInterface $mock) { + $this->mock(BillingContract::class, function (MockInterface $mock): void { $mock->shouldReceive('hasSubscription')->andReturn(false); $mock->shouldReceive('hasTrial')->andReturn(false); + $mock->shouldReceive('getTrialUntil')->andReturn(null); $mock->shouldReceive('isBlocked')->andReturn(false); }); Passport::actingAs($user->user); @@ -45,6 +47,33 @@ class HandleInertiaRequestsMiddlewareTest extends MiddlewareTestAbstract $response->assertInertia(fn (Assert $page) => $page ->where('billing.has_subscription', false) ->where('billing.has_trial', false) + ->where('billing.trial_until', null) + ->where('billing.is_blocked', false) + ); + } + + public function test_adds_billing_information_to_shared_data_of_inertia_requests_with_active_trial(): void + { + // Arrange + $user = $this->createUserWithPermission(); + $route = $this->createTestRoute(); + $trialUntil = Carbon::now()->addDays(10); + $this->mock(BillingContract::class, function (MockInterface $mock) use ($trialUntil): void { + $mock->shouldReceive('hasSubscription')->andReturn(false); + $mock->shouldReceive('hasTrial')->andReturn(true); + $mock->shouldReceive('getTrialUntil')->andReturn($trialUntil); + $mock->shouldReceive('isBlocked')->andReturn(false); + }); + Passport::actingAs($user->user); + + // Act + $response = $this->get($route); + + // Assert + $response->assertInertia(fn (Assert $page) => $page + ->where('billing.has_subscription', false) + ->where('billing.has_trial', true) + ->where('billing.trial_until', $trialUntil->toIso8601ZuluString()) ->where('billing.is_blocked', false) ); }