mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-15 09:13:16 +01:00
Prevent time entries with end before start
Partial updates of time entries only validated end against start if both were part of the payload. Start and end are now also validated against the persisted value if only one of them is sent (fixes #1189). The importers now fail if the end of a time entry is before the start and the database consistency check reports such time entries.
This commit is contained in:
committed by
Constantin Graf
parent
9a845bad8f
commit
fd008dd4a8
@@ -59,6 +59,14 @@ class SelfHostDatabaseConsistency extends Command
|
||||
->get();
|
||||
$this->logProblems($problems, 'Time entries have a client but no project', $hadAProblem);
|
||||
|
||||
// End of time entries can not be before the start
|
||||
$problems = DB::table('time_entries')
|
||||
->select(['id'])
|
||||
->whereNotNull('end')
|
||||
->whereColumn('end', '<', 'start')
|
||||
->get();
|
||||
$this->logProblems($problems, 'Time entries have an end that is before the start', $hadAProblem);
|
||||
|
||||
// Every user needs to be a member of at least one organization
|
||||
$problems = DB::table('users')
|
||||
->select(['users.id as id'])
|
||||
|
||||
@@ -13,9 +13,11 @@ use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Service\PermissionStore;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ConditionalRules;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -90,12 +92,32 @@ class TimeEntryUpdateRequest extends BaseFormRequest
|
||||
// Start of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||
'start' => [
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
function (string $attribute, mixed $value, Closure $fail) use ($timeEntry): void {
|
||||
// If the payload does not contain an end, the start needs to be validated against the persisted end
|
||||
if ($this->has('end') || $timeEntry?->end === null) {
|
||||
return;
|
||||
}
|
||||
$start = $this->parseDate($value);
|
||||
if ($start !== null && $start->gt($timeEntry->end)) {
|
||||
$fail('The start field must be a date before or equal to end.');
|
||||
}
|
||||
},
|
||||
],
|
||||
// End of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||
'end' => [
|
||||
'nullable',
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
'after_or_equal:start',
|
||||
function (string $attribute, mixed $value, Closure $fail) use ($timeEntry): void {
|
||||
// If the payload does not contain a start, the end needs to be validated against the persisted start
|
||||
if ($this->has('start') || $timeEntry === null) {
|
||||
return;
|
||||
}
|
||||
$end = $this->parseDate($value);
|
||||
if ($end !== null && $end->lt($timeEntry->start)) {
|
||||
$fail('The end field must be a date after or equal to start.');
|
||||
}
|
||||
},
|
||||
],
|
||||
// Whether time entry is billable
|
||||
'billable' => [
|
||||
@@ -137,4 +159,16 @@ class TimeEntryUpdateRequest extends BaseFormRequest
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function parseDate(mixed $value): ?Carbon
|
||||
{
|
||||
if (! is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $value, 'UTC');
|
||||
} catch (InvalidFormatException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,9 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
if ($end === null) {
|
||||
throw new ImportException('End date ("'.$endDateStr.'") or time ("'.$endTimeStr.'") are invalid');
|
||||
}
|
||||
if ($end->lt($start)) {
|
||||
throw new ImportException('End ("'.$endStr.'") is before start ("'.$startStr.'")');
|
||||
}
|
||||
$timeEntry->end = $end->utc();
|
||||
|
||||
$timeEntry->billable_rate = $this->billableRateService->getBillableRateForTimeEntryWithGivenRelations(
|
||||
|
||||
@@ -155,6 +155,9 @@ class GenericTimeEntriesImporter extends DefaultImporter
|
||||
if ($end === null) {
|
||||
throw new ImportException('Value of end ("'.$record['end'].'") is invalid');
|
||||
}
|
||||
if ($end->lt($start)) {
|
||||
throw new ImportException('Value of end ("'.$record['end'].'") is before start ("'.$record['start'].'")');
|
||||
}
|
||||
$timeEntry->end = $end->utc();
|
||||
$timeEntry->billable_rate = $this->billableRateService->getBillableRateForTimeEntryWithGivenRelations(
|
||||
$timeEntry,
|
||||
|
||||
@@ -136,6 +136,9 @@ class HarvestTimeEntriesImporter extends DefaultImporter
|
||||
throw new ImportException('Hours ("'.$record['Hours'].'") is invalid');
|
||||
}
|
||||
$hours = (float) $hoursField;
|
||||
if ($hours < 0) {
|
||||
throw new ImportException('Hours ("'.$record['Hours'].'") is negative');
|
||||
}
|
||||
$timeEntry->start = $date->copy()->startOfDay()->utc();
|
||||
$timeEntry->end = $date->copy()->startOfDay()->addHours($hours)->utc();
|
||||
$timeEntry->billable_rate = $this->billableRateService->getBillableRateForTimeEntryWithGivenRelations(
|
||||
|
||||
@@ -295,6 +295,9 @@ class SolidtimeImporter extends DefaultImporter
|
||||
if ($end === null) {
|
||||
throw new ImportException('End date ("'.$timeEntryRow['end'].'") is invalid');
|
||||
}
|
||||
if ($end->lt($start)) {
|
||||
throw new ImportException('End date ("'.$timeEntryRow['end'].'") is before start date ("'.$timeEntryRow['start'].'")');
|
||||
}
|
||||
$timeEntry->end = $end->utc();
|
||||
} else {
|
||||
$timeEntry->end = null;
|
||||
|
||||
@@ -139,6 +139,9 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
||||
if ($end === null) {
|
||||
throw new ImportException('End date ("'.$record['End date'].'") or time ("'.$record['End time'].'") are invalid');
|
||||
}
|
||||
if ($end->lt($start)) {
|
||||
throw new ImportException('End ("'.$record['End date'].' '.$record['End time'].'") is before start ("'.$record['Start date'].' '.$record['Start time'].'")');
|
||||
}
|
||||
$timeEntry->end = $end->utc();
|
||||
$timeEntry->billable_rate = $this->billableRateService->getBillableRateForTimeEntryWithGivenRelations(
|
||||
$timeEntry,
|
||||
|
||||
Reference in New Issue
Block a user