From 0bfeab156c30ef5add7637952d057099510d77ef Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Tue, 13 Oct 2026 18:04:11 +0200 Subject: [PATCH] Skip running time entries in solidtime import Imported running time entries could give a user a second running time entry, for example when an export is imported again while a timer runs. --- .../Import/Importers/SolidtimeImporter.php | 24 +++++++++--------- .../solidtime_import_test_3/clients.csv | 3 +++ .../solidtime_import_test_3/members.csv | 2 ++ .../solidtime_import_test_3/meta.json | 1 + .../organization_invitations.csv | 1 + .../solidtime_import_test_3/organizations.csv | 2 ++ .../project_members.csv | 2 ++ .../solidtime_import_test_3/projects.csv | 4 +++ .../solidtime_import_test_3/tags.csv | 3 +++ .../solidtime_import_test_3/tasks.csv | 3 +++ .../solidtime_import_test_3/time_entries.csv | 5 ++++ .../Importers/SolidtimeImporterTest.php | 25 +++++++++++++++++++ 12 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 resources/testfiles/solidtime_import_test_3/clients.csv create mode 100644 resources/testfiles/solidtime_import_test_3/members.csv create mode 100644 resources/testfiles/solidtime_import_test_3/meta.json create mode 100644 resources/testfiles/solidtime_import_test_3/organization_invitations.csv create mode 100644 resources/testfiles/solidtime_import_test_3/organizations.csv create mode 100644 resources/testfiles/solidtime_import_test_3/project_members.csv create mode 100644 resources/testfiles/solidtime_import_test_3/projects.csv create mode 100644 resources/testfiles/solidtime_import_test_3/tags.csv create mode 100644 resources/testfiles/solidtime_import_test_3/tasks.csv create mode 100644 resources/testfiles/solidtime_import_test_3/time_entries.csv diff --git a/app/Service/Import/Importers/SolidtimeImporter.php b/app/Service/Import/Importers/SolidtimeImporter.php index f6eade0e..e5280d36 100644 --- a/app/Service/Import/Importers/SolidtimeImporter.php +++ b/app/Service/Import/Importers/SolidtimeImporter.php @@ -211,6 +211,10 @@ class SolidtimeImporter extends DefaultImporter // Time entries foreach ($timeEntriesReader as $timeEntryRow) { + // Running time entries are skipped, since the user could already have a running time entry + if ($timeEntryRow['end'] === '') { + continue; + } $userId = $this->userImportHelper->getKeyByExternalIdentifier($timeEntryRow['user_id']); $memberId = $this->memberImportHelper->getKeyByExternalIdentifier($timeEntryRow['member_id']); $member = $this->memberImportHelper->getModelById($memberId); @@ -271,19 +275,15 @@ class SolidtimeImporter extends DefaultImporter } $timeEntry->start = $start->utc(); - if ($timeEntryRow['end'] !== '') { - try { - $end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $timeEntryRow['end'], 'UTC'); - } catch (InvalidFormatException) { - throw new ImportException('End date ("'.$timeEntryRow['end'].'") is invalid'); - } - if ($end === null) { - throw new ImportException('End date ("'.$timeEntryRow['end'].'") is invalid'); - } - $timeEntry->end = $end->utc(); - } else { - $timeEntry->end = null; + try { + $end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $timeEntryRow['end'], 'UTC'); + } catch (InvalidFormatException) { + throw new ImportException('End date ("'.$timeEntryRow['end'].'") is invalid'); } + if ($end === null) { + throw new ImportException('End date ("'.$timeEntryRow['end'].'") is invalid'); + } + $timeEntry->end = $end->utc(); if ($timeEntryRow['still_active_email_sent_at'] !== '') { try { diff --git a/resources/testfiles/solidtime_import_test_3/clients.csv b/resources/testfiles/solidtime_import_test_3/clients.csv new file mode 100644 index 00000000..e84ef176 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/clients.csv @@ -0,0 +1,3 @@ +id,name,organization_id,archived_at,created_at,updated_at +b4187a44-41f4-46d7-8460-f15a25b3aad6,"Big Company",ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +e5a4d8f5-81ae-4606-8e84-6ab1ffa58b72,"Other Company (Archived)",ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_3/members.csv b/resources/testfiles/solidtime_import_test_3/members.csv new file mode 100644 index 00000000..70140e1b --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/members.csv @@ -0,0 +1,2 @@ +id,user_id,name,email,organization_id,billable_rate,role,created_at,updated_at +06e6e605-86bd-417b-b75d-02f671e5d520,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,"Peter Tester",peter.test@email.test,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,admin,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_3/meta.json b/resources/testfiles/solidtime_import_test_3/meta.json new file mode 100644 index 00000000..b40876f8 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/meta.json @@ -0,0 +1 @@ +{"id":"d6a324ee-58d5-4096-8069-c63bd55608f7","version":"1.0","organizations":["ee5a8cd6-312f-4ae6-b044-e2014f09ecc2"],"exported_at":"2024-08-26T18:21:59Z"} \ No newline at end of file diff --git a/resources/testfiles/solidtime_import_test_3/organization_invitations.csv b/resources/testfiles/solidtime_import_test_3/organization_invitations.csv new file mode 100644 index 00000000..01bbe37d --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/organization_invitations.csv @@ -0,0 +1 @@ +id,email,organization_id,role,created_at,updated_at diff --git a/resources/testfiles/solidtime_import_test_3/organizations.csv b/resources/testfiles/solidtime_import_test_3/organizations.csv new file mode 100644 index 00000000..09cfbed0 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/organizations.csv @@ -0,0 +1,2 @@ +id,name,billable_rate,currency,created_at,updated_at +ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,"ACME Corp",,EUR,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_3/project_members.csv b/resources/testfiles/solidtime_import_test_3/project_members.csv new file mode 100644 index 00000000..3633c3d5 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/project_members.csv @@ -0,0 +1,2 @@ +id,billable_rate,project_id,user_id,member_id,created_at,updated_at +180a1a98-2f1c-4596-86e4-63a6be0d7b1d,10002,06e79ec4-33f8-4730-804c-d03c014991d1,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,06e6e605-86bd-417b-b75d-02f671e5d520,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_3/projects.csv b/resources/testfiles/solidtime_import_test_3/projects.csv new file mode 100644 index 00000000..3c3b9e3d --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/projects.csv @@ -0,0 +1,4 @@ +id,name,color,billable_rate,is_public,client_id,organization_id,is_billable,archived_at,created_at,updated_at +06e79ec4-33f8-4730-804c-d03c014991d1,"Project for Big Company",#ec407a,10001,false,b4187a44-41f4-46d7-8460-f15a25b3aad6,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,true,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +622c74a9-7e64-44c2-9426-2a37ac738206,"Project without Client",#ef5350,,false,,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,false,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +aa831162-dbb2-4cfe-bfe0-5e3a252c66f0,"Project (Archived)",#6a407f,,true,e5a4d8f5-81ae-4606-8e84-6ab1ffa58b72,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,true,2024-08-25T10:00:00Z,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_3/tags.csv b/resources/testfiles/solidtime_import_test_3/tags.csv new file mode 100644 index 00000000..6791d565 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/tags.csv @@ -0,0 +1,3 @@ +id,name,organization_id,created_at,updated_at +2c5c2da7-9ef8-4410-bb8f-6e0a90f9d2c7,Development,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +bf6c0ac5-2587-474b-8983-40bb3ea8002f,Backend,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_3/tasks.csv b/resources/testfiles/solidtime_import_test_3/tasks.csv new file mode 100644 index 00000000..01babe7c --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/tasks.csv @@ -0,0 +1,3 @@ +id,name,project_id,organization_id,done_at,created_at,updated_at +b49688a0-94f3-4cb3-9ca1-5003de955fb0,"Task 1",06e79ec4-33f8-4730-804c-d03c014991d1,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +b49688a0-94f3-4cb3-9ca1-5003de955fb0,"Task 2",06e79ec4-33f8-4730-804c-d03c014991d1,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,2024-08-24T10:00:00Z,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/resources/testfiles/solidtime_import_test_3/time_entries.csv b/resources/testfiles/solidtime_import_test_3/time_entries.csv new file mode 100644 index 00000000..2640acc6 --- /dev/null +++ b/resources/testfiles/solidtime_import_test_3/time_entries.csv @@ -0,0 +1,5 @@ +id,description,start,end,billable_rate,billable,type,member_id,user_id,organization_id,client_id,project_id,task_id,tags,is_imported,still_active_email_sent_at,created_at,updated_at +00aae3be-18fc-462d-bee4-350fb605b2f3,,2024-03-04T09:23:52Z,2024-03-04T09:23:52Z,,false,,06e6e605-86bd-417b-b75d-02f671e5d520,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,,,"[""2c5c2da7-9ef8-4410-bb8f-6e0a90f9d2c7"",""bf6c0ac5-2587-474b-8983-40bb3ea8002f""]",false,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +1c7a905d-aa12-4d08-bc41-7e92577e7cdf,"Working hard",2024-03-04T09:23:00Z,2024-03-04T10:23:01Z,,true,work,06e6e605-86bd-417b-b75d-02f671e5d520,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,b4187a44-41f4-46d7-8460-f15a25b3aad6,06e79ec4-33f8-4730-804c-d03c014991d1,b49688a0-94f3-4cb3-9ca1-5003de955fb0,[],false,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +9f2f5b83-3a6b-4a9c-8d21-4f1e5cf1b7aa,"Lunch break",2024-03-04T12:00:00Z,2024-03-04T12:30:00Z,,false,break,06e6e605-86bd-417b-b75d-02f671e5d520,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,,,[],false,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z +5d0e8c4a-7b1f-4e2a-9c3d-8a6b2f1e4c7d,"Still running",2024-03-04T13:00:00Z,,,true,work,06e6e605-86bd-417b-b75d-02f671e5d520,0446cdd8-3ad1-43d6-9231-9e0dc4eeb71c,ee5a8cd6-312f-4ae6-b044-e2014f09ecc2,,,,[],false,,2024-08-22T10:36:48Z,2024-08-22T10:36:48Z diff --git a/tests/Unit/Service/Import/Importers/SolidtimeImporterTest.php b/tests/Unit/Service/Import/Importers/SolidtimeImporterTest.php index 3e93d675..9db82efa 100644 --- a/tests/Unit/Service/Import/Importers/SolidtimeImporterTest.php +++ b/tests/Unit/Service/Import/Importers/SolidtimeImporterTest.php @@ -140,6 +140,31 @@ class SolidtimeImporterTest extends ImporterTestAbstract $this->assertSame([], $breakEntry->tags); } + public function test_import_skips_running_time_entries(): void + { + // Arrange + $zipPath = $this->createTestZip('solidtime_import_test_3'); + $timezone = 'Europe/Vienna'; + $organization = Organization::factory()->create(); + $importer = new SolidtimeImporter; + $importer->init($organization); + $data = file_get_contents($zipPath); + Queue::fake([ + RecalculateSpentTimeForProject::class, + RecalculateSpentTimeForTask::class, + ]); + + // Act + $importer->importData($data, $timezone); + $report = $importer->getReport(); + + // Assert + $this->assertSame(3, $report->timeEntriesCreated); + $this->assertSame(3, TimeEntry::query()->count()); + $this->assertSame(0, TimeEntry::query()->whereNull('end')->count()); + $this->assertNull(TimeEntry::query()->where('description', 'Still running')->first()); + } + public function test_import_of_test_file_twice_succeeds(): void { // Arrange