Add billable rate calculation to creation and deletion of project members

This commit is contained in:
Constantin Graf
2024-08-07 12:49:53 +02:00
committed by Constantin Graf
parent 9df91f4e4a
commit a01e1d6b0b
2 changed files with 93 additions and 5 deletions

View File

@@ -59,7 +59,7 @@ class ProjectMemberController extends Controller
*
* @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 @@ class ProjectMemberController extends Controller
$projectMember->project()->associate($project);
$projectMember->save();
if ($request->getBillableRate() !== null) {
$billableRateService->updateTimeEntriesBillableRateForProjectMember($projectMember);
}
return new ProjectMemberResource($projectMember);
}
@@ -109,12 +113,22 @@ class ProjectMemberController extends Controller
*
* @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);
}

View File

@@ -6,6 +6,7 @@ namespace Tests\Unit\Endpoint\Api\V1;
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 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]);
}
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 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]);
}
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 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
'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