mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 19:22:14 +01:00
Compare commits
2 Commits
main
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f6131d88 | ||
|
|
496ccbc45c |
@@ -31,12 +31,17 @@ class TimeEntryService
|
|||||||
throw new LogicException('Rounding minutes must be greater than 0');
|
throw new LogicException('Rounding minutes must be greater than 0');
|
||||||
}
|
}
|
||||||
$end = 'coalesce("end", \''.Carbon::now()->toDateTimeString().'\')';
|
$end = 'coalesce("end", \''.Carbon::now()->toDateTimeString().'\')';
|
||||||
|
$start = $this->getStartSelectRawForRounding($roundingType, $roundingMinutes);
|
||||||
if ($roundingType === TimeEntryRoundingType::Down) {
|
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) {
|
} 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) {
|
} 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
|
public function test_index_endpoint_ignores_rounding_if_organization_has_no_premium_features(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -1205,4 +1205,101 @@ class TimeEntryAggregationServiceTest extends TestCaseWithDatabase
|
|||||||
];
|
];
|
||||||
$this->assertEqualsCanonicalizing($expected, $result);
|
$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