diff --git a/app/Http/Controllers/Api/V1/ProjectMemberController.php b/app/Http/Controllers/Api/V1/ProjectMemberController.php index 0701fb8b..1043b2ee 100644 --- a/app/Http/Controllers/Api/V1/ProjectMemberController.php +++ b/app/Http/Controllers/Api/V1/ProjectMemberController.php @@ -59,7 +59,7 @@ public function index(Organization $organization, Project $project): ProjectMemb * * @operationId createProjectMember */ - public function store(Organization $organization, Project $project, ProjectMemberStoreRequest $request): JsonResource + public function store(Organization $organization, Project $project, ProjectMemberStoreRequest $request, BillableRateService $billableRateService): JsonResource { $this->checkPermission($organization, 'project-members:create', $project); @@ -78,6 +78,10 @@ public function store(Organization $organization, Project $project, ProjectMembe $projectMember->project()->associate($project); $projectMember->save(); + if ($request->getBillableRate() !== null) { + $billableRateService->updateTimeEntriesBillableRateForProjectMember($projectMember); + } + return new ProjectMemberResource($projectMember); } @@ -109,12 +113,22 @@ public function update(Organization $organization, ProjectMember $projectMember, * * @operationId deleteProjectMember */ - public function destroy(Organization $organization, ProjectMember $projectMember): JsonResponse + public function destroy(Organization $organization, ProjectMember $projectMember, BillableRateService $billableRateService): JsonResponse { $this->checkPermission($organization, 'project-members:delete', projectMember: $projectMember); + $hadBillableRate = $projectMember->billable_rate !== null; + $project = $projectMember->project; + $member = $projectMember->member; + $projectMember->delete(); + if ($hadBillableRate) { + $billableRateService->updateTimeEntriesBillableRateForMember($member); + $billableRateService->updateTimeEntriesBillableRateForProject($project); + $billableRateService->updateTimeEntriesBillableRateForOrganization($organization); + } + return response() ->json(null, 204); } diff --git a/tests/Unit/Endpoint/Api/V1/ProjectMemberEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ProjectMemberEndpointTest.php index f8a4f819..6f503fcc 100644 --- a/tests/Unit/Endpoint/Api/V1/ProjectMemberEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ProjectMemberEndpointTest.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Api\V1\ProjectMemberController; use App\Models\Member; +use App\Models\Organization; use App\Models\Project; use App\Models\ProjectMember; use App\Models\User; @@ -213,16 +214,23 @@ public function test_store_endpoint_fails_if_user_is_already_member_of_project() ]); } - public function test_store_endpoint_creates_new_project_member(): void + public function test_store_endpoint_creates_new_project_member_and_updates_billable_rate(): void { // Arrange $data = $this->createUserWithPermission([ 'project-members:create', ]); $project = Project::factory()->forOrganization($data->organization)->create(); - $projectMemberFake = ProjectMember::factory()->make(); + $projectMemberFake = ProjectMember::factory()->make([ + 'billable_rate' => 1200, + ]); $user = User::factory()->create(); $member = Member::factory()->forOrganization($data->organization)->forUser($user)->create(); + $this->mock(BillableRateService::class, function (MockInterface $mock) use ($projectMemberFake): void { + $mock->shouldReceive('updateTimeEntriesBillableRateForProjectMember') + ->once() + ->withArgs(fn (ProjectMember $projectMemberArg) => $projectMemberArg->billable_rate === $projectMemberFake->billable_rate); + }); Passport::actingAs($data->user); // Act @@ -240,6 +248,36 @@ public function test_store_endpoint_creates_new_project_member(): void ]); } + public function test_store_endpoint_creates_new_project_member_and_does_not_update_billable_rate_if_it_is_null(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'project-members:create', + ]); + $project = Project::factory()->forOrganization($data->organization)->create(); + $projectMemberFake = ProjectMember::factory()->make([ + 'billable_rate' => null, + ]); + $user = User::factory()->create(); + $member = Member::factory()->forOrganization($data->organization)->forUser($user)->create(); + $this->assertBillableRateServiceIsUnused(); + Passport::actingAs($data->user); + + // Act + $response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [ + 'billable_rate' => $projectMemberFake->billable_rate, + 'member_id' => $member->getKey(), + ]); + + // Assert + $response->assertStatus(201); + $this->assertDatabaseHas(ProjectMember::class, [ + 'billable_rate' => null, + 'member_id' => $member->getKey(), + 'project_id' => $project->getKey(), + ]); + } + public function test_update_endpoint_fails_if_project_member_is_not_part_of_organization(): void { // Arrange @@ -384,7 +422,43 @@ public function test_destroy_endpoint_deletes_project_member(): void 'project-members:delete', ]); $project = Project::factory()->forOrganization($data->organization)->create(); - $projectMember = ProjectMember::factory()->forProject($project)->forMember($data->member)->create(); + $projectMember = ProjectMember::factory()->forProject($project)->forMember($data->member)->create([ + 'billable_rate' => null, + ]); + $this->assertBillableRateServiceIsUnused(); + Passport::actingAs($data->user); + + // Act + $response = $this->deleteJson(route('api.v1.project-members.destroy', [$data->organization->getKey(), $projectMember->getKey()])); + + // Assert + $response->assertStatus(204); + $response->assertNoContent(); + $this->assertDatabaseMissing(ProjectMember::class, [ + 'id' => $projectMember->getKey(), + ]); + } + + public function test_destroy_endpoint_updates_billable_rate_of_time_entries_if_project_member_had_billable_rate(): void + { + $data = $this->createUserWithPermission([ + 'project-members:delete', + ]); + $project = Project::factory()->forOrganization($data->organization)->create(); + $projectMember = ProjectMember::factory()->forProject($project)->forMember($data->member)->create([ + 'billable_rate' => 1200, + ]); + $this->mock(BillableRateService::class, function (MockInterface $mock) use ($projectMember): void { + $mock->shouldReceive('updateTimeEntriesBillableRateForMember') + ->once() + ->withArgs(fn (Member $memberArg) => $memberArg->is($projectMember->member)); + $mock->shouldReceive('updateTimeEntriesBillableRateForProject') + ->once() + ->withArgs(fn (Project $projectArg) => $projectArg->is($projectMember->project)); + $mock->shouldReceive('updateTimeEntriesBillableRateForOrganization') + ->once() + ->withArgs(fn (Organization $organizationArg) => $organizationArg->is($projectMember->project->organization)); + }); Passport::actingAs($data->user); // Act