From e7fa414c06189ac30b455c1aa48cdf3f93dda5f9 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Wed, 23 Jul 2025 10:50:10 +0200 Subject: [PATCH] Restrict rounding to premium users --- .../Api/V1/TimeEntryController.php | 32 +++++++----- .../Endpoint/Api/V1/TimeEntryEndpointTest.php | 49 +++++++++++++++++++ 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index a2bb23ac..a36c9f71 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -86,7 +86,8 @@ class TimeEntryController extends Controller $this->checkPermission($organization, 'time-entries:view:all'); } - $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member); + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); + $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures); $totalCount = $timeEntriesQuery->count(); @@ -140,13 +141,15 @@ class TimeEntryController extends Controller /** * @return Builder */ - private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder + private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member, bool $canAccessPremiumFeatures): Builder { $select = TimeEntry::SELECT_COLUMNS; - if ($request->getRoundingType() !== null && $request->getRoundingMinutes() !== null) { + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; + if ($roundingType !== null && $roundingMinutes !== null) { $select = array_diff($select, ['start', 'end']); - $select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as start'); - $select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as end'); + $select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start'); + $select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end'); } $timeEntriesQuery = TimeEntry::query() ->whereBelongsTo($organization, 'organization') @@ -184,18 +187,19 @@ class TimeEntryController extends Controller } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $debug = $request->getDebug(); $format = $request->getFormatValue(); - if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) { + if ($format === ExportFormat::PDF && ! $canAccessPremiumFeatures) { throw new FeatureIsNotAvailableInFreePlanApiException; } $user = $this->user(); $timezone = $user->timezone; $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; - $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member); + $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures); $timeEntriesQuery->with([ 'task', 'client', @@ -332,14 +336,15 @@ class TimeEntryController extends Controller } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $user = $this->user(); $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; $group1Type = $request->getGroup(); $group2Type = $request->getSubGroup(); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member); - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries( $timeEntriesAggregateQuery, @@ -380,6 +385,7 @@ class TimeEntryController extends Controller } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $format = $request->getFormatValue(); if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) { throw new FeatureIsNotAvailableInFreePlanApiException; @@ -391,8 +397,8 @@ class TimeEntryController extends Controller $group = $request->getGroup(); $subGroup = $request->getSubGroup(); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member); - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions( $timeEntriesAggregateQuery->clone(), diff --git a/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php b/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php index c1ba8a95..6e7e6cdc 100644 --- a/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php @@ -409,6 +409,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'), 'end' => null, ]); + $this->actAsOrganizationWithSubscription(); Passport::actingAs($data->user); // Act @@ -435,6 +436,52 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract ); } + public function test_index_endpoint_ignores_rounding_if_organization_has_no_premium_features(): void + { + // Arrange + $this->travelTo(Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:15:04')); + $data = $this->createUserWithPermission([ + 'time-entries:view:own', + ]); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization) + ->forMember($data->member) + ->create([ + 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:08'), + 'end' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:01'), + ]); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization) + ->forMember($data->member) + ->create([ + 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'), + 'end' => null, + ]); + $this->actAsOrganizationWithoutSubscriptionAndWithoutTrial(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index', [ + $data->organization->getKey(), + 'member_id' => $data->member->getKey(), + 'rounding_type' => TimeEntryRoundingType::Up, + 'rounding_minutes' => 6, + ])); + + // Assert + $this->assertResponseCode($response, 200); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->has('meta') + ->where('meta.total', 2) + ->count('data', 2) + ->where('data.0.id', $timeEntry1->getKey()) + ->where('data.0.start', '2020-01-01T00:00:08Z') + ->where('data.0.end', '2020-01-01T00:00:01Z') + ->where('data.1.id', $timeEntry2->getKey()) + ->where('data.1.start', '2020-01-01T00:00:07Z') + ->where('data.1.end', null) + ); + } + public function test_index_endpoint_can_round_down(): void { // Arrange @@ -454,6 +501,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'), 'end' => null, ]); + $this->actAsOrganizationWithSubscription(); Passport::actingAs($data->user); // Act @@ -499,6 +547,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'), 'end' => null, ]); + $this->actAsOrganizationWithSubscription(); Passport::actingAs($data->user); // Act