Skip to content

Commit

Permalink
Add billable rate calculation to creation and deletion of project mem…
Browse files Browse the repository at this point in the history
…bers
  • Loading branch information
korridor committed Sep 3, 2024
1 parent 9df91f4 commit a01e1d6
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 5 deletions.
18 changes: 16 additions & 2 deletions app/Http/Controllers/Api/V1/ProjectMemberController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}
Expand Down
80 changes: 77 additions & 3 deletions tests/Unit/Endpoint/Api/V1/ProjectMemberEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a01e1d6

Please sign in to comment.