mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-16 01:33:18 +01:00
Add correction command to delete time entries with end before start
This commit is contained in:
committed by
Constantin Graf
parent
fd008dd4a8
commit
26926d3b2c
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Correction;
|
||||||
|
|
||||||
|
use App\Jobs\RecalculateSpentTimeForProject;
|
||||||
|
use App\Jobs\RecalculateSpentTimeForTask;
|
||||||
|
use App\Models\TimeEntry;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CorrectionDeleteTimeEntriesWithEndBeforeStartCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'correction:delete-time-entries-with-end-before-start '.
|
||||||
|
' { --dry-run : Do not actually save anything to the database, just output what would happen }';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Deletes all time entries where the end is before the start';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->comment('Deletes all time entries where the end is before the start...');
|
||||||
|
$dryRun = (bool) $this->option('dry-run');
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->comment('Running in dry-run mode. Nothing will be saved to the database.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$timeEntries = TimeEntry::query()
|
||||||
|
->whereNotNull('end')
|
||||||
|
->whereColumn('end', '<', 'start')
|
||||||
|
->with(['project', 'task'])
|
||||||
|
->get();
|
||||||
|
foreach ($timeEntries as $timeEntry) {
|
||||||
|
/** @var TimeEntry $timeEntry */
|
||||||
|
if (! $dryRun) {
|
||||||
|
// Delete via Eloquent, so that the deletion is audited
|
||||||
|
$timeEntry->delete();
|
||||||
|
if ($timeEntry->project !== null) {
|
||||||
|
RecalculateSpentTimeForProject::dispatch($timeEntry->project);
|
||||||
|
}
|
||||||
|
if ($timeEntry->task !== null) {
|
||||||
|
RecalculateSpentTimeForTask::dispatch($timeEntry->task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->line('Deleted time entry (id='.$timeEntry->getKey().', start='.$timeEntry->start->toIso8601ZuluString().', end='.$timeEntry->end?->toIso8601ZuluString().')');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit\Console\Commands\Correction;
|
||||||
|
|
||||||
|
use App\Console\Commands\Correction\CorrectionDeleteTimeEntriesWithEndBeforeStartCommand;
|
||||||
|
use App\Jobs\RecalculateSpentTimeForProject;
|
||||||
|
use App\Jobs\RecalculateSpentTimeForTask;
|
||||||
|
use App\Models\Audit;
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Task;
|
||||||
|
use App\Models\TimeEntry;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use Tests\TestCaseWithDatabase;
|
||||||
|
|
||||||
|
#[CoversClass(CorrectionDeleteTimeEntriesWithEndBeforeStartCommand::class)]
|
||||||
|
class CorrectionDeleteTimeEntriesWithEndBeforeStartCommandTest extends TestCaseWithDatabase
|
||||||
|
{
|
||||||
|
public function test_deletes_time_entries_with_end_before_start_and_audits_the_deletion(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Queue::fake([
|
||||||
|
RecalculateSpentTimeForProject::class,
|
||||||
|
RecalculateSpentTimeForTask::class,
|
||||||
|
]);
|
||||||
|
$organization = Organization::factory()->create();
|
||||||
|
$member = Member::factory()->forOrganization($organization)->create();
|
||||||
|
$project = Project::factory()->forOrganization($organization)->create();
|
||||||
|
$task = Task::factory()->forOrganization($organization)->forProject($project)->create();
|
||||||
|
$invalidTimeEntry = TimeEntry::factory()->forOrganization($organization)->forMember($member)->forProject($project)->forTask($task)->create([
|
||||||
|
'start' => Carbon::parse('2026-08-01T03:00:00Z'),
|
||||||
|
'end' => Carbon::parse('2026-08-01T02:59:55Z'),
|
||||||
|
]);
|
||||||
|
$validTimeEntry = TimeEntry::factory()->forOrganization($organization)->forMember($member)->create([
|
||||||
|
'start' => Carbon::parse('2026-08-01T04:00:00Z'),
|
||||||
|
'end' => Carbon::parse('2026-08-01T04:00:00Z'),
|
||||||
|
]);
|
||||||
|
$runningTimeEntry = TimeEntry::factory()->forOrganization($organization)->forMember($member)->active()->create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$exitCode = $this->withoutMockingConsoleOutput()->artisan('correction:delete-time-entries-with-end-before-start');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||||
|
$output = Artisan::output();
|
||||||
|
$this->assertSame("Deletes all time entries where the end is before the start...\n".
|
||||||
|
'Deleted time entry (id='.$invalidTimeEntry->getKey().", start=2026-08-01T03:00:00Z, end=2026-08-01T02:59:55Z)\n", $output);
|
||||||
|
$this->assertDatabaseMissing(TimeEntry::class, ['id' => $invalidTimeEntry->getKey()]);
|
||||||
|
$this->assertDatabaseHas(TimeEntry::class, ['id' => $validTimeEntry->getKey()]);
|
||||||
|
$this->assertDatabaseHas(TimeEntry::class, ['id' => $runningTimeEntry->getKey()]);
|
||||||
|
$this->assertSame(1, Audit::query()
|
||||||
|
->where('auditable_type', $invalidTimeEntry->getMorphClass())
|
||||||
|
->where('auditable_id', $invalidTimeEntry->getKey())
|
||||||
|
->where('event', 'deleted')
|
||||||
|
->count());
|
||||||
|
Queue::assertPushed(RecalculateSpentTimeForProject::class, 1);
|
||||||
|
Queue::assertPushed(RecalculateSpentTimeForTask::class, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_does_not_delete_time_entries_in_dry_run(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$organization = Organization::factory()->create();
|
||||||
|
$member = Member::factory()->forOrganization($organization)->create();
|
||||||
|
$invalidTimeEntry = TimeEntry::factory()->forOrganization($organization)->forMember($member)->create([
|
||||||
|
'start' => Carbon::parse('2026-08-01T03:00:00Z'),
|
||||||
|
'end' => Carbon::parse('2026-08-01T02:59:55Z'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$exitCode = $this->withoutMockingConsoleOutput()->artisan('correction:delete-time-entries-with-end-before-start --dry-run');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||||
|
$output = Artisan::output();
|
||||||
|
$this->assertSame("Deletes all time entries where the end is before the start...\n".
|
||||||
|
"Running in dry-run mode. Nothing will be saved to the database.\n".
|
||||||
|
'Deleted time entry (id='.$invalidTimeEntry->getKey().", start=2026-08-01T03:00:00Z, end=2026-08-01T02:59:55Z)\n", $output);
|
||||||
|
$this->assertDatabaseHas(TimeEntry::class, ['id' => $invalidTimeEntry->getKey()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user