mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
Compare commits
2 Commits
f826474f88
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f6131d88 | ||
|
|
496ccbc45c |
@@ -31,12 +31,17 @@ class TimeEntryService
|
||||
throw new LogicException('Rounding minutes must be greater than 0');
|
||||
}
|
||||
$end = 'coalesce("end", \''.Carbon::now()->toDateTimeString().'\')';
|
||||
$start = $this->getStartSelectRawForRounding($roundingType, $roundingMinutes);
|
||||
if ($roundingType === TimeEntryRoundingType::Down) {
|
||||
return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.', '.$this->getStartSelectRawForRounding($roundingType, $roundingMinutes).')';
|
||||
return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.', '.$start.')';
|
||||
} elseif ($roundingType === TimeEntryRoundingType::Up) {
|
||||
return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.' + interval \''.$roundingMinutes.' minutes\', '.$this->getStartSelectRawForRounding($roundingType, $roundingMinutes).')';
|
||||
// If end is already on a boundary, keep it; otherwise round up to next boundary
|
||||
return 'CASE WHEN '.$end.' = date_bin(\''.$roundingMinutes.' minutes\', '.$end.', '.$start.') '.
|
||||
'THEN '.$end.' '.
|
||||
'ELSE date_bin(\''.$roundingMinutes.' minutes\', '.$end.' + interval \''.$roundingMinutes.' minutes\', '.$start.') '.
|
||||
'END';
|
||||
} elseif ($roundingType === TimeEntryRoundingType::Nearest) {
|
||||
return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.' + interval \''.($roundingMinutes / 2).' minutes\', '.$this->getStartSelectRawForRounding($roundingType, $roundingMinutes).')';
|
||||
return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.' + interval \''.($roundingMinutes / 2).' minutes\', '.$start.')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +436,52 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
);
|
||||
}
|
||||
|
||||
public function test_index_endpoint_can_round_up_but_does_not_round_up_if_already_on_border(): 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:06:00'),
|
||||
]);
|
||||
$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->actAsOrganizationWithSubscription();
|
||||
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:00Z')
|
||||
->where('data.0.end', '2020-01-01T00:06:00Z')
|
||||
->where('data.1.id', $timeEntry2->getKey())
|
||||
->where('data.1.start', '2020-01-01T00:00:00Z')
|
||||
->where('data.1.end', '2020-01-01T00:18:00Z')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_index_endpoint_ignores_rounding_if_organization_has_no_premium_features(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1205,4 +1205,101 @@ class TimeEntryAggregationServiceTest extends TestCaseWithDatabase
|
||||
];
|
||||
$this->assertEqualsCanonicalizing($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that rounding up does NOT add extra time when the entry is already on a 15-minute boundary.
|
||||
* f.e. 13:00 - 14:30 (90 minutes) should stay at 90 minutes when rounding up with 15-minute interval.
|
||||
*/
|
||||
public function test_aggregate_time_round_up_does_not_add_time_when_already_on_boundary(): void
|
||||
{
|
||||
// Arrange
|
||||
// Create a time entry with duration exactly on a 15-minute boundary (90 minutes = 5400 seconds)
|
||||
// This simulates 13:00 - 14:30 (or any 90-minute entry)
|
||||
$project = Project::factory()->create();
|
||||
TimeEntry::factory()->startWithDuration(
|
||||
Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 13:00:00'),
|
||||
5400 // 90 minutes = 1 hour 30 minutes, exactly on 15-minute boundary
|
||||
)->forProject($project)->create();
|
||||
$query = TimeEntry::query();
|
||||
|
||||
// Act
|
||||
$result = $this->service->getAggregatedTimeEntries(
|
||||
$query,
|
||||
TimeEntryAggregationType::Project,
|
||||
null,
|
||||
'Europe/Vienna',
|
||||
Weekday::Monday,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
TimeEntryRoundingType::Up,
|
||||
15
|
||||
);
|
||||
|
||||
// Assert
|
||||
// The entry is already on a 15-minute boundary (90 minutes), so it should stay at 90 minutes (5400 seconds)
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'seconds' => 5400, // 90 minutes - should NOT be rounded to 105 minutes (6300 seconds)
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'project',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => $project->getKey(),
|
||||
'seconds' => 5400, // 90 minutes
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
],
|
||||
],
|
||||
], $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that rounding up works correctly for entries NOT on a boundary.
|
||||
* Example: 13:00 - 13:48 (48 minutes) should round up to 13:00 - 14:00 (60 minutes).
|
||||
*/
|
||||
public function test_aggregate_time_round_up_works_when_not_on_boundary(): void
|
||||
{
|
||||
// Arrange
|
||||
// Create a time entry with duration NOT on a 15-minute boundary (48 minutes = 2880 seconds)
|
||||
$project = Project::factory()->create();
|
||||
TimeEntry::factory()->startWithDuration(
|
||||
Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 13:00:00'),
|
||||
2880 // 48 minutes, not on 15-minute boundary
|
||||
)->forProject($project)->create();
|
||||
$query = TimeEntry::query();
|
||||
|
||||
// Act
|
||||
$result = $this->service->getAggregatedTimeEntries(
|
||||
$query,
|
||||
TimeEntryAggregationType::Project,
|
||||
null,
|
||||
'Europe/Vienna',
|
||||
Weekday::Monday,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
TimeEntryRoundingType::Up,
|
||||
15
|
||||
);
|
||||
|
||||
// Assert
|
||||
// 48 minutes rounded up to 15-minute interval = 60 minutes (3600 seconds)
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'seconds' => 3600, // 60 minutes
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'project',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => $project->getKey(),
|
||||
'seconds' => 3600, // 60 minutes
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
],
|
||||
],
|
||||
], $result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user