mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
Add endpoint to delete multiple time entries
This commit is contained in:
committed by
Gregor Vostrak
parent
9a50e144b3
commit
071895791c
@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Api\V1;
|
||||
use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException;
|
||||
use App\Exceptions\Api\TimeEntryStillRunningApiException;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryAggregateRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryDestroyMultipleRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateMultipleRequest;
|
||||
@@ -429,4 +430,52 @@ class TimeEntryController extends Controller
|
||||
return response()
|
||||
->json(null, 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple time entries
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId deleteTimeEntries
|
||||
*/
|
||||
public function destroyMultiple(Organization $organization, TimeEntryDestroyMultipleRequest $request): JsonResponse
|
||||
{
|
||||
$this->checkAnyPermission($organization, ['time-entries:delete:all', 'time-entries:delete:own']);
|
||||
$canDeleteAll = $this->hasPermission($organization, 'time-entries:delete:all');
|
||||
|
||||
$ids = $request->validated('ids');
|
||||
$timeEntries = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->whereIn('id', $ids)
|
||||
->get();
|
||||
|
||||
$success = new Collection();
|
||||
$error = new Collection();
|
||||
|
||||
foreach ($ids as $id) {
|
||||
/** @var TimeEntry|null $timeEntry */
|
||||
$timeEntry = $timeEntries->firstWhere('id', $id);
|
||||
if ($timeEntry === null) {
|
||||
// Note: ID wrong or time entry in different organization
|
||||
$error->push($id);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $canDeleteAll && $timeEntry->user_id !== Auth::id()) {
|
||||
$error->push($id);
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
$timeEntry->delete();
|
||||
$success->push($id);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => $success->toArray(),
|
||||
'error' => $error->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Models\Organization;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* @property Organization $organization Organization from model binding
|
||||
*/
|
||||
class TimeEntryDestroyMultipleRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => [
|
||||
'required',
|
||||
'array',
|
||||
],
|
||||
'ids.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,7 @@ Route::middleware([
|
||||
Route::put('/organizations/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'update'])->name('update')->middleware('check-organization-blocked');
|
||||
Route::patch('/organizations/{organization}/time-entries', [TimeEntryController::class, 'updateMultiple'])->name('update-multiple')->middleware('check-organization-blocked');
|
||||
Route::delete('/organizations/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'destroy'])->name('destroy');
|
||||
Route::delete('/organizations/{organization}/time-entries', [TimeEntryController::class, 'destroyMultiple'])->name('destroy-multiple');
|
||||
});
|
||||
|
||||
Route::name('users.time-entries.')->group(static function (): void {
|
||||
|
||||
@@ -1660,6 +1660,146 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_multiple_endpoint_fails_if_user_has_no_permission_to_delete_own_time_entries_or_all_time_entries(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission();
|
||||
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->createMany(3);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.time-entries.destroy-multiple', [$data->organization->getKey()]), [
|
||||
'ids' => $timeEntries->pluck('id')->toArray(),
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertValid();
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_destroy_multiple_endpoint_fails_if_ids_contains_non_uuid_id(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:delete:own',
|
||||
]);
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.time-entries.destroy-multiple', [$data->organization->getKey()]), [
|
||||
'ids' => [
|
||||
Str::uuid(),
|
||||
'non-uuid',
|
||||
],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors([
|
||||
'ids.1' => ['The ids.1 field must be a valid UUID.'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_multiple_endpoint_own_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_own_time_entries_permission(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:delete:own',
|
||||
]);
|
||||
$otherData = $this->createUserWithPermission();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
|
||||
|
||||
$ownTimeEntry = TimeEntry::factory()->forMember($data->member)->create();
|
||||
$otherTimeEntry = TimeEntry::factory()->forMember($otherMember)->create();
|
||||
$otherOrganizationTimeEntry = TimeEntry::factory()->forMember($otherData->member)->create();
|
||||
$wrongId = Str::uuid();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.time-entries.destroy-multiple', [$data->organization->getKey()]), [
|
||||
'ids' => [
|
||||
$ownTimeEntry->getKey(),
|
||||
$otherTimeEntry->getKey(),
|
||||
$otherOrganizationTimeEntry->getKey(),
|
||||
$wrongId,
|
||||
],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertValid();
|
||||
$response->assertStatus(200);
|
||||
$response->assertExactJson([
|
||||
'success' => [
|
||||
$ownTimeEntry->getKey(),
|
||||
],
|
||||
'error' => [
|
||||
$otherTimeEntry->getKey(),
|
||||
$otherOrganizationTimeEntry->getKey(),
|
||||
$wrongId,
|
||||
],
|
||||
]);
|
||||
$this->assertDatabaseMissing(TimeEntry::class, [
|
||||
'id' => $ownTimeEntry->getKey(),
|
||||
]);
|
||||
$this->assertDatabaseHas(TimeEntry::class, [
|
||||
'id' => $otherTimeEntry->getKey(),
|
||||
]);
|
||||
$this->assertDatabaseHas(TimeEntry::class, [
|
||||
'id' => $otherOrganizationTimeEntry->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_multiple_deletes_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'time-entries:delete:all',
|
||||
]);
|
||||
$otherData = $this->createUserWithPermission();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
|
||||
|
||||
$ownTimeEntry = TimeEntry::factory()->forMember($data->member)->create();
|
||||
$otherTimeEntry = TimeEntry::factory()->forMember($otherMember)->create();
|
||||
$otherOrganizationTimeEntry = TimeEntry::factory()->forMember($otherData->member)->create();
|
||||
$wrongId = Str::uuid();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->deleteJson(route('api.v1.time-entries.destroy-multiple', [$data->organization->getKey()]), [
|
||||
'ids' => [
|
||||
$ownTimeEntry->getKey(),
|
||||
$otherTimeEntry->getKey(),
|
||||
$otherOrganizationTimeEntry->getKey(),
|
||||
$wrongId,
|
||||
],
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertValid();
|
||||
$response->assertStatus(200);
|
||||
$response->assertExactJson([
|
||||
'success' => [
|
||||
$ownTimeEntry->getKey(),
|
||||
$otherTimeEntry->getKey(),
|
||||
],
|
||||
'error' => [
|
||||
$otherOrganizationTimeEntry->getKey(),
|
||||
$wrongId,
|
||||
],
|
||||
]);
|
||||
$this->assertDatabaseMissing(TimeEntry::class, [
|
||||
'id' => $ownTimeEntry->getKey(),
|
||||
]);
|
||||
$this->assertDatabaseMissing(TimeEntry::class, [
|
||||
'id' => $otherTimeEntry->getKey(),
|
||||
]);
|
||||
$this->assertDatabaseHas(TimeEntry::class, [
|
||||
'id' => $otherOrganizationTimeEntry->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_endpoint_recalculates_project_and_task_spend_time_after_deleting_time_entry(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Reference in New Issue
Block a user