Migrated endpoints from user to member; Renamed membership to member

This commit is contained in:
Constantin Graf
2024-05-13 19:50:50 +02:00
parent ba335b4f05
commit b2365e2778
64 changed files with 853 additions and 456 deletions

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Actions\Jetstream; namespace App\Actions\Jetstream;
use App\Enums\Role; use App\Enums\Role;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use App\Service\PermissionStore; use App\Service\PermissionStore;
@@ -32,7 +32,7 @@ class UpdateMemberRole
} }
$user = User::where('id', '=', $userId)->firstOrFail(); $user = User::where('id', '=', $userId)->firstOrFail();
$member = Membership::whereBelongsTo($user)->whereBelongsTo($organization)->firstOrFail(); $member = Member::whereBelongsTo($user)->whereBelongsTo($organization)->firstOrFail();
if ($member->role === Role::Placeholder->value) { if ($member->role === Role::Placeholder->value) {
abort(403, 'Cannot update the role of a placeholder member.'); abort(403, 'Cannot update the role of a placeholder member.');
} }

View File

@@ -108,11 +108,15 @@ class OrganizationResource extends Resource
->icon('heroicon-o-inbox-arrow-down') ->icon('heroicon-o-inbox-arrow-down')
->action(function (Organization $record, array $data) { ->action(function (Organization $record, array $data) {
try { try {
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
if ($file === null) {
throw new \Exception('File not found');
}
/** @var ReportDto $report */ /** @var ReportDto $report */
$report = app(ImportService::class)->import( $report = app(ImportService::class)->import(
$record, $record,
$data['type'], $data['type'],
Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']) $file
); );
Notification::make() Notification::make()
->title('Import successful') ->title('Import successful')

View File

@@ -5,8 +5,11 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1; namespace App\Http\Controllers\Api\V1;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User;
use App\Service\PermissionStore; use App\Service\PermissionStore;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class Controller extends \App\Http\Controllers\Controller class Controller extends \App\Http\Controllers\Controller
{ {
@@ -29,4 +32,19 @@ class Controller extends \App\Http\Controllers\Controller
{ {
return $this->permissionStore->has($organization, $permission); return $this->permissionStore->has($organization, $permission);
} }
/**
* @throws AuthorizationException
*/
protected function user(): User
{
/** @var User|null $user */
$user = Auth::user();
if ($user === null) {
Log::error('This function should only be called in authenticated context');
throw new AuthorizationException();
}
return $user;
}
} }

View File

@@ -57,7 +57,7 @@ class InvitationController extends Controller
$this->checkPermission($organization, 'invitations:create'); $this->checkPermission($organization, 'invitations:create');
app(InvitesTeamMembers::class)->invite( app(InvitesTeamMembers::class)->invite(
$request->user(), $this->user(),
$organization, $organization,
$request->input('email'), $request->input('email'),
$request->input('role') $request->input('role')

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1; namespace App\Http\Controllers\Api\V1;
use App\Enums\Role;
use App\Exceptions\Api\EntityStillInUseApiException; use App\Exceptions\Api\EntityStillInUseApiException;
use App\Exceptions\Api\UserNotPlaceholderApiException; use App\Exceptions\Api\UserNotPlaceholderApiException;
use App\Http\Requests\V1\Member\MemberIndexRequest; use App\Http\Requests\V1\Member\MemberIndexRequest;
@@ -11,7 +12,7 @@ use App\Http\Requests\V1\Member\MemberUpdateRequest;
use App\Http\Resources\V1\Member\MemberCollection; use App\Http\Resources\V1\Member\MemberCollection;
use App\Http\Resources\V1\Member\MemberPivotResource; use App\Http\Resources\V1\Member\MemberPivotResource;
use App\Http\Resources\V1\Member\MemberResource; use App\Http\Resources\V1\Member\MemberResource;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\TimeEntry; use App\Models\TimeEntry;
@@ -23,10 +24,10 @@ use Laravel\Jetstream\Contracts\InvitesTeamMembers;
class MemberController extends Controller class MemberController extends Controller
{ {
protected function checkPermission(Organization $organization, string $permission, ?Membership $membership = null): void protected function checkPermission(Organization $organization, string $permission, ?Member $member = null): void
{ {
parent::checkPermission($organization, $permission); parent::checkPermission($organization, $permission);
if ($membership !== null && $membership->organization_id !== $organization->id) { if ($member !== null && $member->organization_id !== $organization->id) {
throw new AuthorizationException('Member does not belong to organization'); throw new AuthorizationException('Member does not belong to organization');
} }
} }
@@ -57,15 +58,15 @@ class MemberController extends Controller
* *
* @operationId updateMember * @operationId updateMember
*/ */
public function update(Organization $organization, Membership $membership, MemberUpdateRequest $request): JsonResource public function update(Organization $organization, Member $member, MemberUpdateRequest $request): JsonResource
{ {
$this->checkPermission($organization, 'members:update', $membership); $this->checkPermission($organization, 'members:update', $member);
$membership->billable_rate = $request->input('billable_rate'); $member->billable_rate = $request->input('billable_rate');
$membership->role = $request->input('role'); $member->role = $request->input('role');
$membership->save(); $member->save();
return new MemberResource($membership); return new MemberResource($member);
} }
/** /**
@@ -75,18 +76,18 @@ class MemberController extends Controller
* *
* @operationId removeMember * @operationId removeMember
*/ */
public function destroy(Organization $organization, Membership $membership): JsonResponse public function destroy(Organization $organization, Member $member): JsonResponse
{ {
$this->checkPermission($organization, 'members:delete', $membership); $this->checkPermission($organization, 'members:delete', $member);
if (TimeEntry::query()->where('user_id', $membership->user_id)->whereBelongsTo($organization, 'organization')->exists()) { if (TimeEntry::query()->where('user_id', $member->user_id)->whereBelongsTo($organization, 'organization')->exists()) {
throw new EntityStillInUseApiException('member', 'time_entry'); throw new EntityStillInUseApiException('member', 'time_entry');
} }
if (ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $membership->user_id)->exists()) { if (ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $member->user_id)->exists()) {
throw new EntityStillInUseApiException('member', 'project_member'); throw new EntityStillInUseApiException('member', 'project_member');
} }
$membership->delete(); $member->delete();
return response() return response()
->json(null, 204); ->json(null, 204);
@@ -99,20 +100,20 @@ class MemberController extends Controller
* *
* @operationId invitePlaceholder * @operationId invitePlaceholder
*/ */
public function invitePlaceholder(Organization $organization, Membership $membership, Request $request): JsonResponse public function invitePlaceholder(Organization $organization, Member $member, Request $request): JsonResponse
{ {
$this->checkPermission($organization, 'members:invite-placeholder', $membership); $this->checkPermission($organization, 'members:invite-placeholder', $member);
$user = $membership->user; $user = $member->user;
if (! $user->is_placeholder) { if (! $user->is_placeholder) {
throw new UserNotPlaceholderApiException(); throw new UserNotPlaceholderApiException();
} }
app(InvitesTeamMembers::class)->invite( app(InvitesTeamMembers::class)->invite(
$request->user(), $this->user(),
$organization, $organization,
$user->email, $user->email,
'employee' Role::Employee->value,
); );
return response()->json(null, 204); return response()->json(null, 204);

View File

@@ -17,7 +17,6 @@ use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class ProjectController extends Controller class ProjectController extends Controller
@@ -43,8 +42,7 @@ class ProjectController extends Controller
{ {
$this->checkPermission($organization, 'projects:view'); $this->checkPermission($organization, 'projects:view');
$canViewAllProjects = $this->hasPermission($organization, 'projects:view:all'); $canViewAllProjects = $this->hasPermission($organization, 'projects:view:all');
/** @var User $user */ $user = $this->user();
$user = Auth::user();
$projectsQuery = Project::query() $projectsQuery = Project::query()
->whereBelongsTo($organization, 'organization'); ->whereBelongsTo($organization, 'organization');

View File

@@ -10,10 +10,10 @@ use App\Http\Requests\V1\ProjectMember\ProjectMemberStoreRequest;
use App\Http\Requests\V1\ProjectMember\ProjectMemberUpdateRequest; use App\Http\Requests\V1\ProjectMember\ProjectMemberUpdateRequest;
use App\Http\Resources\V1\ProjectMember\ProjectMemberCollection; use App\Http\Resources\V1\ProjectMember\ProjectMemberCollection;
use App\Http\Resources\V1\ProjectMember\ProjectMemberResource; use App\Http\Resources\V1\ProjectMember\ProjectMemberResource;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@@ -62,17 +62,18 @@ class ProjectMemberController extends Controller
{ {
$this->checkPermission($organization, 'project-members:create', $project); $this->checkPermission($organization, 'project-members:create', $project);
$user = User::findOrFail((string) $request->input('user_id')); $member = Member::findOrFail((string) $request->input('member_id'));
if ($user->is_placeholder) { if ($member->user->is_placeholder) {
throw new InactiveUserCanNotBeUsedApiException(); throw new InactiveUserCanNotBeUsedApiException();
} }
if (ProjectMember::whereBelongsTo($project, 'project')->whereBelongsTo($user, 'user')->exists()) { if (ProjectMember::whereBelongsTo($project, 'project')->whereBelongsTo($member, 'member')->exists()) {
throw new UserIsAlreadyMemberOfProjectApiException(); throw new UserIsAlreadyMemberOfProjectApiException();
} }
$projectMember = new ProjectMember(); $projectMember = new ProjectMember();
$projectMember->billable_rate = $request->input('billable_rate'); $projectMember->billable_rate = $request->input('billable_rate');
$projectMember->user()->associate($user); $projectMember->member()->associate($member);
$projectMember->user()->associate($member->user);
$projectMember->project()->associate($project); $projectMember->project()->associate($project);
$projectMember->save(); $projectMember->save();

View File

@@ -12,11 +12,9 @@ use App\Http\Resources\V1\Task\TaskCollection;
use App\Http\Resources\V1\Task\TaskResource; use App\Http\Resources\V1\Task\TaskResource;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Task; use App\Models\Task;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
class TaskController extends Controller class TaskController extends Controller
{ {
@@ -41,8 +39,7 @@ class TaskController extends Controller
{ {
$this->checkPermission($organization, 'tasks:view'); $this->checkPermission($organization, 'tasks:view');
$canViewAllTasks = $this->hasPermission($organization, 'tasks:view:all'); $canViewAllTasks = $this->hasPermission($organization, 'tasks:view:all');
/** @var User $user */ $user = $this->user();
$user = Auth::user();
$projectId = $request->input('project_id'); $projectId = $request->input('project_id');

View File

@@ -13,6 +13,7 @@ use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest; use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest;
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection; use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
use App\Http\Resources\V1\TimeEntry\TimeEntryResource; use App\Http\Resources\V1\TimeEntry\TimeEntryResource;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Service\TimeEntryFilter; use App\Service\TimeEntryFilter;
@@ -22,9 +23,9 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Ramsey\Uuid\Type\Time;
class TimeEntryController extends Controller class TimeEntryController extends Controller
{ {
@@ -48,7 +49,9 @@ class TimeEntryController extends Controller
*/ */
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
{ {
if ($request->has('user_id') && $request->get('user_id') === Auth::id()) { /** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
if ($member !== null && $member->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:view:own'); $this->checkPermission($organization, 'time-entries:view:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:view:all'); $this->checkPermission($organization, 'time-entries:view:all');
@@ -62,7 +65,8 @@ class TimeEntryController extends Controller
$filter->addBeforeFilter($request->input('before')); $filter->addBeforeFilter($request->input('before'));
$filter->addAfterFilter($request->input('after')); $filter->addAfterFilter($request->input('after'));
$filter->addActiveFilter($request->input('active')); $filter->addActiveFilter($request->input('active'));
$filter->addUserIdFilter($request->input('user_id')); $filter->addMemberIdFilter($member);
$filter->addMemberIdsFilter($request->input('member_ids'));
$filter->addProjectIdsFilter($request->input('project_ids')); $filter->addProjectIdsFilter($request->input('project_ids'));
$filter->addTagIdsFilter($request->input('tag_ids')); $filter->addTagIdsFilter($request->input('tag_ids'));
$filter->addTaskIdsFilter($request->input('task_ids')); $filter->addTaskIdsFilter($request->input('task_ids'));
@@ -78,7 +82,7 @@ class TimeEntryController extends Controller
$timeEntries = $timeEntriesQuery->get(); $timeEntries = $timeEntriesQuery->get();
if ($timeEntries->count() === $limit && $request->has('only_full_dates') && (bool) $request->get('only_full_dates') === true) { if ($timeEntries->count() === $limit && $request->has('only_full_dates') && (bool) $request->get('only_full_dates') === true) {
$user = Auth::user(); $user = $this->user();
$timezone = app(TimezoneService::class)->getTimezoneFromUser($user); $timezone = app(TimezoneService::class)->getTimezoneFromUser($user);
$lastDate = null; $lastDate = null;
/** @var TimeEntry $timeEntry */ /** @var TimeEntry $timeEntry */
@@ -113,11 +117,38 @@ class TimeEntryController extends Controller
/** /**
* Get aggregated time entries in organization * Get aggregated time entries in organization
* *
* This endpoint allows you to filter time entries and aggregate them by different criteria.
* The parameters `group` and `sub_group` allow you to group the time entries by different criteria.
* If the group parameters are all set to `null` or are all missing, the endpoint will aggregate all filtered time entries.
*
* @operationId getAggregatedTimeEntries
*
* @return array{
* data: array{
* grouped_data: null|array<array{
* type: string,
* value: string|null,
* aggregate: int,
* cost: int,
* grouped_data: null|array<array{
* type: string,
* value: string|null,
* aggregate: int,
* cost: int
* }>
* }>,
* aggregate: int,
* cost: int
* }
* }
*
* @throws AuthorizationException * @throws AuthorizationException
*/ */
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request): array public function aggregate(Organization $organization, TimeEntryAggregateRequest $request): array
{ {
if ($request->has('user_id') && $request->get('user_id') === Auth::id()) { /** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
if ($member !== null && $member->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:view:own'); $this->checkPermission($organization, 'time-entries:view:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:view:all'); $this->checkPermission($organization, 'time-entries:view:all');
@@ -130,7 +161,8 @@ class TimeEntryController extends Controller
$filter->addBeforeFilter($request->input('before')); $filter->addBeforeFilter($request->input('before'));
$filter->addAfterFilter($request->input('after')); $filter->addAfterFilter($request->input('after'));
$filter->addActiveFilter($request->input('active')); $filter->addActiveFilter($request->input('active'));
$filter->addUserIdFilter($request->input('user_id')); $filter->addMemberIdFilter($member);
$filter->addMemberIdsFilter($request->input('member_ids'));
$filter->addProjectIdsFilter($request->input('project_ids')); $filter->addProjectIdsFilter($request->input('project_ids'));
$filter->addTagIdsFilter($request->input('tag_ids')); $filter->addTagIdsFilter($request->input('tag_ids'));
$filter->addTaskIdsFilter($request->input('task_ids')); $filter->addTaskIdsFilter($request->input('task_ids'));
@@ -138,10 +170,12 @@ class TimeEntryController extends Controller
$filter->addBillableFilter($request->input('billable')); $filter->addBillableFilter($request->input('billable'));
$timeEntriesQuery = $filter->get(); $timeEntriesQuery = $filter->get();
$user = Auth::user(); $user = $this->user();
$group1Type = $request->get('group_1'); /** @var string|null $group1Type */
$group2Type = $request->get('group_2'); $group1Type = $request->get('group');
/** @var string|null $group2Type */
$group2Type = $request->get('sub_group');
$group1Select = null; $group1Select = null;
$group2Select = null; $group2Select = null;
@@ -178,11 +212,15 @@ class TimeEntryController extends Controller
$group1ResponseSum = 0; $group1ResponseSum = 0;
$group1ResponseCost = 0; $group1ResponseCost = 0;
foreach ($groupedAggregates as $group1 => $group1Aggregates) { foreach ($groupedAggregates as $group1 => $group1Aggregates) {
/** @var string $group1 */
$group2Response = []; $group2Response = [];
if ($group2Select !== null) { if ($group2Select !== null) {
$group2ResponseSum = 0; $group2ResponseSum = 0;
$group2ResponseCost = 0; $group2ResponseCost = 0;
foreach ($group1Aggregates as $group2 => $aggregate) { foreach ($group1Aggregates as $group2 => $aggregate) {
/** @var string $group2 */
/** @var Collection<int, object{aggregate: int, cost: int}> $aggregate */
/** @var string $group2Type */
$group2Response[] = [ $group2Response[] = [
'type' => $group2Type, 'type' => $group2Type,
'value' => $group2 === '' ? null : $group2, 'value' => $group2 === '' ? null : $group2,
@@ -193,15 +231,18 @@ class TimeEntryController extends Controller
$group2ResponseCost += (int) $aggregate->get(0)->cost; $group2ResponseCost += (int) $aggregate->get(0)->cost;
} }
} else { } else {
/** @var Collection<int, object{aggregate: int, cost: int}> $group1Aggregates */
$group2ResponseSum = (int) $group1Aggregates->get(0)->aggregate; $group2ResponseSum = (int) $group1Aggregates->get(0)->aggregate;
$group2ResponseCost = (int) $group1Aggregates->get(0)->cost; $group2ResponseCost = (int) $group1Aggregates->get(0)->cost;
$group2Response = null; $group2Response = null;
} }
/** @var string $group1Type */
$group1Response[] = [ $group1Response[] = [
'type' => $group1Type, 'type' => $group1Type,
'value' => $group1 === '' ? null : $group1, 'value' => $group1 === '' ? null : $group1,
'aggregate' => $group2ResponseSum, 'aggregate' => $group2ResponseSum,
'cost' => $group2ResponseCost,
'grouped_data' => $group2Response, 'grouped_data' => $group2Response,
]; ];
$group1ResponseSum += $group2ResponseSum; $group1ResponseSum += $group2ResponseSum;
@@ -209,6 +250,7 @@ class TimeEntryController extends Controller
} }
} else { } else {
$group1Response = null; $group1Response = null;
/** @var Collection<int, object{aggregate: int, cost: int}> $timeEntriesAggregates */
$group1ResponseSum = (int) $timeEntriesAggregates->get(0)->aggregate; $group1ResponseSum = (int) $timeEntriesAggregates->get(0)->aggregate;
$group1ResponseCost = (int) $timeEntriesAggregates->get(0)->cost; $group1ResponseCost = (int) $timeEntriesAggregates->get(0)->cost;
} }
@@ -266,18 +308,21 @@ class TimeEntryController extends Controller
*/ */
public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource
{ {
if ($request->get('user_id') === Auth::id()) { /** @var Member $member */
$member = Member::query()->findOrFail($request->get('member_id'));
if ($member->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:create:own'); $this->checkPermission($organization, 'time-entries:create:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:create:all'); $this->checkPermission($organization, 'time-entries:create:all');
} }
if ($request->get('end') === null && TimeEntry::query()->where('user_id', $request->get('user_id'))->where('end', null)->exists()) { if ($request->get('end') === null && TimeEntry::query()->whereBelongsTo($member, 'member')->where('end', null)->exists()) {
throw new TimeEntryStillRunningApiException(); throw new TimeEntryStillRunningApiException();
} }
$timeEntry = new TimeEntry(); $timeEntry = new TimeEntry();
$timeEntry->fill($request->validated()); $timeEntry->fill($request->validated());
$timeEntry->user_id = $member->user_id;
$timeEntry->description = $request->get('description') ?? ''; $timeEntry->description = $request->get('description') ?? '';
$timeEntry->organization()->associate($organization); $timeEntry->organization()->associate($organization);
$timeEntry->setComputedAttributeValue('billable_rate'); $timeEntry->setComputedAttributeValue('billable_rate');
@@ -295,10 +340,12 @@ class TimeEntryController extends Controller
*/ */
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
{ {
if ($timeEntry->user_id === Auth::id() && $request->get('user_id') === Auth::id()) { /** @var Member|null $member */
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry); $member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
if ($timeEntry->member->user_id === Auth::id() && $member?->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:update:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry); $this->checkPermission($organization, 'time-entries:update:all');
} }
if ($timeEntry->end !== null && $request->has('end') && $request->get('end') === null) { if ($timeEntry->end !== null && $request->has('end') && $request->get('end') === null) {
@@ -321,7 +368,7 @@ class TimeEntryController extends Controller
*/ */
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
{ {
if ($timeEntry->user_id === Auth::id()) { if ($timeEntry->member->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry); $this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
} else { } else {
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry); $this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);

View File

@@ -10,7 +10,6 @@ use App\Models\TimeEntry;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class UserTimeEntryController extends Controller class UserTimeEntryController extends Controller
@@ -24,8 +23,7 @@ class UserTimeEntryController extends Controller
*/ */
public function myActive(): JsonResource public function myActive(): JsonResource
{ {
/** @var User $user */ $user = $this->user();
$user = Auth::user();
$activeTimeEntriesOfUser = TimeEntry::query() $activeTimeEntriesOfUser = TimeEntry::query()
->whereBelongsTo($user, 'user') ->whereBelongsTo($user, 'user')

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Http\Requests\V1\ProjectMember; namespace App\Http\Requests\V1\ProjectMember;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
@@ -24,12 +24,12 @@ class ProjectMemberStoreRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'user_id' => [ 'member_id' => [
'required', 'required',
'uuid', 'uuid',
new ExistsEloquent(User::class, null, function (Builder $builder): Builder { new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<User> $builder */ /** @var Builder<Member> $builder */
return $builder->belongsToOrganization($this->organization); return $builder->whereBelongsTo($this->organization, 'organization');
}), }),
], ],
'billable_rate' => [ 'billable_rate' => [

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Requests\V1\TimeEntry; namespace App\Http\Requests\V1\TimeEntry;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Tag; use App\Models\Tag;
@@ -28,14 +29,38 @@ class TimeEntryAggregateRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'group_1' => [ 'group' => [
'nullable',
'required_with:group_2', 'required_with:group_2',
'in:day,week,month,year,user,project,task,client,billable', 'in:day,week,month,year,user,project,task,client,billable',
], ],
'group_2' => [ 'sub_group' => [
'nullable',
'in:day,week,month,year,user,project,task,client,billable', 'in:day,week,month,year,user,project,task,client,billable',
], ],
// Filter by member ID
'member_id' => [
'string',
'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
}),
],
// Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter
'member_ids' => [
'array',
'min:1',
],
'member_ids.*' => [
'string',
'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
}),
],
// Filter by user ID // Filter by user ID
'user_id' => [ 'user_id' => [

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Http\Requests\V1\TimeEntry; namespace App\Http\Requests\V1\TimeEntry;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Tag; use App\Models\Tag;
use App\Models\Task; use App\Models\Task;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
@@ -28,13 +28,26 @@ class TimeEntryIndexRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
// Filter by user ID // Filter by member ID
'user_id' => [ 'member_id' => [
'string', 'string',
'uuid', 'uuid',
new ExistsEloquent(User::class, null, function (Builder $builder): Builder { new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<User> $builder */ /** @var Builder<Member> $builder */
return $builder->belongsToOrganization($this->organization); return $builder->whereBelongsTo($this->organization, 'organization');
}),
],
// Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter
'member_ids' => [
'array',
'min:1',
],
'member_ids.*' => [
'string',
'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
}), }),
], ],
// Filter by project IDs, project IDs are OR combined // Filter by project IDs, project IDs are OR combined

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Http\Requests\V1\TimeEntry; namespace App\Http\Requests\V1\TimeEntry;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Tag; use App\Models\Tag;
use App\Models\Task; use App\Models\Task;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
@@ -27,14 +27,14 @@ class TimeEntryStoreRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
// ID of the user that the time entry should belong to // ID of the organization member that the time entry should belong to
'user_id' => [ 'member_id' => [
'required', 'required',
'string', 'string',
'uuid', 'uuid',
new ExistsEloquent(User::class, null, function (Builder $builder): Builder { new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<User> $builder */ /** @var Builder<Member> $builder */
return $builder->belongsToOrganization($this->organization); return $builder->whereBelongsTo($this->organization, 'organization');
}), }),
], ],
'project_id' => [ 'project_id' => [

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Requests\V1\TimeEntry; namespace App\Http\Requests\V1\TimeEntry;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Tag; use App\Models\Tag;
@@ -26,6 +27,16 @@ class TimeEntryUpdateRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
// ID of the organization member that the time entry should belong to
'member_id' => [
'string',
'uuid',
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
}),
],
// ID of the project that the time entry should belong to
'project_id' => [ 'project_id' => [
'nullable', 'nullable',
'string', 'string',

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Http\Resources\V1\Member; namespace App\Http\Resources\V1\Member;
use App\Http\Resources\V1\BaseResource; use App\Http\Resources\V1\BaseResource;
use App\Models\Membership; use App\Models\Member;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -21,12 +21,12 @@ class MemberPivotResource extends BaseResource
*/ */
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
/** @var Membership $membership */ /** @var Member $member */
$membership = $this->resource->getRelationValue('membership'); $member = $this->resource->getRelationValue('membership');
return [ return [
/** @var string $id ID of membership */ /** @var string $id ID of membership */
'id' => $membership->id, 'id' => $member->id,
/** @var string $id ID of user */ /** @var string $id ID of user */
'user_id' => $this->resource->id, 'user_id' => $this->resource->id,
/** @var string $name Name */ /** @var string $name Name */
@@ -34,11 +34,11 @@ class MemberPivotResource extends BaseResource
/** @var string $email Email */ /** @var string $email Email */
'email' => $this->resource->email, 'email' => $this->resource->email,
/** @var string $role Role */ /** @var string $role Role */
'role' => $membership->role, 'role' => $member->role,
/** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */ /** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */
'is_placeholder' => $this->resource->is_placeholder, 'is_placeholder' => $this->resource->is_placeholder,
/** @var int|null $billable_rate Billable rate in cents per hour */ /** @var int|null $billable_rate Billable rate in cents per hour */
'billable_rate' => $membership->billable_rate, 'billable_rate' => $member->billable_rate,
]; ];
} }
} }

View File

@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace App\Http\Resources\V1\Member; namespace App\Http\Resources\V1\Member;
use App\Http\Resources\V1\BaseResource; use App\Http\Resources\V1\BaseResource;
use App\Models\Membership; use App\Models\Member;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
/** /**
* @property Membership $resource * @property Member $resource
*/ */
class MemberResource extends BaseResource class MemberResource extends BaseResource
{ {

View File

@@ -25,8 +25,8 @@ class ProjectMemberResource extends BaseResource
'id' => $this->resource->id, 'id' => $this->resource->id,
/** @var int|null $billable_rate Billable rate in cents per hour */ /** @var int|null $billable_rate Billable rate in cents per hour */
'billable_rate' => $this->resource->billable_rate, 'billable_rate' => $this->resource->billable_rate,
/** @var string $user_id ID of the user */ /** @var string $member_id ID of the organization member */
'user_id' => $this->resource->user_id, 'member_id' => $this->resource->member_id,
/** @var string $project_id ID of the project */ /** @var string $project_id ID of the project */
'project_id' => $this->resource->project_id, 'project_id' => $this->resource->project_id,
]; ];

View File

@@ -4,8 +4,9 @@ declare(strict_types=1);
namespace App\Listeners; namespace App\Listeners;
use App\Models\User; use App\Models\Member;
use App\Service\UserService; use App\Service\UserService;
use Illuminate\Database\Eloquent\Builder;
use Laravel\Jetstream\Events\TeamMemberAdded; use Laravel\Jetstream\Events\TeamMemberAdded;
class RemovePlaceholder class RemovePlaceholder
@@ -17,15 +18,21 @@ class RemovePlaceholder
{ {
/** @var UserService $userService */ /** @var UserService $userService */
$userService = app(UserService::class); $userService = app(UserService::class);
$placeholders = User::query() $placeholders = Member::query()
->where('is_placeholder', '=', true) ->whereHas('user', function (Builder $query) use ($event) {
->where('email', '=', $event->user->email) $query->where('is_placeholder', '=', true)
->belongsToOrganization($event->team) ->where('email', '=', $event->user->email);
})
->whereBelongsTo($event->team, 'organization')
->with(['user'])
->get(); ->get();
foreach ($placeholders as $placeholder) { foreach ($placeholders as $placeholder) {
$userService->assignOrganizationEntitiesToDifferentUser($event->team, $placeholder, $event->user); /** @var Member $placeholder */
$placeholderUser = $placeholder->user;
$userService->assignOrganizationEntitiesToDifferentUser($event->team, $placeholderUser, $event->user);
$placeholder->delete(); $placeholder->delete();
$placeholderUser->delete();
} }
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Models\Concerns\HasUuids; use App\Models\Concerns\HasUuids;
use Database\Factories\MembershipFactory; use Database\Factories\MemberFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Jetstream\Membership as JetstreamMembership; use Laravel\Jetstream\Membership as JetstreamMembership;
@@ -21,9 +21,9 @@ use Laravel\Jetstream\Membership as JetstreamMembership;
* @property-read Organization $organization * @property-read Organization $organization
* @property-read User $user * @property-read User $user
* *
* @method static MembershipFactory factory() * @method static MemberFactory factory()
*/ */
class Membership extends JetstreamMembership class Member extends JetstreamMembership
{ {
use HasFactory; use HasFactory;
use HasUuids; use HasUuids;
@@ -33,10 +33,10 @@ class Membership extends JetstreamMembership
* *
* @var string * @var string
*/ */
protected $table = 'organization_user'; protected $table = 'members';
/** /**
* @return BelongsTo<User, Membership> * @return BelongsTo<User, Member>
*/ */
public function user(): BelongsTo public function user(): BelongsTo
{ {
@@ -44,7 +44,7 @@ class Membership extends JetstreamMembership
} }
/** /**
* @return BelongsTo<Organization, Membership> * @return BelongsTo<Organization, Member>
*/ */
public function organization(): BelongsTo public function organization(): BelongsTo
{ {

View File

@@ -30,7 +30,7 @@ use Laravel\Jetstream\Team as JetstreamTeam;
* @property Collection<int, User> $users * @property Collection<int, User> $users
* @property Collection<int, User> $realUsers * @property Collection<int, User> $realUsers
* @property-read Collection<int, OrganizationInvitation> $teamInvitations * @property-read Collection<int, OrganizationInvitation> $teamInvitations
* @property Membership $membership * @property Member $membership
* *
* @method HasMany<OrganizationInvitation> teamInvitations() * @method HasMany<OrganizationInvitation> teamInvitations()
* @method static OrganizationFactory factory() * @method static OrganizationFactory factory()

View File

@@ -14,9 +14,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* @property string $id * @property string $id
* @property int|null $billable_rate * @property int|null $billable_rate
* @property string $project_id * @property string $project_id Project ID
* @property string $user_id * @property string $member_id Member ID
* @property string $user_id User ID (legacy)
* @property-read Project $project * @property-read Project $project
* @property-read Member $member
* @property-read User $user * @property-read User $user
* *
* @method static Builder<ProjectMember> whereBelongsToOrganization(Organization $organization) * @method static Builder<ProjectMember> whereBelongsToOrganization(Organization $organization)
@@ -45,6 +47,8 @@ class ProjectMember extends Model
} }
/** /**
* @deprecated Use member relationship instead
*
* @return BelongsTo<User, ProjectMember> * @return BelongsTo<User, ProjectMember>
*/ */
public function user(): BelongsTo public function user(): BelongsTo
@@ -52,6 +56,14 @@ class ProjectMember extends Model
return $this->belongsTo(User::class, 'user_id'); return $this->belongsTo(User::class, 'user_id');
} }
/**
* @return BelongsTo<Member, ProjectMember>
*/
public function member(): BelongsTo
{
return $this->belongsTo(Member::class, 'member_id');
}
/** /**
* @param Builder<ProjectMember> $builder * @param Builder<ProjectMember> $builder
*/ */

View File

@@ -65,9 +65,14 @@ class Task extends Model
return $this->hasMany(TimeEntry::class, 'task_id'); return $this->hasMany(TimeEntry::class, 'task_id');
} }
/**
* @param Builder<Task> $builder
* @return Builder<Task>
*/
public function scopeVisibleByUser(Builder $builder, User $user): Builder public function scopeVisibleByUser(Builder $builder, User $user): Builder
{ {
return $builder->whereHas('project', function (Builder $builder) use ($user): Builder { return $builder->whereHas('project', function (Builder $builder) use ($user): Builder {
/** @var Builder<Project> $builder */
return $builder->visibleByUser($user); return $builder->visibleByUser($user);
}); });
} }

View File

@@ -24,7 +24,9 @@ use Korridor\LaravelComputedAttributes\ComputedAttributes;
* @property bool $billable * @property bool $billable
* @property array $tags * @property array $tags
* @property string $user_id * @property string $user_id
* @property string $member_id
* @property-read User $user * @property-read User $user
* @property-read Member $member
* @property string $organization_id * @property string $organization_id
* @property-read Organization $organization * @property-read Organization $organization
* @property string|null $project_id * @property string|null $project_id
@@ -91,6 +93,14 @@ class TimeEntry extends Model
return $this->belongsTo(User::class, 'user_id'); return $this->belongsTo(User::class, 'user_id');
} }
/**
* @return BelongsTo<Member, TimeEntry>
*/
public function member(): BelongsTo
{
return $this->belongsTo(Member::class, 'member_id');
}
/** /**
* @return BelongsTo<Organization, TimeEntry> * @return BelongsTo<Organization, TimeEntry>
*/ */

View File

@@ -43,7 +43,7 @@ use Laravel\Passport\HasApiTokens;
* @property string $current_team_id * @property string $current_team_id
* @property Collection<int, Organization> $organizations * @property Collection<int, Organization> $organizations
* @property Collection<int, TimeEntry> $timeEntries * @property Collection<int, TimeEntry> $timeEntries
* @property Membership $membership * @property Member $membership
* *
* @method HasMany<Organization> ownedTeams() * @method HasMany<Organization> ownedTeams()
* @method static UserFactory factory() * @method static UserFactory factory()
@@ -136,7 +136,7 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
*/ */
public function organizations(): BelongsToMany public function organizations(): BelongsToMany
{ {
return $this->belongsToMany(Organization::class, Membership::class) return $this->belongsToMany(Organization::class, Member::class)
->withPivot([ ->withPivot([
'id', 'id',
'role', 'role',

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use App\Models\Client; use App\Models\Client;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\OrganizationInvitation; use App\Models\OrganizationInvitation;
use App\Models\Project; use App\Models\Project;
@@ -51,7 +51,7 @@ class AppServiceProvider extends ServiceProvider
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction()); Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
Model::preventAccessingMissingAttributes(! $this->app->isProduction()); Model::preventAccessingMissingAttributes(! $this->app->isProduction());
Relation::enforceMorphMap([ Relation::enforceMorphMap([
'membership' => Membership::class, 'membership' => Member::class,
'organization' => Organization::class, 'organization' => Organization::class,
'organization-invitation' => OrganizationInvitation::class, 'organization-invitation' => OrganizationInvitation::class,
'user' => User::class, 'user' => User::class,
@@ -85,7 +85,7 @@ class AppServiceProvider extends ServiceProvider
return new PermissionStore(); return new PermissionStore();
}); });
Route::model('member', Membership::class); Route::model('member', Member::class);
Route::model('invitation', OrganizationInvitation::class); Route::model('invitation', OrganizationInvitation::class);
} }
} }

View File

@@ -14,6 +14,7 @@ use App\Actions\Jetstream\UpdateMemberRole;
use App\Actions\Jetstream\UpdateOrganization; use App\Actions\Jetstream\UpdateOrganization;
use App\Enums\Role; use App\Enums\Role;
use App\Enums\Weekday; use App\Enums\Weekday;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\OrganizationInvitation; use App\Models\OrganizationInvitation;
use App\Models\User; use App\Models\User;
@@ -50,6 +51,7 @@ class JetstreamServiceProvider extends ServiceProvider
Jetstream::deleteTeamsUsing(DeleteOrganization::class); Jetstream::deleteTeamsUsing(DeleteOrganization::class);
Jetstream::deleteUsersUsing(DeleteUser::class); Jetstream::deleteUsersUsing(DeleteUser::class);
Jetstream::useTeamModel(Organization::class); Jetstream::useTeamModel(Organization::class);
Jetstream::useMembershipModel(Member::class);
Jetstream::useTeamInvitationModel(OrganizationInvitation::class); Jetstream::useTeamInvitationModel(OrganizationInvitation::class);
app()->singleton(UpdateTeamMemberRole::class, UpdateMemberRole::class); app()->singleton(UpdateTeamMemberRole::class, UpdateMemberRole::class);
} }

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service; namespace App\Service;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
@@ -36,13 +36,13 @@ class BillableRateService
} }
} }
// Member rate // Member rate
/** @var Membership|null $membership */ /** @var Member|null $member */
$membership = Membership::query() $member = Member::query()
->where('user_id', '=', $timeEntry->user_id) ->where('user_id', '=', $timeEntry->user_id)
->where('organization_id', '=', $timeEntry->organization_id) ->where('organization_id', '=', $timeEntry->organization_id)
->first(); ->first();
if ($membership !== null && $membership->billable_rate !== null) { if ($member !== null && $member->billable_rate !== null) {
return $membership->billable_rate; return $member->billable_rate;
} }
// Organization rate // Organization rate

View File

@@ -335,20 +335,22 @@ class DashboardService
} }
/** /**
* Rhe 4 most recently active members of your team with user_id, name, description of the latest time entry, time_entry_id, task_id and a boolean status if the team member is currently working * Rhe 4 most recently active members of your team with member_id, name, description of the latest time entry, time_entry_id, task_id and a boolean status if the team member is currently working
* *
* @return array<int, array{user_id: string, name: string, description: string|null, time_entry_id: string, task_id: string|null, status: bool }> * @return array<int, array{member_id: string, name: string, description: string|null, time_entry_id: string, task_id: string|null, status: bool }>
*/ */
public function latestTeamActivity(Organization $organization): array public function latestTeamActivity(Organization $organization): array
{ {
$timeEntries = TimeEntry::query() $timeEntries = TimeEntry::query()
->select(DB::raw('distinct on (user_id) user_id, description, id, task_id, start, "end"')) ->select(DB::raw('distinct on (member_id) member_id, description, id, task_id, start, "end"'))
->whereBelongsTo($organization, 'organization') ->whereBelongsTo($organization, 'organization')
->orderBy('user_id') ->orderBy('member_id')
->orderBy('start', 'desc') ->orderBy('start', 'desc')
// Note: limit here does not work because of the distinct on // Note: limit here does not work because of the distinct on
->with([ ->with([
'user', 'member' => [
'user',
],
]) ])
->get() ->get()
->sortByDesc('start') ->sortByDesc('start')
@@ -358,8 +360,8 @@ class DashboardService
foreach ($timeEntries as $timeEntry) { foreach ($timeEntries as $timeEntry) {
$response[] = [ $response[] = [
'user_id' => $timeEntry->user_id, 'member_id' => $timeEntry->member_id,
'name' => $timeEntry->user->name, 'name' => $timeEntry->member->user->name,
'description' => $timeEntry->description, 'description' => $timeEntry->description,
'time_entry_id' => $timeEntry->id, 'time_entry_id' => $timeEntry->id,
'task_id' => $timeEntry->task_id, 'task_id' => $timeEntry->task_id,

View File

@@ -56,6 +56,10 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
'timezone' => 'UTC', 'timezone' => 'UTC',
'is_placeholder' => true, 'is_placeholder' => true,
]); ]);
$memberId = $this->memberImportHelper->getKey([
'user_id' => $userId,
'organization_id' => $this->organization->getKey(),
]);
$clientId = null; $clientId = null;
if ($record['Client'] !== '') { if ($record['Client'] !== '') {
$clientId = $this->clientImportHelper->getKey([ $clientId = $this->clientImportHelper->getKey([
@@ -83,6 +87,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
} }
$timeEntry = new TimeEntry(); $timeEntry = new TimeEntry();
$timeEntry->user_id = $userId; $timeEntry->user_id = $userId;
$timeEntry->member_id = $memberId;
$timeEntry->task_id = $taskId; $timeEntry->task_id = $taskId;
$timeEntry->project_id = $projectId; $timeEntry->project_id = $projectId;
$timeEntry->organization_id = $this->organization->id; $timeEntry->organization_id = $this->organization->id;

View File

@@ -6,6 +6,7 @@ namespace App\Service\Import\Importers;
use App\Enums\Role; use App\Enums\Role;
use App\Models\Client; use App\Models\Client;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
@@ -26,6 +27,11 @@ abstract class DefaultImporter implements ImporterContract
*/ */
protected ImportDatabaseHelper $userImportHelper; protected ImportDatabaseHelper $userImportHelper;
/**
* @var ImportDatabaseHelper<Member>
*/
protected ImportDatabaseHelper $memberImportHelper;
/** /**
* @var ImportDatabaseHelper<Project> * @var ImportDatabaseHelper<Project>
*/ */
@@ -77,6 +83,10 @@ abstract class DefaultImporter implements ImporterContract
'timezone:all', 'timezone:all',
], ],
]); ]);
$this->memberImportHelper = new ImportDatabaseHelper(Member::class, ['user_id', 'organization_id'], true, function (Builder $builder) {
/** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
});
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) { $this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) {
/** @var Builder<Project> $builder */ /** @var Builder<Project> $builder */
return $builder->where('organization_id', $this->organization->id); return $builder->where('organization_id', $this->organization->id);
@@ -90,7 +100,7 @@ abstract class DefaultImporter implements ImporterContract
'integer', 'integer',
], ],
]); ]);
$this->projectMemberImportHelper = new ImportDatabaseHelper(ProjectMember::class, ['project_id', 'user_id'], true, function (Builder $builder) { $this->projectMemberImportHelper = new ImportDatabaseHelper(ProjectMember::class, ['project_id', 'member_id'], true, function (Builder $builder) {
/** @var Builder<ProjectMember> $builder */ /** @var Builder<ProjectMember> $builder */
return $builder->whereBelongsToOrganization($this->organization); return $builder->whereBelongsToOrganization($this->organization);
}, validate: [ }, validate: [

View File

@@ -74,13 +74,17 @@ class TogglDataImporter extends DefaultImporter
} }
foreach ($workspaceUsers as $workspaceUser) { foreach ($workspaceUsers as $workspaceUser) {
$this->userImportHelper->getKey([ $userId = $this->userImportHelper->getKey([
'email' => $workspaceUser->email, 'email' => $workspaceUser->email,
], [ ], [
'name' => $workspaceUser->name, 'name' => $workspaceUser->name,
'timezone' => $workspaceUser->timezone ?? 'UTC', 'timezone' => $workspaceUser->timezone ?? 'UTC',
'is_placeholder' => true, 'is_placeholder' => true,
], (string) $workspaceUser->uid); ], (string) $workspaceUser->uid);
$memberId = $this->memberImportHelper->getKey([
'user_id' => $userId,
'organization_id' => $this->organization->getKey(),
], [], $userId);
} }
foreach ($projects as $project) { foreach ($projects as $project) {
@@ -114,10 +118,12 @@ class TogglDataImporter extends DefaultImporter
} }
$projectMembers = json_decode($projectMembersFileContent); $projectMembers = json_decode($projectMembersFileContent);
foreach ($projectMembers as $projectMember) { foreach ($projectMembers as $projectMember) {
$userId = $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id);
$this->projectMemberImportHelper->getKey([ $this->projectMemberImportHelper->getKey([
'project_id' => $projectId, 'project_id' => $projectId,
'user_id' => $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id), 'member_id' => $this->memberImportHelper->getKeyByExternalIdentifier($userId),
], [ ], [
'user_id' => $userId,
'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null, 'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null,
]); ]);
} }

View File

@@ -56,6 +56,10 @@ class TogglTimeEntriesImporter extends DefaultImporter
'timezone' => 'UTC', 'timezone' => 'UTC',
'is_placeholder' => true, 'is_placeholder' => true,
]); ]);
$memberId = $this->memberImportHelper->getKey([
'user_id' => $userId,
'organization_id' => $this->organization->getKey(),
]);
$clientId = null; $clientId = null;
if ($record['Client'] !== '') { if ($record['Client'] !== '') {
$clientId = $this->clientImportHelper->getKey([ $clientId = $this->clientImportHelper->getKey([
@@ -83,6 +87,7 @@ class TogglTimeEntriesImporter extends DefaultImporter
} }
$timeEntry = new TimeEntry(); $timeEntry = new TimeEntry();
$timeEntry->user_id = $userId; $timeEntry->user_id = $userId;
$timeEntry->member_id = $memberId;
$timeEntry->task_id = $taskId; $timeEntry->task_id = $taskId;
$timeEntry->project_id = $projectId; $timeEntry->project_id = $projectId;
$timeEntry->organization_id = $this->organization->id; $timeEntry->organization_id = $this->organization->id;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Service; namespace App\Service;
use App\Models\Member;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@@ -59,12 +60,25 @@ class TimeEntryFilter
return $this; return $this;
} }
public function addUserIdFilter(?string $userId): self public function addMemberIdFilter(?Member $member): self
{ {
if ($userId === null) { if ($member === null) {
return $this; return $this;
} }
$this->builder->where('user_id', $userId); $this->builder->where('member_id', $member->getKey());
return $this;
}
/**
* @param array<string>|null $memberIds
*/
public function addMemberIdsFilter(?array $memberIds): self
{
if ($memberIds === null) {
return $this;
}
$this->builder->whereIn('member_id', $memberIds);
return $this; return $this;
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Service; namespace App\Service;
use App\Enums\Role; use App\Enums\Role;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\TimeEntry; use App\Models\TimeEntry;
@@ -19,12 +19,22 @@ class UserService
*/ */
public function assignOrganizationEntitiesToDifferentUser(Organization $organization, User $fromUser, User $toUser): void public function assignOrganizationEntitiesToDifferentUser(Organization $organization, User $fromUser, User $toUser): void
{ {
/** @var Member|null $toMember */
$toMember = Member::query()
->whereBelongsTo($organization, 'organization')
->whereBelongsTo($toUser, 'user')
->first();
if ($toMember === null) {
throw new \InvalidArgumentException('User is not a member of the organization');
}
// Time entries // Time entries
TimeEntry::query() TimeEntry::query()
->whereBelongsTo($organization, 'organization') ->whereBelongsTo($organization, 'organization')
->whereBelongsTo($fromUser, 'user') ->whereBelongsTo($fromUser, 'user')
->update([ ->update([
'user_id' => $toUser->getKey(), 'user_id' => $toUser->getKey(),
'member_id' => $toMember->getKey(),
]); ]);
// Project members // Project members
@@ -33,6 +43,7 @@ class UserService
->whereBelongsTo($fromUser, 'user') ->whereBelongsTo($fromUser, 'user')
->update([ ->update([
'user_id' => $toUser->getKey(), 'user_id' => $toUser->getKey(),
'member_id' => $toMember->getKey(),
]); ]);
} }
@@ -45,13 +56,17 @@ class UserService
$organization->update([ $organization->update([
'user_id' => $newOwner->getKey(), 'user_id' => $newOwner->getKey(),
]); ]);
$userMembership = Membership::query() /** @var Member|null $userMembership */
$userMembership = Member::query()
->whereBelongsTo($organization, 'organization') ->whereBelongsTo($organization, 'organization')
->whereBelongsTo($newOwner, 'user') ->whereBelongsTo($newOwner, 'user')
->first(); ->first();
if ($userMembership === null) {
throw new \InvalidArgumentException('User is not a member of the organization');
}
$userMembership->role = Role::Owner->value; $userMembership->role = Role::Owner->value;
$userMembership->save(); $userMembership->save();
$oldOwners = Membership::query() $oldOwners = Member::query()
->whereBelongsTo($organization, 'organization') ->whereBelongsTo($organization, 'organization')
->where('role', '=', Role::Owner->value) ->where('role', '=', Role::Owner->value)
->where('user_id', '!=', $newOwner->getKey()) ->where('user_id', '!=', $newOwner->getKey())

View File

@@ -5,15 +5,15 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Enums\Role; use App\Enums\Role;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends Factory<Membership> * @extends Factory<Member>
*/ */
class MembershipFactory extends Factory class MemberFactory extends Factory
{ {
/** /**
* Define the model's default state. * Define the model's default state.
@@ -24,11 +24,20 @@ class MembershipFactory extends Factory
{ {
return [ return [
'role' => Role::Employee, 'role' => Role::Employee,
'organization_id' => OrganizationFactory::class, 'organization_id' => Organization::factory(),
'user_id' => UserFactory::class, 'user_id' => User::factory(),
]; ];
} }
public function role(Role $role): static
{
return $this->state(function (array $attributes) use ($role): array {
return [
'role' => $role->value,
];
});
}
public function forOrganization(Organization $organization): static public function forOrganization(Organization $organization): static
{ {
return $this->state(function (array $attributes) use ($organization): array { return $this->state(function (array $attributes) use ($organization): array {

View File

@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Client; use App\Models\Client;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\User;
use App\Service\ColorService; use App\Service\ColorService;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
@@ -61,12 +61,12 @@ class ProjectFactory extends Factory
}); });
} }
public function addMember(User $user, array $attributes = []): self public function addMember(Member $member, array $attributes = []): self
{ {
return $this->afterCreating(function (Project $project) use ($user, $attributes): void { return $this->afterCreating(function (Project $project) use ($member, $attributes): void {
ProjectMember::factory() ProjectMember::factory()
->forProject($project) ->forProject($project)
->forUser($user) ->forMember($member)
->create($attributes); ->create($attributes);
}); });
} }

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Member;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\User; use App\Models\User;
@@ -25,9 +26,13 @@ class ProjectMemberFactory extends Factory
'billable_rate' => $this->faker->numberBetween(10, 10000) * 100, 'billable_rate' => $this->faker->numberBetween(10, 10000) * 100,
'project_id' => Project::factory(), 'project_id' => Project::factory(),
'user_id' => User::factory(), 'user_id' => User::factory(),
'member_id' => Member::factory(),
]; ];
} }
/**
* @deprecated Use forMember instead
*/
public function forUser(User $user): self public function forUser(User $user): self
{ {
return $this->state(function (array $attributes) use ($user): array { return $this->state(function (array $attributes) use ($user): array {
@@ -37,6 +42,16 @@ class ProjectMemberFactory extends Factory
}); });
} }
public function forMember(Member $member): self
{
return $this->state(function (array $attributes) use ($member): array {
return [
'member_id' => $member->getKey(),
'user_id' => $member->user_id, // Legacy
];
});
}
public function forProject(Project $project): self public function forProject(Project $project): self
{ {
return $this->state(function (array $attributes) use ($project): array { return $this->state(function (array $attributes) use ($project): array {

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Tag; use App\Models\Tag;
@@ -34,6 +35,7 @@ class TimeEntryFactory extends Factory
'billable' => $this->faker->boolean(), 'billable' => $this->faker->boolean(),
'tags' => [], 'tags' => [],
'user_id' => User::factory(), 'user_id' => User::factory(),
'member_id' => Member::factory(),
'task_id' => null, 'task_id' => null,
'project_id' => null, 'project_id' => null,
'organization_id' => Organization::factory(), 'organization_id' => Organization::factory(),
@@ -86,6 +88,9 @@ class TimeEntryFactory extends Factory
}); });
} }
/**
* @deprecated Use forMember instead
*/
public function forUser(User $user): self public function forUser(User $user): self
{ {
return $this->state(function (array $attributes) use ($user) { return $this->state(function (array $attributes) use ($user) {
@@ -95,6 +100,17 @@ class TimeEntryFactory extends Factory
}); });
} }
public function forMember(Member $member): static
{
return $this->state(function (array $attributes) use ($member): array {
return [
'member_id' => $member->getKey(),
'user_id' => $member->user_id,
'organization_id' => $member->organization_id,
];
});
}
public function forOrganization(Organization $organization): self public function forOrganization(Organization $organization): self
{ {
return $this->state(function (array $attributes) use ($organization) { return $this->state(function (array $attributes) use ($organization) {

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('project_members', function (Blueprint $table): void {
$table->foreignUuid('member_id')
->nullable()
->constrained('organization_user')
->cascadeOnDelete()
->cascadeOnUpdate();
});
DB::statement('
update project_members
set member_id = organization_user.id
from projects
join organization_user on organization_user.organization_id = projects.organization_id
where projects.id = project_members.project_id and project_members.user_id = organization_user.user_id
');
Schema::table('project_members', function (Blueprint $table): void {
$table->uuid('member_id')->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('project_members', function (Blueprint $table): void {
$table->dropForeign(['member_id']);
$table->dropColumn('member_id');
});
}
};

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('time_entries', function (Blueprint $table): void {
$table->foreignUuid('member_id')
->nullable()
->constrained('organization_user')
->cascadeOnDelete()
->cascadeOnUpdate();
});
DB::statement('
update time_entries
set member_id = organization_user.id
from organization_user
where time_entries.organization_id = organization_user.organization_id and
time_entries.user_id = organization_user.user_id
');
Schema::table('time_entries', function (Blueprint $table): void {
$table->uuid('member_id')->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('time_entries', function (Blueprint $table): void {
$table->dropForeign(['member_id']);
$table->dropColumn('member_id');
});
}
};

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::rename('organization_user', 'members');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::rename('members', 'organization_user');
}
};

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Database\Seeders; namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Enums\Role; use App\Enums\Role;
use App\Models\Client; use App\Models\Client;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
@@ -25,6 +25,11 @@ class DatabaseSeeder extends Seeder
public function run(): void public function run(): void
{ {
$this->deleteAll(); $this->deleteAll();
$userWithMultipleOrganizations = User::factory()->withPersonalOrganization()->create([
'name' => 'Mister Overemployed',
'email' => 'overemployed@acme.test',
]);
$userAcmeOwner = User::factory()->withPersonalOrganization()->create([ $userAcmeOwner = User::factory()->withPersonalOrganization()->create([
'name' => 'Acme Owner', 'name' => 'Acme Owner',
'email' => 'owner@acme.test', 'email' => 'owner@acme.test',
@@ -34,7 +39,7 @@ class DatabaseSeeder extends Seeder
'personal_team' => false, 'personal_team' => false,
'currency' => 'EUR', 'currency' => 'EUR',
]); ]);
$userAcmeManager = User::factory()->withPersonalOrganization()->create([ $userRivalManager = User::factory()->withPersonalOrganization()->create([
'name' => 'Acme Manager', 'name' => 'Acme Manager',
'email' => 'test@example.com', 'email' => 'test@example.com',
]); ]);
@@ -51,36 +56,28 @@ class DatabaseSeeder extends Seeder
'email' => 'old.employee@acme.test', 'email' => 'old.employee@acme.test',
'password' => null, 'password' => null,
]); ]);
$userAcmeOwner->organizations()->attach($organizationAcme, [ $userAcmeOwnerMember = Member::factory()->forUser($userAcmeOwner)->forOrganization($organizationAcme)->role(Role::Owner)->create();
'role' => Role::Owner->value, $userAcmeManagerMember = Member::factory()->forUser($userRivalManager)->forOrganization($organizationAcme)->role(Role::Manager)->create();
]); $userAcmeAdminMember = Member::factory()->forUser($userAcmeAdmin)->forOrganization($organizationAcme)->role(Role::Admin)->create();
$userAcmeManager->organizations()->attach($organizationAcme, [ $userAcmeEmployeeMember = Member::factory()->forUser($userAcmeEmployee)->forOrganization($organizationAcme)->role(Role::Employee)->create();
'role' => Role::Manager->value, $userAcmePlaceholderMember = Member::factory()->forUser($userAcmePlaceholder)->forOrganization($organizationAcme)->role(Role::Placeholder)->create();
]); $userWithMultipleOrganizationsAcmeMember = Member::factory()->forUser($userWithMultipleOrganizations)->forOrganization($organizationAcme)->role(Role::Employee)->create();
$userAcmeAdmin->organizations()->attach($organizationAcme, [
'role' => Role::Admin->value,
]);
$userAcmeEmployee->organizations()->attach($organizationAcme, [
'role' => Role::Employee->value,
]);
$userAcmePlaceholder->organizations()->attach($organizationAcme, [
'role' => Role::Placeholder->value,
]);
$timeEntriesAcmeAdmin = TimeEntry::factory() TimeEntry::factory()
->count(10) ->count(10)
->forUser($userAcmeAdmin) ->forMember($userAcmeAdminMember)
->forOrganization($organizationAcme)
->create(); ->create();
$timeEntriesAcmePlaceholder = TimeEntry::factory() TimeEntry::factory()
->count(10) ->count(10)
->forUser($userAcmePlaceholder) ->forMember($userAcmePlaceholderMember)
->forOrganization($organizationAcme)
->create(); ->create();
$timeEntriesAcmePlaceholder = TimeEntry::factory() TimeEntry::factory()
->count(10) ->count(10)
->forUser($userAcmeEmployee) ->forMember($userAcmeEmployeeMember)
->forOrganization($organizationAcme) ->create();
TimeEntry::factory()
->count(5)
->forMember($userWithMultipleOrganizationsAcmeMember)
->create(); ->create();
$client = Client::factory()->forOrganization($organizationAcme)->create([ $client = Client::factory()->forOrganization($organizationAcme)->create([
'name' => 'Big Company', 'name' => 'Big Company',
@@ -88,6 +85,10 @@ class DatabaseSeeder extends Seeder
$bigCompanyProject = Project::factory()->forOrganization($organizationAcme)->forClient($client)->create([ $bigCompanyProject = Project::factory()->forOrganization($organizationAcme)->forClient($client)->create([
'name' => 'Big Company Project', 'name' => 'Big Company Project',
]); ]);
ProjectMember::factory()->forProject($bigCompanyProject)->forMember($userAcmeEmployeeMember)->create();
ProjectMember::factory()->forProject($bigCompanyProject)->forMember($userAcmeAdminMember)->create();
ProjectMember::factory()->forProject($bigCompanyProject)->forMember($userWithMultipleOrganizationsAcmeMember)->create();
Task::factory()->forOrganization($organizationAcme)->forProject($bigCompanyProject)->create(); Task::factory()->forOrganization($organizationAcme)->forProject($bigCompanyProject)->create();
$internalProject = Project::factory()->forOrganization($organizationAcme)->create([ $internalProject = Project::factory()->forOrganization($organizationAcme)->create([
@@ -98,21 +99,26 @@ class DatabaseSeeder extends Seeder
'name' => 'Other Owner', 'name' => 'Other Owner',
'email' => 'owner@rival-company.test', 'email' => 'owner@rival-company.test',
]); ]);
$organization2 = Organization::factory()->withOwner($organization2Owner)->create([ $organizationRival = Organization::factory()->withOwner($organization2Owner)->create([
'name' => 'Rival Corp', 'name' => 'Rival Corp',
'personal_team' => true, 'personal_team' => true,
'currency' => 'USD', 'currency' => 'USD',
]); ]);
$userAcmeManager = User::factory()->withPersonalOrganization()->create([ $userRivalManager = User::factory()->withPersonalOrganization()->create([
'name' => 'Other User', 'name' => 'Other User',
'email' => 'test@rival-company.test', 'email' => 'test@rival-company.test',
]); ]);
$userAcmeManager->organizations()->attach($organization2, [ $userRivalManagerMember = Member::factory()->forUser($userRivalManager)->forOrganization($organizationRival)->role(Role::Admin)->create();
'role' => Role::Admin->value, $userWithMultipleOrganizationsRivalMember = Member::factory()->forUser($userWithMultipleOrganizations)->forOrganization($organizationRival)->role(Role::Employee)->create();
]); $otherCompanyProject = Project::factory()->forOrganization($organizationRival)->forClient($client)->create([
$otherCompanyProject = Project::factory()->forOrganization($organization2)->forClient($client)->create([
'name' => 'Scale Company', 'name' => 'Scale Company',
]); ]);
ProjectMember::factory()->forProject($otherCompanyProject)->forMember($userRivalManagerMember)->create();
ProjectMember::factory()->forProject($otherCompanyProject)->forMember($userWithMultipleOrganizationsRivalMember)->create();
TimeEntry::factory()
->count(5)
->forMember($userWithMultipleOrganizationsRivalMember)
->create();
User::factory()->withPersonalOrganization()->create([ User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com', 'email' => 'admin@example.com',

View File

@@ -5,7 +5,8 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use App\Enums\Role; use App\Enums\Role;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@@ -19,6 +20,7 @@ class CreateTeamTest extends TestCase
// Arrange // Arrange
$user = User::factory()->withPersonalOrganization()->create(); $user = User::factory()->withPersonalOrganization()->create();
$this->actingAs($user); $this->actingAs($user);
sleep(1);
// Act // Act
$response = $this->post('/teams', [ $response = $this->post('/teams', [
@@ -26,10 +28,13 @@ class CreateTeamTest extends TestCase
]); ]);
// Assert // Assert
$newOrganization = $user->fresh()->ownedTeams()->latest('id')->first(); /** @var Organization|null $newOrganization */
$this->assertCount(2, $user->fresh()->ownedTeams); $ownedTeams = $user->fresh()->ownedTeams;
$this->assertEquals('Test Organization', $newOrganization->name); $this->assertCount(2, $ownedTeams);
$member = Membership::query()->whereBelongsTo($user, 'user')->whereBelongsTo($newOrganization, 'organization')->firstOrFail(); $this->assertTrue($ownedTeams->contains('name', 'Test Organization'));
$newOrganization = $ownedTeams->firstWhere('name', 'Test Organization');
/** @var Member $member */
$member = Member::query()->whereBelongsTo($user, 'user')->whereBelongsTo($newOrganization, 'organization')->firstOrFail();
$this->assertSame(Role::Owner->value, $member->role); $this->assertSame(Role::Owner->value, $member->role);
} }
} }

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use App\Enums\Role;
use App\Models\Member;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -62,13 +64,13 @@ class InviteTeamMemberTest extends TestCase
$existingUser = User::factory()->create([ $existingUser = User::factory()->create([
'is_placeholder' => true, 'is_placeholder' => true,
]); ]);
$user->currentTeam->users()->attach($existingUser, ['role' => 'employee']); $user->currentTeam->users()->attach($existingUser, ['role' => Role::Employee->value]);
$this->actingAs($user); $this->actingAs($user);
// Act // Act
$response = $this->post('/teams/'.$user->currentTeam->id.'/members', [ $response = $this->post('/teams/'.$user->currentTeam->id.'/members', [
'email' => $existingUser->email, 'email' => $existingUser->email,
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Assert // Assert
@@ -103,7 +105,7 @@ class InviteTeamMemberTest extends TestCase
$user = User::factory()->withPersonalOrganization()->create(); $user = User::factory()->withPersonalOrganization()->create();
$invitation = $owner->currentTeam->teamInvitations()->create([ $invitation = $owner->currentTeam->teamInvitations()->create([
'email' => $user->email, 'email' => $user->email,
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
$this->actingAs($user); $this->actingAs($user);
@@ -126,11 +128,11 @@ class InviteTeamMemberTest extends TestCase
{ {
// Arrange // Arrange
Mail::fake(); Mail::fake();
$placeholder = User::factory()->withPersonalOrganization()->placeholder()->create(); $placeholder = User::factory()->placeholder()->create();
$owner = User::factory()->withPersonalOrganization()->create(); $owner = User::factory()->withPersonalOrganization()->create();
$owner->currentTeam->users()->attach($placeholder, ['role' => 'employee']); $placeholderMember = Member::factory()->forOrganization($owner->currentTeam)->forUser($placeholder)->create();
$timeEntries = TimeEntry::factory()->forOrganization($owner->currentTeam)->forUser($placeholder)->createMany(5);
$timeEntries = TimeEntry::factory()->forOrganization($owner->currentTeam)->forMember($placeholderMember)->createMany(5);
$user = User::factory()->withPersonalOrganization()->create([ $user = User::factory()->withPersonalOrganization()->create([
'email' => $placeholder->email, 'email' => $placeholder->email,
@@ -138,7 +140,7 @@ class InviteTeamMemberTest extends TestCase
$invitation = $owner->currentTeam->teamInvitations()->create([ $invitation = $owner->currentTeam->teamInvitations()->create([
'email' => $user->email, 'email' => $user->email,
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
$this->actingAs($user); $this->actingAs($user);
@@ -167,7 +169,7 @@ class InviteTeamMemberTest extends TestCase
$user = User::factory()->withPersonalOrganization()->create(); $user = User::factory()->withPersonalOrganization()->create();
$invitation = $owner->currentTeam->teamInvitations()->create([ $invitation = $owner->currentTeam->teamInvitations()->create([
'email' => 'firstname.lastname@mail.test', 'email' => 'firstname.lastname@mail.test',
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
$this->actingAs($user); $this->actingAs($user);

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use App\Enums\Role; use App\Enums\Role;
use App\Models\Membership; use App\Models\Member;
use App\Models\User; use App\Models\User;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -58,7 +58,7 @@ class RegistrationTest extends TestCase
$this->assertSame('UTC', $user->timezone); $this->assertSame('UTC', $user->timezone);
$organization = $user->organizations()->firstOrFail(); $organization = $user->organizations()->firstOrFail();
$this->assertSame(true, $organization->personal_team); $this->assertSame(true, $organization->personal_team);
$member = Membership::query()->whereBelongsTo($user, 'user')->whereBelongsTo($organization, 'organization')->firstOrFail(); $member = Member::query()->whereBelongsTo($user, 'user')->whereBelongsTo($organization, 'organization')->firstOrFail();
$this->assertSame(Role::Owner->value, $member->role); $this->assertSame(Role::Owner->value, $member->role);
} }

View File

@@ -25,12 +25,12 @@ class UpdateTeamMemberRoleTest extends TestCase
// Act // Act
$response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ $response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Assert // Assert
$this->assertTrue($otherUser->fresh()->hasTeamRole( $this->assertTrue($otherUser->fresh()->hasTeamRole(
$user->currentTeam->fresh(), 'employee' $user->currentTeam->fresh(), Role::Employee->value,
)); ));
} }
@@ -88,7 +88,7 @@ class UpdateTeamMemberRoleTest extends TestCase
// Act // Act
$response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ $response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Assert // Assert

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1; namespace Tests\Unit\Endpoint\Api\V1;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -17,7 +17,7 @@ class ApiEndpointTestAbstract extends TestCase
/** /**
* @param array<string> $permissions * @param array<string> $permissions
* @return object{user: User, organization: Organization, member: Membership} * @return object{user: User, organization: Organization, member: Member}
*/ */
protected function createUserWithPermission(array $permissions, bool $isOwner = false): object protected function createUserWithPermission(array $permissions, bool $isOwner = false): object
{ {
@@ -29,14 +29,14 @@ class ApiEndpointTestAbstract extends TestCase
} else { } else {
$organization = Organization::factory()->create(); $organization = Organization::factory()->create();
} }
$membership = Membership::factory()->forUser($user)->forOrganization($organization)->create([ $member = Member::factory()->forUser($user)->forOrganization($organization)->create([
'role' => 'custom-test', 'role' => 'custom-test',
]); ]);
return (object) [ return (object) [
'user' => $user, 'user' => $user,
'organization' => $organization, 'organization' => $organization,
'member' => $membership, 'member' => $member,
]; ];
} }
} }

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1; namespace Tests\Unit\Endpoint\Api\V1;
use App\Enums\Role;
use App\Models\OrganizationInvitation; use App\Models\OrganizationInvitation;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Laravel\Jetstream\Mail\TeamInvitation; use Laravel\Jetstream\Mail\TeamInvitation;
@@ -50,7 +51,7 @@ class InvitationEndpointTest extends ApiEndpointTestAbstract
// Act // Act
$response = $this->postJson(route('api.v1.invitations.store', $data->organization->getKey()), [ $response = $this->postJson(route('api.v1.invitations.store', $data->organization->getKey()), [
'email' => 'test@mail.test', 'email' => 'test@mail.test',
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Assert // Assert
@@ -68,7 +69,7 @@ class InvitationEndpointTest extends ApiEndpointTestAbstract
// Act // Act
$response = $this->postJson(route('api.v1.invitations.store', $data->organization->getKey()), [ $response = $this->postJson(route('api.v1.invitations.store', $data->organization->getKey()), [
'email' => 'test@asdf.at', 'email' => 'test@asdf.at',
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Assert // Assert
@@ -76,7 +77,7 @@ class InvitationEndpointTest extends ApiEndpointTestAbstract
$invitation = OrganizationInvitation::first(); $invitation = OrganizationInvitation::first();
$this->assertNotNull($invitation); $this->assertNotNull($invitation);
$this->assertEquals('test@asdf.at', $invitation->email); $this->assertEquals('test@asdf.at', $invitation->email);
$this->assertEquals('employee', $invitation->role); $this->assertEquals(Role::Employee->value, $invitation->role);
} }
public function test_resend_fails_if_user_has_no_permission_to_resend_the_invitation(): void public function test_resend_fails_if_user_has_no_permission_to_resend_the_invitation(): void

View File

@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1; namespace Tests\Unit\Endpoint\Api\V1;
use App\Models\Membership; use App\Enums\Role;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
@@ -53,7 +54,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
// Act // Act
$response = $this->putJson(route('api.v1.members.update', [$data->organization->getKey(), $data->member->getKey()]), [ $response = $this->putJson(route('api.v1.members.update', [$data->organization->getKey(), $data->member->getKey()]), [
'billable_rate' => 10001, 'billable_rate' => 10001,
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Assert // Assert
@@ -74,7 +75,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
// Act // Act
$response = $this->putJson(route('api.v1.members.update', [$data->organization->getKey(), $otherData->member->getKey()]), [ $response = $this->putJson(route('api.v1.members.update', [$data->organization->getKey(), $otherData->member->getKey()]), [
'billable_rate' => 10001, 'billable_rate' => 10001,
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Assert // Assert
@@ -92,7 +93,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
// Act // Act
$response = $this->putJson(route('api.v1.members.update', [$data->organization->id, $data->member]), [ $response = $this->putJson(route('api.v1.members.update', [$data->organization->id, $data->member]), [
'billable_rate' => 10001, 'billable_rate' => 10001,
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Assert // Assert
@@ -100,7 +101,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
$member = $data->member; $member = $data->member;
$member->refresh(); $member->refresh();
$this->assertSame(10001, $member->billable_rate); $this->assertSame(10001, $member->billable_rate);
$this->assertSame('employee', $member->role); $this->assertSame(Role::Employee->value, $member->role);
} }
public function test_invite_placeholder_succeeds_if_data_is_valid(): void public function test_invite_placeholder_succeeds_if_data_is_valid(): void
@@ -112,7 +113,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
$user = User::factory()->create([ $user = User::factory()->create([
'is_placeholder' => true, 'is_placeholder' => true,
]); ]);
$member = Membership::factory()->forUser($user)->forOrganization($data->organization)->create(); $member = Member::factory()->forUser($user)->forOrganization($data->organization)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -164,7 +165,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'members:delete', 'members:delete',
]); ]);
TimeEntry::factory()->forUser($data->user)->forOrganization($data->organization)->create(); TimeEntry::factory()->forMember($data->member)->forOrganization($data->organization)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -173,7 +174,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
// Assert // Assert
$response->assertStatus(400); $response->assertStatus(400);
$response->assertJsonPath('message', 'The member is still used by a time entry and can not be deleted.'); $response->assertJsonPath('message', 'The member is still used by a time entry and can not be deleted.');
$this->assertDatabaseHas(Membership::class, [ $this->assertDatabaseHas(Member::class, [
'id' => $data->member->getKey(), 'id' => $data->member->getKey(),
]); ]);
} }
@@ -185,7 +186,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
'members:delete', 'members:delete',
]); ]);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
ProjectMember::factory()->forProject($project)->forUser($data->user)->create(); ProjectMember::factory()->forProject($project)->forMember($data->member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -194,7 +195,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
// Assert // Assert
$response->assertStatus(400); $response->assertStatus(400);
$response->assertJsonPath('message', 'The member is still used by a project member and can not be deleted.'); $response->assertJsonPath('message', 'The member is still used by a project member and can not be deleted.');
$this->assertDatabaseHas(Membership::class, [ $this->assertDatabaseHas(Member::class, [
'id' => $data->member->getKey(), 'id' => $data->member->getKey(),
]); ]);
} }
@@ -212,7 +213,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
// Assert // Assert
$response->assertStatus(204); $response->assertStatus(204);
$this->assertDatabaseMissing(Membership::class, [ $this->assertDatabaseMissing(Member::class, [
'id' => $data->member->getKey(), 'id' => $data->member->getKey(),
]); ]);
} }
@@ -225,7 +226,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
$user = User::factory()->create([ $user = User::factory()->create([
'is_placeholder' => true, 'is_placeholder' => true,
]); ]);
$member = Membership::factory()->forUser($user)->forOrganization($data->organization)->create(); $member = Member::factory()->forUser($user)->forOrganization($data->organization)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -249,7 +250,7 @@ class MemberEndpointTest extends ApiEndpointTestAbstract
$user = User::factory()->create([ $user = User::factory()->create([
'is_placeholder' => true, 'is_placeholder' => true,
]); ]);
$member = Membership::factory()->forUser($user)->forOrganization($otherOrganization)->create(); $member = Member::factory()->forUser($user)->forOrganization($otherOrganization)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act

View File

@@ -55,7 +55,7 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
]); ]);
$privateProjects = Project::factory()->forOrganization($data->organization)->isPrivate()->createMany(2); $privateProjects = Project::factory()->forOrganization($data->organization)->isPrivate()->createMany(2);
$publicProjects = Project::factory()->forOrganization($data->organization)->isPublic()->createMany(2); $publicProjects = Project::factory()->forOrganization($data->organization)->isPublic()->createMany(2);
$privateProjectsWithMembership = Project::factory()->forOrganization($data->organization)->addMember($data->user)->isPrivate()->createMany(2); $privateProjectsWithMembership = Project::factory()->forOrganization($data->organization)->addMember($data->member)->isPrivate()->createMany(2);
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1; namespace Tests\Unit\Endpoint\Api\V1;
use App\Models\Member;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\User; use App\Models\User;
@@ -81,13 +82,14 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]); ]);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
$projectMemberFake = ProjectMember::factory()->make(); $projectMemberFake = ProjectMember::factory()->make();
$user = User::factory()->attachToOrganization($data->organization)->create(); $user = User::factory()->create();
$member = Member::factory()->forOrganization($data->organization)->forUser($user)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [ $response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
]); ]);
// Assert // Assert
@@ -105,13 +107,14 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]); ]);
$project = Project::factory()->forOrganization($otherData->organization)->create(); $project = Project::factory()->forOrganization($otherData->organization)->create();
$projectMemberFake = ProjectMember::factory()->make(); $projectMemberFake = ProjectMember::factory()->make();
$user = User::factory()->attachToOrganization($data->organization)->create(); $user = User::factory()->create();
$member = Member::factory()->forOrganization($data->organization)->forUser($user)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [ $response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
]); ]);
// Assert // Assert
@@ -129,17 +132,18 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]); ]);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
$projectMemberFake = ProjectMember::factory()->make(); $projectMemberFake = ProjectMember::factory()->make();
$user = User::factory()->attachToOrganization($otherData->organization)->create(); $user = User::factory()->create();
$member = Member::factory()->forOrganization($otherData->organization)->forUser($user)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [ $response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
]); ]);
// Assert // Assert
$response->assertInvalid(['user_id']); $response->assertInvalid(['member_id']);
} }
public function test_store_endpoint_fails_if_user_is_a_placeholder(): void public function test_store_endpoint_fails_if_user_is_a_placeholder(): void
@@ -150,13 +154,14 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]); ]);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
$projectMemberFake = ProjectMember::factory()->make(); $projectMemberFake = ProjectMember::factory()->make();
$user = User::factory()->attachToOrganization($data->organization)->placeholder()->create(); $user = User::factory()->placeholder()->create();
$member = Member::factory()->forOrganization($data->organization)->forUser($user)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [ $response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
]); ]);
// Assert // Assert
@@ -168,7 +173,7 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]); ]);
$this->assertDatabaseMissing(ProjectMember::class, [ $this->assertDatabaseMissing(ProjectMember::class, [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
'project_id' => $project->getKey(), 'project_id' => $project->getKey(),
]); ]);
} }
@@ -181,14 +186,14 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]); ]);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
$projectMemberFake = ProjectMember::factory()->make(); $projectMemberFake = ProjectMember::factory()->make();
$user = User::factory()->attachToOrganization($data->organization)->create(); $member = Member::factory()->forOrganization($data->organization)->create();
ProjectMember::factory()->forProject($project)->forUser($user)->create(); ProjectMember::factory()->forProject($project)->forMember($member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [ $response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
]); ]);
// Assert // Assert
@@ -200,7 +205,7 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]); ]);
$this->assertDatabaseMissing(ProjectMember::class, [ $this->assertDatabaseMissing(ProjectMember::class, [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
'project_id' => $project->getKey(), 'project_id' => $project->getKey(),
]); ]);
} }
@@ -213,20 +218,21 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
]); ]);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
$projectMemberFake = ProjectMember::factory()->make(); $projectMemberFake = ProjectMember::factory()->make();
$user = User::factory()->attachToOrganization($data->organization)->create(); $user = User::factory()->create();
$member = Member::factory()->forOrganization($data->organization)->forUser($user)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [ $response = $this->postJson(route('api.v1.project-members.store', [$data->organization->getKey(), $project->getKey()]), [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
]); ]);
// Assert // Assert
$response->assertStatus(201); $response->assertStatus(201);
$this->assertDatabaseHas(ProjectMember::class, [ $this->assertDatabaseHas(ProjectMember::class, [
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
'project_id' => $project->getKey(), 'project_id' => $project->getKey(),
]); ]);
} }
@@ -294,7 +300,7 @@ class ProjectMemberEndpointTest extends ApiEndpointTestAbstract
$this->assertDatabaseHas(ProjectMember::class, [ $this->assertDatabaseHas(ProjectMember::class, [
'id' => $projectMember->getKey(), 'id' => $projectMember->getKey(),
'billable_rate' => $projectMemberFake->billable_rate, 'billable_rate' => $projectMemberFake->billable_rate,
'user_id' => $projectMember->user_id, 'member_id' => $projectMember->member_id,
]); ]);
} }

View File

@@ -207,7 +207,7 @@ class TagEndpointTest extends ApiEndpointTestAbstract
'tags:delete', 'tags:delete',
]); ]);
$tag = Tag::factory()->forOrganization($data->organization)->create(); $tag = Tag::factory()->forOrganization($data->organization)->create();
TimeEntry::factory()->forUser($data->user)->forOrganization($data->organization)->create([ TimeEntry::factory()->forMember($data->member)->forOrganization($data->organization)->create([
'tags' => [$tag->getKey()], 'tags' => [$tag->getKey()],
]); ]);
Passport::actingAs($data->user); Passport::actingAs($data->user);

View File

@@ -86,7 +86,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
$projectPublic = Project::factory()->isPublic()->create(); $projectPublic = Project::factory()->isPublic()->create();
Task::factory()->forOrganization($data->organization)->forProject($projectPublic)->createMany(2); Task::factory()->forOrganization($data->organization)->forProject($projectPublic)->createMany(2);
$projectAsMember = Project::factory()->isPrivate()->create(); $projectAsMember = Project::factory()->isPrivate()->create();
ProjectMember::factory()->forProject($projectAsMember)->forUser($data->user)->create(); ProjectMember::factory()->forProject($projectAsMember)->forMember($data->member)->create();
Task::factory()->forOrganization($data->organization)->forProject($projectAsMember)->createMany(2); Task::factory()->forOrganization($data->organization)->forProject($projectAsMember)->createMany(2);
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -177,7 +177,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
'tasks:view', 'tasks:view',
]); ]);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
ProjectMember::factory()->forProject($project)->forUser($data->user)->create(); ProjectMember::factory()->forProject($project)->forMember($data->member)->create();
Task::factory()->forOrganization($data->organization)->createMany(4); Task::factory()->forOrganization($data->organization)->createMany(4);
Task::factory()->forOrganization($data->organization)->forProject($project)->createMany(2); Task::factory()->forOrganization($data->organization)->forProject($project)->createMany(2);
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -311,7 +311,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract
'tasks:delete', 'tasks:delete',
]); ]);
$task = Task::factory()->forOrganization($data->organization)->create(); $task = Task::factory()->forOrganization($data->organization)->create();
TimeEntry::factory()->forUser($data->user)->forTask($task)->forOrganization($data->organization)->create(); TimeEntry::factory()->forMember($data->member)->forTask($task)->forOrganization($data->organization)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1; namespace Tests\Unit\Endpoint\Api\V1;
use App\Enums\Role;
use App\Models\Member;
use App\Models\Project; use App\Models\Project;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Models\User; use App\Models\User;
@@ -51,11 +53,14 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:own', 'time-entries:view:own',
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->getJson(route('api.v1.time-entries.index', [$data->organization->getKey(), 'user_id' => $data->user->getKey()])); $response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(),
'member_id' => $data->member->getKey(),
]));
// Assert // Assert
$response->assertStatus(200); $response->assertStatus(200);
@@ -68,15 +73,20 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:all', 'time-entries:view:all',
]); ]);
$user = User::factory()->withPersonalOrganization()->create(); $otherData = $this->createUserWithPermission([
'time-entries:view:all',
]);
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->getJson(route('api.v1.time-entries.index', [$data->organization->getKey(), 'user_id' => $user->getKey()])); $response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(),
'member_id' => $otherData->member->getKey(),
]));
// Assert // Assert
$response->assertStatus(422); $response->assertStatus(422);
$response->assertJsonValidationErrorFor('user_id'); $response->assertJsonValidationErrorFor('member_id');
} }
public function test_index_endpoint_returns_time_entries_for_other_user_in_organization(): void public function test_index_endpoint_returns_time_entries_for_other_user_in_organization(): void
@@ -86,10 +96,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'time-entries:view:all', 'time-entries:view:all',
]); ]);
$user = User::factory()->create(); $user = User::factory()->create();
$data->organization->users()->attach($user, [ $member = Member::factory()->forOrganization($data->organization)->forUser($user)->role(Role::Employee)->create();
'role' => 'employee', $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($member)->create();
]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($user)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -107,16 +115,14 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'time-entries:view:all', 'time-entries:view:all',
]); ]);
$user = User::factory()->create(); $user = User::factory()->create();
$data->organization->users()->attach($user, [ $member = Member::factory()->forOrganization($data->organization)->forUser($user)->role(Role::Employee)->create();
'role' => 'employee', $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create([
]);
$timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create([
'start' => Carbon::now()->subDay(), 'start' => Carbon::now()->subDay(),
]); ]);
$timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forUser($user)->create([ $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forMember($member)->create([
'start' => Carbon::now()->subDays(2), 'start' => Carbon::now()->subDays(2),
]); ]);
$timeEntry3 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create([ $timeEntry3 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create([
'start' => Carbon::now()->subDays(3), 'start' => Carbon::now()->subDays(3),
]); ]);
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -137,15 +143,15 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:own', 'time-entries:view:own',
]); ]);
$activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->active()->create(); $activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->active()->create();
$nonActiveTimeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->createMany(3); $nonActiveTimeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->createMany(3);
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->getJson(route('api.v1.time-entries.index', [ $response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(), $data->organization->getKey(),
'active' => 'true', 'active' => 'true',
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
])); ]));
// Assert // Assert
@@ -160,15 +166,15 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:own', 'time-entries:view:own',
]); ]);
$activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->active()->createMany(3); $activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->active()->createMany(3);
$nonActiveTimeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create(); $nonActiveTimeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->getJson(route('api.v1.time-entries.index', [ $response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(), $data->organization->getKey(),
'active' => 'false', 'active' => 'false',
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
])); ]));
// Assert // Assert
@@ -184,7 +190,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:own', 'time-entries:view:own',
]); ]);
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->createMany(3); $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->createMany(3);
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -192,7 +198,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data->organization->getKey(), $data->organization->getKey(),
'only_full_dates' => 'true', 'only_full_dates' => 'true',
'limit' => 5, 'limit' => 5,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
])); ]));
// Assert // Assert
@@ -206,10 +212,10 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:own', 'time-entries:view:own',
]); ]);
$timeEntriesDay1 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesDay1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->startBetween(Carbon::now($data->user->timezone)->subDay()->startOfDay(), Carbon::now($data->user->timezone)->subDay()->endOfDay()) ->startBetween(Carbon::now($data->user->timezone)->subDay()->startOfDay(), Carbon::now($data->user->timezone)->subDay()->endOfDay())
->createMany(3); ->createMany(3);
$timeEntriesDay2 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesDay2 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->startBetween(Carbon::now($data->user->timezone)->subDays(2)->startOfDay(), Carbon::now($data->user->timezone)->subDays(2)->endOfDay()) ->startBetween(Carbon::now($data->user->timezone)->subDays(2)->startOfDay(), Carbon::now($data->user->timezone)->subDays(2)->endOfDay())
->createMany(3); ->createMany(3);
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -219,7 +225,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data->organization->getKey(), $data->organization->getKey(),
'only_full_dates' => 'true', 'only_full_dates' => 'true',
'limit' => 5, 'limit' => 5,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
])); ]));
// Assert // Assert
@@ -241,7 +247,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
*/ */
// Note: This entry is yesterday in user timezone and yesterday in UTC // Note: This entry is yesterday in user timezone and yesterday in UTC
$timeEntriesDay1InUserTimeZone = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesDay1InUserTimeZone = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->state([ ->state([
'start' => Carbon::now($data->user->timezone)->subDay()->startOfDay()->utc(), 'start' => Carbon::now($data->user->timezone)->subDay()->startOfDay()->utc(),
]) ])
@@ -249,7 +255,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
//dump($timeEntriesDay1InUserTimeZone->first()->refresh()->start->toImmutable()->timezone('UTC')->toDateString()); //dump($timeEntriesDay1InUserTimeZone->first()->refresh()->start->toImmutable()->timezone('UTC')->toDateString());
//dump($timeEntriesDay1InUserTimeZone->first()->refresh()->start->toImmutable()->timezone($data->user->timezone)->toDateString()); //dump($timeEntriesDay1InUserTimeZone->first()->refresh()->start->toImmutable()->timezone($data->user->timezone)->toDateString());
// Note: This entry is yesterday in UTC timezone, but two days ago in user timezone // Note: This entry is yesterday in UTC timezone, but two days ago in user timezone
$timeEntriesDay1InUTC = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesDay1InUTC = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->state([ ->state([
'start' => Carbon::now('UTC')->subDay()->startOfDay()->utc(), 'start' => Carbon::now('UTC')->subDay()->startOfDay()->utc(),
]) ])
@@ -257,7 +263,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
//dump($timeEntriesDay1InUTC->first()->refresh()->start->toImmutable()->timezone('UTC')->toDateString()); //dump($timeEntriesDay1InUTC->first()->refresh()->start->toImmutable()->timezone('UTC')->toDateString());
//dump($timeEntriesDay1InUTC->first()->refresh()->start->toImmutable()->timezone($data->user->timezone)->toDateString()); //dump($timeEntriesDay1InUTC->first()->refresh()->start->toImmutable()->timezone($data->user->timezone)->toDateString());
// Note: This entry is two days ago in user timezone // Note: This entry is two days ago in user timezone
$timeEntriesDay2InUserTimeZone = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesDay2InUserTimeZone = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->state([ ->state([
'start' => Carbon::now($data->user->timezone)->subDays(2)->startOfDay()->utc(), 'start' => Carbon::now($data->user->timezone)->subDays(2)->startOfDay()->utc(),
]) ])
@@ -270,7 +276,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data->organization->getKey(), $data->organization->getKey(),
'only_full_dates' => 'true', 'only_full_dates' => 'true',
'limit' => 5, 'limit' => 5,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
])); ]));
// Assert // Assert
@@ -284,7 +290,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:own', 'time-entries:view:own',
]); ]);
$timeEntriesDay1 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesDay1 = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->startBetween(Carbon::now()->subDay()->startOfDay(), Carbon::now()->subDay()->endOfDay()) ->startBetween(Carbon::now()->subDay()->startOfDay(), Carbon::now()->subDay()->endOfDay())
->createMany(7); ->createMany(7);
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -294,7 +300,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data->organization->getKey(), $data->organization->getKey(),
'only_full_dates' => 'true', 'only_full_dates' => 'true',
'limit' => 5, 'limit' => 5,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
])); ]));
// Assert // Assert
@@ -311,19 +317,19 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:own', 'time-entries:view:own',
]); ]);
$timeEntriesAfter = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesAfter = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->startBetween( ->startBetween(
Carbon::now()->timezone($data->user->timezone)->subDay()->startOfDay()->utc(), Carbon::now()->timezone($data->user->timezone)->subDay()->startOfDay()->utc(),
Carbon::now()->timezone($data->user->timezone)->utc() Carbon::now()->timezone($data->user->timezone)->utc()
) )
->createMany(3); ->createMany(3);
$timeEntriesBefore = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesBefore = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->startBetween( ->startBetween(
Carbon::now()->timezone($data->user->timezone)->subDays(2)->startOfDay()->utc(), Carbon::now()->timezone($data->user->timezone)->subDays(2)->startOfDay()->utc(),
Carbon::now()->timezone($data->user->timezone)->subDays(2)->endOfDay()->utc() Carbon::now()->timezone($data->user->timezone)->subDays(2)->endOfDay()->utc()
) )
->createMany(3); ->createMany(3);
$timeEntriesDirectlyBeforeLimit = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesDirectlyBeforeLimit = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->create([ ->create([
'start' => Carbon::now()->timezone($data->user->timezone)->subDays(2)->endOfDay()->utc(), 'start' => Carbon::now()->timezone($data->user->timezone)->subDays(2)->endOfDay()->utc(),
]); ]);
@@ -333,7 +339,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$response = $this->getJson(route('api.v1.time-entries.index', [ $response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(), $data->organization->getKey(),
'before' => Carbon::now()->timezone($data->user->timezone)->subDay()->startOfDay()->toIso8601ZuluString(), 'before' => Carbon::now()->timezone($data->user->timezone)->subDay()->startOfDay()->toIso8601ZuluString(),
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
])); ]));
// Assert // Assert
@@ -354,13 +360,13 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:own', 'time-entries:view:own',
]); ]);
$timeEntriesAfter = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesAfter = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->startBetween(Carbon::now($data->user->timezone)->startOfDay()->utc(), Carbon::now($data->user->timezone)->utc()) ->startBetween(Carbon::now($data->user->timezone)->startOfDay()->utc(), Carbon::now($data->user->timezone)->utc())
->createMany(3); ->createMany(3);
$timeEntriesBefore = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesBefore = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->startBetween(Carbon::now($data->user->timezone)->subDay()->startOfDay()->utc(), Carbon::now($data->user->timezone)->subDay()->endOfDay()->utc()) ->startBetween(Carbon::now($data->user->timezone)->subDay()->startOfDay()->utc(), Carbon::now($data->user->timezone)->subDay()->endOfDay()->utc())
->createMany(3); ->createMany(3);
$timeEntriesDirectlyAfterLimit = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user) $timeEntriesDirectlyAfterLimit = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)
->create([ ->create([
'start' => Carbon::now($data->user->timezone)->startOfDay()->utc(), 'start' => Carbon::now($data->user->timezone)->startOfDay()->utc(),
]); ]);
@@ -370,7 +376,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$response = $this->getJson(route('api.v1.time-entries.index', [ $response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(), $data->organization->getKey(),
'after' => Carbon::now($data->user->timezone)->subDay()->endOfDay()->toIso8601ZuluString(), // yesterday 'after' => Carbon::now($data->user->timezone)->subDay()->endOfDay()->toIso8601ZuluString(), // yesterday
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
])); ]));
// Assert // Assert
@@ -405,10 +411,10 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:all', 'time-entries:view:all',
]); ]);
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->createMany(3); $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->createMany(3);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->forProject($project)->createMany(3); $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->forProject($project)->createMany(3);
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->state([ $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->state([
'start' => $timeEntries->get(0)->start, 'start' => $timeEntries->get(0)->start,
])->createMany(3); ])->createMany(3);
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -416,8 +422,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
// Act // Act
$response = $this->getJson(route('api.v1.time-entries.aggregate', [ $response = $this->getJson(route('api.v1.time-entries.aggregate', [
$data->organization->getKey(), $data->organization->getKey(),
'group_1' => 'day', 'group' => 'day',
'group_2' => 'project', 'sub_group' => 'project',
])); ]));
// Assert // Assert
@@ -430,10 +436,10 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:all', 'time-entries:view:all',
]); ]);
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->createMany(3); $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->createMany(3);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->forProject($project)->createMany(3); $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->forProject($project)->createMany(3);
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->state([ $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->state([
'start' => $timeEntries->get(0)->start, 'start' => $timeEntries->get(0)->start,
])->createMany(3); ])->createMany(3);
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -441,7 +447,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
// Act // Act
$response = $this->getJson(route('api.v1.time-entries.aggregate', [ $response = $this->getJson(route('api.v1.time-entries.aggregate', [
$data->organization->getKey(), $data->organization->getKey(),
'group_1' => 'week', 'group' => 'week',
])); ]));
// Assert // Assert
@@ -454,10 +460,10 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:view:all', 'time-entries:view:all',
]); ]);
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->createMany(3); $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->createMany(3);
$project = Project::factory()->forOrganization($data->organization)->create(); $project = Project::factory()->forOrganization($data->organization)->create();
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->forProject($project)->createMany(3); $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->forProject($project)->createMany(3);
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->state([ $timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->state([
'start' => $timeEntries->get(0)->start, 'start' => $timeEntries->get(0)->start,
])->createMany(3); ])->createMany(3);
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -486,7 +492,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -500,7 +506,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:create:own', 'time-entries:create:own',
]); ]);
$activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->active()->create(); $activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->active()->create();
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->withTags($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->withTags($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -511,7 +517,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => null, 'end' => null,
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'project_id' => $timeEntryFake->project_id, 'project_id' => $timeEntryFake->project_id,
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -538,7 +544,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'project_id' => $timeEntryFake->project_id, 'project_id' => $timeEntryFake->project_id,
'task_id' => $timeEntryFake2->task_id, 'task_id' => $timeEntryFake2->task_id,
]); ]);
@@ -567,7 +573,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'task_id' => $timeEntryFake2->task_id, 'task_id' => $timeEntryFake2->task_id,
]); ]);
@@ -595,7 +601,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'project_id' => $timeEntryFake->project_id, 'project_id' => $timeEntryFake->project_id,
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -604,7 +610,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$response->assertStatus(201); $response->assertStatus(201);
$this->assertDatabaseHas(TimeEntry::class, [ $this->assertDatabaseHas(TimeEntry::class, [
'id' => $response->json('data.id'), 'id' => $response->json('data.id'),
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
} }
@@ -622,14 +628,14 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [ $response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
'billable' => $timeEntryFake->billable, 'billable' => $timeEntryFake->billable,
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
]); ]);
// Assert // Assert
$response->assertStatus(201); $response->assertStatus(201);
$this->assertDatabaseHas(TimeEntry::class, [ $this->assertDatabaseHas(TimeEntry::class, [
'id' => $response->json('data.id'), 'id' => $response->json('data.id'),
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'task_id' => null, 'task_id' => null,
]); ]);
} }
@@ -641,9 +647,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'time-entries:create:own', 'time-entries:create:own',
]); ]);
$otherUser = User::factory()->create(); $otherUser = User::factory()->create();
$data->organization->users()->attach($otherUser, [ $otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
'role' => 'employee',
]);
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -654,7 +658,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $otherUser->getKey(), 'member_id' => $otherMember->getKey(),
'project_id' => $timeEntryFake->project_id, 'project_id' => $timeEntryFake->project_id,
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -670,9 +674,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'time-entries:create:all', 'time-entries:create:all',
]); ]);
$otherUser = User::factory()->create(); $otherUser = User::factory()->create();
$data->organization->users()->attach($otherUser, [ $otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
'role' => 'employee',
]);
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -683,7 +685,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $otherUser->getKey(), 'member_id' => $otherMember->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -692,6 +694,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$this->assertDatabaseHas(TimeEntry::class, [ $this->assertDatabaseHas(TimeEntry::class, [
'id' => $response->json('data.id'), 'id' => $response->json('data.id'),
'user_id' => $otherUser->getKey(), 'user_id' => $otherUser->getKey(),
'member_id' => $otherMember->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
} }
@@ -701,7 +704,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
// Arrange // Arrange
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create();
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -712,7 +715,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -729,7 +732,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$otherUser = $this->createUserWithPermission([ $otherUser = $this->createUserWithPermission([
'time-entries:update:own', 'time-entries:update:own',
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($otherUser->organization)->forUser($otherUser->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($otherUser->organization)->forMember($otherUser->member)->create();
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -739,7 +742,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -754,10 +756,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'time-entries:update:own', 'time-entries:update:own',
]); ]);
$user = User::factory()->create(); $user = User::factory()->create();
$data->organization->users()->attach($user, [ $member = Member::factory()->forOrganization($data->organization)->forUser($user)->role(Role::Employee)->create();
'role' => 'employee', $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($member)->create();
]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($user)->create();
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -767,7 +767,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $user->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -781,7 +780,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:update:own', 'time-entries:update:own',
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create();
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->make();
$timeEntryFake2 = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->make(); $timeEntryFake2 = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -793,7 +792,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(),
'project_id' => $timeEntryFake->project_id, 'project_id' => $timeEntryFake->project_id,
'task_id' => $timeEntryFake2->task_id, 'task_id' => $timeEntryFake2->task_id,
]); ]);
@@ -811,7 +809,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:update:own', 'time-entries:update:own',
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create();
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->make();
$timeEntryFake2 = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->make(); $timeEntryFake2 = TimeEntry::factory()->forOrganization($data->organization)->withTask($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -823,7 +821,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(),
'task_id' => $timeEntryFake2->task_id, 'task_id' => $timeEntryFake2->task_id,
]); ]);
@@ -841,7 +838,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:update:own', 'time-entries:update:own',
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create();
$timeEntryFake = TimeEntry::factory()->withTags($data->organization)->forOrganization($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->withTags($data->organization)->forOrganization($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -851,14 +848,14 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
]); ]);
// Assert // Assert
$response->assertStatus(200); $response->assertStatus(200);
$this->assertDatabaseHas(TimeEntry::class, [ $this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry->getKey(), 'id' => $timeEntry->getKey(),
'user_id' => $data->user->getKey(), 'member_id' => $data->member->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
} }
@@ -870,10 +867,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'time-entries:update:all', 'time-entries:update:all',
]); ]);
$user = User::factory()->create(); $user = User::factory()->create();
$data->organization->users()->attach($user, [ $member = Member::factory()->forOrganization($data->organization)->forUser($user)->role(Role::Employee)->create();
'role' => 'employee', $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($member)->create();
]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($user)->create();
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make(); $timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user); Passport::actingAs($data->user);
@@ -883,7 +878,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'start' => $timeEntryFake->start->toIso8601ZuluString(), 'start' => $timeEntryFake->start->toIso8601ZuluString(),
'end' => $timeEntryFake->end->toIso8601ZuluString(), 'end' => $timeEntryFake->end->toIso8601ZuluString(),
'tags' => $timeEntryFake->tags, 'tags' => $timeEntryFake->tags,
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
@@ -891,7 +886,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$response->assertStatus(200); $response->assertStatus(200);
$this->assertDatabaseHas(TimeEntry::class, [ $this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry->getKey(), 'id' => $timeEntry->getKey(),
'user_id' => $user->getKey(), 'member_id' => $member->getKey(),
'task_id' => $timeEntryFake->task_id, 'task_id' => $timeEntryFake->task_id,
]); ]);
} }
@@ -905,7 +900,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$otherUser = $this->createUserWithPermission([ $otherUser = $this->createUserWithPermission([
'time-entries:delete:all', 'time-entries:delete:all',
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($otherUser->organization)->forUser($otherUser->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($otherUser->organization)->forMember($otherUser->member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -935,7 +930,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
// Arrange // Arrange
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -952,10 +947,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'time-entries:delete:own', 'time-entries:delete:own',
]); ]);
$user = User::factory()->create(); $user = User::factory()->create();
$data->organization->users()->attach($user, [ $member = Member::factory()->forOrganization($data->organization)->forUser($user)->role(Role::Employee)->create();
'role' => 'employee', $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($member)->create();
]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($user)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -971,7 +964,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
'time-entries:delete:own', 'time-entries:delete:own',
]); ]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create(); $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($data->member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -992,10 +985,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
'time-entries:delete:all', 'time-entries:delete:all',
]); ]);
$user = User::factory()->create(); $user = User::factory()->create();
$data->organization->users()->attach($user, [ $member = Member::factory()->forOrganization($data->organization)->forUser($user)->role(Role::Employee)->create();
'role' => 'employee', $timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forMember($member)->create();
]);
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($user)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act

View File

@@ -27,8 +27,8 @@ class UserTimeEntryEndpointTest extends ApiEndpointTestAbstract
// Arrange // Arrange
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
]); ]);
$activeTimeEntry = TimeEntry::factory()->forUser($data->user)->active()->create(); $activeTimeEntry = TimeEntry::factory()->forMember($data->member)->active()->create();
$inactiveTimeEntry = TimeEntry::factory()->forUser($data->user)->create(); $inactiveTimeEntry = TimeEntry::factory()->forMember($data->member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
@@ -43,7 +43,7 @@ class UserTimeEntryEndpointTest extends ApiEndpointTestAbstract
// Arrange // Arrange
$data = $this->createUserWithPermission([ $data = $this->createUserWithPermission([
]); ]);
$inactiveTimeEntry = TimeEntry::factory()->forUser($data->user)->create(); $inactiveTimeEntry = TimeEntry::factory()->forMember($data->member)->create();
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\User;
class ProjectMemberModelTest extends ModelTestAbstract class ProjectMemberModelTest extends ModelTestAbstract
{ {
@@ -15,8 +15,8 @@ class ProjectMemberModelTest extends ModelTestAbstract
{ {
// Arrange // Arrange
$project = Project::factory()->create(); $project = Project::factory()->create();
$user = User::factory()->create(); $member = Member::factory()->create();
$projectMember = ProjectMember::factory()->forProject($project)->forUser($user)->create(); $projectMember = ProjectMember::factory()->forProject($project)->forMember($member)->create();
// Act // Act
$projectMember->refresh(); $projectMember->refresh();
@@ -27,19 +27,19 @@ class ProjectMemberModelTest extends ModelTestAbstract
$this->assertTrue($projectRel->is($project)); $this->assertTrue($projectRel->is($project));
} }
public function test_it_belongs_to_a_user(): void public function test_it_belongs_to_a_member(): void
{ {
// Arrange // Arrange
$user = User::factory()->create(); $member = Member::factory()->create();
$projectMember = ProjectMember::factory()->forUser($user)->create(); $projectMember = ProjectMember::factory()->forMember($member)->create();
// Act // Act
$projectMember->refresh(); $projectMember->refresh();
$userRel = $projectMember->user; $memberRel = $projectMember->member;
// Assert // Assert
$this->assertNotNull($userRel); $this->assertNotNull($memberRel);
$this->assertTrue($userRel->is($user)); $this->assertTrue($memberRel->is($member));
} }
public function test_scope_where_belongs_to_organization_filters_project_members_to_only_retrieve_project_members_that_belong_to_a_project_of_the_organization(): void public function test_scope_where_belongs_to_organization_filters_project_members_to_only_retrieve_project_members_that_belong_to_a_project_of_the_organization(): void

View File

@@ -5,11 +5,11 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Models\Client; use App\Models\Client;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\Task; use App\Models\Task;
use App\Models\User;
class ProjectModelTest extends ModelTestAbstract class ProjectModelTest extends ModelTestAbstract
{ {
@@ -91,14 +91,14 @@ class ProjectModelTest extends ModelTestAbstract
public function test_scope_visible_by_user_filters_so_that_only_public_projects_or_projects_where_the_user_is_member_are_shown(): void public function test_scope_visible_by_user_filters_so_that_only_public_projects_or_projects_where_the_user_is_member_are_shown(): void
{ {
// Arrange // Arrange
$user = User::factory()->create(); $member = Member::factory()->create();
$projectPrivate = Project::factory()->isPrivate()->create(); $projectPrivate = Project::factory()->isPrivate()->create();
$projectPublic = Project::factory()->isPublic()->create(); $projectPublic = Project::factory()->isPublic()->create();
$projectPrivateButMember = Project::factory()->isPrivate()->create(); $projectPrivateButMember = Project::factory()->isPrivate()->create();
ProjectMember::factory()->forProject($projectPrivateButMember)->forUser($user)->create(); ProjectMember::factory()->forProject($projectPrivateButMember)->forMember($member)->create();
// Act // Act
$projectsVisible = Project::query()->visibleByUser($user)->get(); $projectsVisible = Project::query()->visibleByUser($member->user)->get();
$allProjects = Project::query()->get(); $allProjects = Project::query()->get();
// Assert // Assert

View File

@@ -4,12 +4,12 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\Task; use App\Models\Task;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Models\User;
class TaskModelTest extends ModelTestAbstract class TaskModelTest extends ModelTestAbstract
{ {
@@ -62,17 +62,17 @@ class TaskModelTest extends ModelTestAbstract
public function test_scope_visible_by_user_filters_so_that_only_tasks_of_public_projects_or_projects_where_the_user_is_member_are_shown(): void public function test_scope_visible_by_user_filters_so_that_only_tasks_of_public_projects_or_projects_where_the_user_is_member_are_shown(): void
{ {
// Arrange // Arrange
$user = User::factory()->create(); $member = Member::factory()->create();
$projectPrivate = Project::factory()->isPrivate()->create(); $projectPrivate = Project::factory()->isPrivate()->create();
$projectPublic = Project::factory()->isPublic()->create(); $projectPublic = Project::factory()->isPublic()->create();
$projectPrivateButMember = Project::factory()->isPrivate()->create(); $projectPrivateButMember = Project::factory()->isPrivate()->create();
ProjectMember::factory()->forProject($projectPrivateButMember)->forUser($user)->create(); ProjectMember::factory()->forProject($projectPrivateButMember)->forMember($member)->create();
$taskPrivate = Task::factory()->forProject($projectPrivate)->create(); $taskPrivate = Task::factory()->forProject($projectPrivate)->create();
$taskPublic = Task::factory()->forProject($projectPublic)->create(); $taskPublic = Task::factory()->forProject($projectPublic)->create();
$taskPrivateButMember = Task::factory()->forProject($projectPrivateButMember)->create(); $taskPrivateButMember = Task::factory()->forProject($projectPrivateButMember)->create();
// Act // Act
$tasksVisible = Task::query()->visibleByUser($user)->get(); $tasksVisible = Task::query()->visibleByUser($member->user)->get();
$allTasks = Task::query()->get(); $allTasks = Task::query()->get();
// Assert // Assert

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Enums\Role;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use App\Models\TimeEntry; use App\Models\TimeEntry;
@@ -57,12 +59,12 @@ class UserModelTest extends ModelTestAbstract
$organization = Organization::factory()->withOwner($owner)->create(); $organization = Organization::factory()->withOwner($owner)->create();
$user = User::factory()->create(); $user = User::factory()->create();
$user->organizations()->attach($organization, [ $user->organizations()->attach($organization, [
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
$otherOrganization = Organization::factory()->create(); $otherOrganization = Organization::factory()->create();
$otherUser = User::factory()->create(); $otherUser = User::factory()->create();
$otherUser->organizations()->attach($otherOrganization, [ $otherUser->organizations()->attach($otherOrganization, [
'role' => 'employee', 'role' => Role::Employee->value,
]); ]);
// Act // Act
@@ -98,8 +100,10 @@ class UserModelTest extends ModelTestAbstract
// Arrange // Arrange
$user = User::factory()->create(); $user = User::factory()->create();
$otherUser = User::factory()->create(); $otherUser = User::factory()->create();
$projectMembers = ProjectMember::factory()->forUser($user)->createMany(3); $member = Member::factory()->forUser($user)->create();
$otherProjectMembers = ProjectMember::factory()->forUser($otherUser)->createMany(3); $otherMember = Member::factory()->forUser($otherUser)->create();
$projectMembers = ProjectMember::factory()->forMember($member)->createMany(3);
$otherProjectMembers = ProjectMember::factory()->forMember($otherMember)->createMany(3);
// Act // Act
$user->refresh(); $user->refresh();

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Unit\Service; namespace Tests\Unit\Service;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
@@ -31,16 +32,17 @@ class BillableRateServiceTest extends TestCase
$organization = Organization::factory()->create([ $organization = Organization::factory()->create([
'billable_rate' => 1001, 'billable_rate' => 1001,
]); ]);
$user = User::factory()->attachToOrganization($organization, [ $user = User::factory()->create();
$member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => 2002, 'billable_rate' => 2002,
])->create(); ]);
$project = Project::factory()->forOrganization($organization)->create([ $project = Project::factory()->forOrganization($organization)->create([
'billable_rate' => 3003, 'billable_rate' => 3003,
]); ]);
$projectMember = ProjectMember::factory()->forUser($user)->forProject($project)->create([ $projectMember = ProjectMember::factory()->forMember($member)->forProject($project)->create([
'billable_rate' => 4004, 'billable_rate' => 4004,
]); ]);
$timeEntry = TimeEntry::factory()->forProject($project)->forUser($user)->forOrganization($organization)->create([ $timeEntry = TimeEntry::factory()->forProject($project)->forMember($member)->forOrganization($organization)->create([
'billable' => false, 'billable' => false,
]); ]);
@@ -57,16 +59,17 @@ class BillableRateServiceTest extends TestCase
$organization = Organization::factory()->create([ $organization = Organization::factory()->create([
'billable_rate' => 1001, 'billable_rate' => 1001,
]); ]);
$user = User::factory()->attachToOrganization($organization, [ $user = User::factory()->create();
$member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => 2002, 'billable_rate' => 2002,
])->create(); ]);
$project = Project::factory()->forOrganization($organization)->create([ $project = Project::factory()->forOrganization($organization)->create([
'billable_rate' => 3003, 'billable_rate' => 3003,
]); ]);
$projectMember = ProjectMember::factory()->forUser($user)->forProject($project)->create([ $projectMember = ProjectMember::factory()->forMember($member)->forProject($project)->create([
'billable_rate' => 4004, 'billable_rate' => 4004,
]); ]);
$timeEntry = TimeEntry::factory()->forProject($project)->forUser($user)->forOrganization($organization)->create([ $timeEntry = TimeEntry::factory()->forProject($project)->forMember($member)->forOrganization($organization)->create([
'billable' => true, 'billable' => true,
]); ]);
@@ -83,16 +86,17 @@ class BillableRateServiceTest extends TestCase
$organization = Organization::factory()->create([ $organization = Organization::factory()->create([
'billable_rate' => 1001, 'billable_rate' => 1001,
]); ]);
$user = User::factory()->attachToOrganization($organization, [ $user = User::factory()->create();
$member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => 2002, 'billable_rate' => 2002,
])->create(); ]);
$project = Project::factory()->forOrganization($organization)->create([ $project = Project::factory()->forOrganization($organization)->create([
'billable_rate' => 3003, 'billable_rate' => 3003,
]); ]);
$projectMember = ProjectMember::factory()->forUser($user)->forProject($project)->create([ $projectMember = ProjectMember::factory()->forMember($member)->forProject($project)->create([
'billable_rate' => null, 'billable_rate' => null,
]); ]);
$timeEntry = TimeEntry::factory()->forProject($project)->forUser($user)->forOrganization($organization)->create([ $timeEntry = TimeEntry::factory()->forProject($project)->forMember($member)->forOrganization($organization)->create([
'billable' => true, 'billable' => true,
]); ]);
@@ -109,13 +113,14 @@ class BillableRateServiceTest extends TestCase
$organization = Organization::factory()->create([ $organization = Organization::factory()->create([
'billable_rate' => 1001, 'billable_rate' => 1001,
]); ]);
$user = User::factory()->attachToOrganization($organization, [ $user = User::factory()->create();
$member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => 2002, 'billable_rate' => 2002,
])->create(); ]);
$project = Project::factory()->forOrganization($organization)->create([ $project = Project::factory()->forOrganization($organization)->create([
'billable_rate' => 3003, 'billable_rate' => 3003,
]); ]);
$timeEntry = TimeEntry::factory()->forProject($project)->forUser($user)->forOrganization($organization)->create([ $timeEntry = TimeEntry::factory()->forProject($project)->forMember($member)->forOrganization($organization)->create([
'billable' => true, 'billable' => true,
]); ]);
@@ -132,16 +137,17 @@ class BillableRateServiceTest extends TestCase
$organization = Organization::factory()->create([ $organization = Organization::factory()->create([
'billable_rate' => 1001, 'billable_rate' => 1001,
]); ]);
$user = User::factory()->attachToOrganization($organization, [ $user = User::factory()->create();
$member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => 2002, 'billable_rate' => 2002,
])->create(); ]);
$project = Project::factory()->forOrganization($organization)->create([ $project = Project::factory()->forOrganization($organization)->create([
'billable_rate' => null, 'billable_rate' => null,
]); ]);
$projectMember = ProjectMember::factory()->forUser($user)->forProject($project)->create([ $projectMember = ProjectMember::factory()->forMember($member)->forProject($project)->create([
'billable_rate' => null, 'billable_rate' => null,
]); ]);
$timeEntry = TimeEntry::factory()->forProject($project)->forUser($user)->forOrganization($organization)->create([ $timeEntry = TimeEntry::factory()->forProject($project)->forMember($member)->forOrganization($organization)->create([
'billable' => true, 'billable' => true,
]); ]);
@@ -158,10 +164,11 @@ class BillableRateServiceTest extends TestCase
$organization = Organization::factory()->create([ $organization = Organization::factory()->create([
'billable_rate' => 1001, 'billable_rate' => 1001,
]); ]);
$user = User::factory()->attachToOrganization($organization, [ $user = User::factory()->create();
$member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => 2002, 'billable_rate' => 2002,
])->create(); ]);
$timeEntry = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
'billable' => true, 'billable' => true,
]); ]);
@@ -178,16 +185,17 @@ class BillableRateServiceTest extends TestCase
$organization = Organization::factory()->create([ $organization = Organization::factory()->create([
'billable_rate' => 1001, 'billable_rate' => 1001,
]); ]);
$user = User::factory()->attachToOrganization($organization, [ $user = User::factory()->create();
$member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => null, 'billable_rate' => null,
])->create(); ]);
$project = Project::factory()->forOrganization($organization)->create([ $project = Project::factory()->forOrganization($organization)->create([
'billable_rate' => null, 'billable_rate' => null,
]); ]);
$projectMember = ProjectMember::factory()->forUser($user)->forProject($project)->create([ $projectMember = ProjectMember::factory()->forMember($member)->forProject($project)->create([
'billable_rate' => null, 'billable_rate' => null,
]); ]);
$timeEntry = TimeEntry::factory()->forProject($project)->forUser($user)->forOrganization($organization)->create([ $timeEntry = TimeEntry::factory()->forProject($project)->forMember($member)->forOrganization($organization)->create([
'billable' => true, 'billable' => true,
]); ]);
@@ -205,7 +213,10 @@ class BillableRateServiceTest extends TestCase
'billable_rate' => 1001, 'billable_rate' => 1001,
]); ]);
$user = User::factory()->create(); $user = User::factory()->create();
$timeEntry = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => null,
]);
$timeEntry = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
'billable' => true, 'billable' => true,
]); ]);
@@ -222,16 +233,17 @@ class BillableRateServiceTest extends TestCase
$organization = Organization::factory()->create([ $organization = Organization::factory()->create([
'billable_rate' => null, 'billable_rate' => null,
]); ]);
$user = User::factory()->attachToOrganization($organization, [ $user = User::factory()->create();
$member = Member::factory()->forOrganization($organization)->forUser($user)->create([
'billable_rate' => null, 'billable_rate' => null,
])->create(); ]);
$project = Project::factory()->forOrganization($organization)->create([ $project = Project::factory()->forOrganization($organization)->create([
'billable_rate' => null, 'billable_rate' => null,
]); ]);
$projectMember = ProjectMember::factory()->forUser($user)->forProject($project)->create([ $projectMember = ProjectMember::factory()->forMember($member)->forProject($project)->create([
'billable_rate' => null, 'billable_rate' => null,
]); ]);
$timeEntry = TimeEntry::factory()->forProject($project)->forUser($user)->forOrganization($organization)->create([ $timeEntry = TimeEntry::factory()->forProject($project)->forMember($member)->forOrganization($organization)->create([
'billable' => true, 'billable' => true,
]); ]);

View File

@@ -6,6 +6,7 @@ namespace Tests\Unit\Service;
use App\Enums\Role; use App\Enums\Role;
use App\Enums\Weekday; use App\Enums\Weekday;
use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Task; use App\Models\Task;
@@ -37,13 +38,13 @@ class DashboardServiceTest extends TestCase
$user = User::factory()->create([ $user = User::factory()->create([
'timezone' => 'Europe/Vienna', 'timezone' => 'Europe/Vienna',
]); ]);
$user->organizations()->attach($organization); $member = Member::factory()->forUser($user)->forOrganization($organization)->create();
$timeEntry1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time shifts in timezone Europe/Vienna to the next day // Note: The start time shifts in timezone Europe/Vienna to the next day
'start' => Carbon::create(2023, 12, 30, 23, 0, 0, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 23, 0, 0, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'),
]); ]);
$timeEntry2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry2 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time NOT shifts in timezone Europe/Vienna to the next day // Note: The start time NOT shifts in timezone Europe/Vienna to the next day
'start' => Carbon::create(2023, 12, 30, 22, 59, 59, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 22, 59, 59, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 39, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 39, 'UTC'),
@@ -87,15 +88,15 @@ class DashboardServiceTest extends TestCase
'timezone' => 'Europe/Vienna', 'timezone' => 'Europe/Vienna',
'week_start' => Weekday::Sunday, 'week_start' => Weekday::Sunday,
]); ]);
$user->organizations()->attach($organization); $member = Member::factory()->forUser($user)->forOrganization($organization)->create();
// Note: This is a Sunday // Note: This is a Sunday
$timeEntry1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time shifts in timezone Europe/Vienna to the next day // Note: The start time shifts in timezone Europe/Vienna to the next day
'start' => Carbon::create(2023, 12, 30, 23, 0, 0, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 23, 0, 0, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'),
]); ]);
// Note: This is a Saturday // Note: This is a Saturday
$timeEntry2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry2 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time NOT shifts in timezone Europe/Vienna to the next day // Note: The start time NOT shifts in timezone Europe/Vienna to the next day
'start' => Carbon::create(2023, 12, 30, 22, 59, 59, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 22, 59, 59, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 39, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 39, 'UTC'),
@@ -147,15 +148,15 @@ class DashboardServiceTest extends TestCase
'timezone' => 'Europe/Vienna', 'timezone' => 'Europe/Vienna',
'week_start' => Weekday::Sunday, 'week_start' => Weekday::Sunday,
]); ]);
$user->organizations()->attach($organization); $member = Member::factory()->forUser($user)->forOrganization($organization)->create();
// Note: This is a Sunday // Note: This is a Sunday
$timeEntry1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time shifts in timezone Europe/Vienna to the next day // Note: The start time shifts in timezone Europe/Vienna to the next day
'start' => Carbon::create(2023, 12, 30, 23, 0, 0, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 23, 0, 0, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'),
]); ]);
// Note: This is a Saturday // Note: This is a Saturday
$timeEntry2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry2 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time NOT shifts in timezone Europe/Vienna to the next day // Note: The start time NOT shifts in timezone Europe/Vienna to the next day
'start' => Carbon::create(2023, 12, 30, 22, 59, 59, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 22, 59, 59, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 39, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 39, 'UTC'),
@@ -178,23 +179,23 @@ class DashboardServiceTest extends TestCase
'timezone' => 'Europe/Vienna', 'timezone' => 'Europe/Vienna',
'week_start' => Weekday::Sunday, 'week_start' => Weekday::Sunday,
]); ]);
$user->organizations()->attach($organization); $member = Member::factory()->forUser($user)->forOrganization($organization)->create();
// Note: This is a Sunday // Note: This is a Sunday
$timeEntry1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time shifts in timezone Europe/Vienna to the next day // Note: The start time shifts in timezone Europe/Vienna to the next day
'billable' => true, 'billable' => true,
'start' => Carbon::create(2023, 12, 30, 23, 0, 0, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 23, 0, 0, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'),
]); ]);
// Note: This is a Sunday (non-billable) // Note: This is a Sunday (non-billable)
$timeEntry1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time shifts in timezone Europe/Vienna to the next day // Note: The start time shifts in timezone Europe/Vienna to the next day
'billable' => false, 'billable' => false,
'start' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 59, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 59, 'UTC'),
]); ]);
// Note: This is a Saturday // Note: This is a Saturday
$timeEntry2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry2 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time NOT shifts in timezone Europe/Vienna to the next day // Note: The start time NOT shifts in timezone Europe/Vienna to the next day
'billable' => true, 'billable' => true,
'start' => Carbon::create(2023, 12, 30, 22, 59, 59, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 22, 59, 59, 'UTC'),
@@ -221,9 +222,9 @@ class DashboardServiceTest extends TestCase
'timezone' => 'Europe/Vienna', 'timezone' => 'Europe/Vienna',
'week_start' => Weekday::Sunday, 'week_start' => Weekday::Sunday,
]); ]);
$user->organizations()->attach($organization); $member = Member::factory()->forUser($user)->forOrganization($organization)->create();
// Note: This is a Sunday // Note: This is a Sunday
$timeEntry1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time shifts in timezone Europe/Vienna to the next day // Note: The start time shifts in timezone Europe/Vienna to the next day
'billable' => true, 'billable' => true,
'billable_rate' => 50 * 100, 'billable_rate' => 50 * 100,
@@ -231,14 +232,14 @@ class DashboardServiceTest extends TestCase
'end' => Carbon::create(2023, 12, 31, 0, 0, 0, 'UTC'), 'end' => Carbon::create(2023, 12, 31, 0, 0, 0, 'UTC'),
]); ]);
// Note: This is a Sunday (non-billable) // Note: This is a Sunday (non-billable)
$timeEntry2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry2 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time shifts in timezone Europe/Vienna to the next day // Note: The start time shifts in timezone Europe/Vienna to the next day
'billable' => false, 'billable' => false,
'start' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'), 'start' => Carbon::create(2023, 12, 30, 23, 0, 40, 'UTC'),
'end' => Carbon::create(2023, 12, 30, 23, 0, 59, 'UTC'), 'end' => Carbon::create(2023, 12, 30, 23, 0, 59, 'UTC'),
]); ]);
// Note: This is a Saturday // Note: This is a Saturday
$timeEntry3 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry3 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: The start time NOT shifts in timezone Europe/Vienna to the next day // Note: The start time NOT shifts in timezone Europe/Vienna to the next day
'billable' => true, 'billable' => true,
'billable_rate' => 100 * 100, 'billable_rate' => 100 * 100,
@@ -267,42 +268,40 @@ class DashboardServiceTest extends TestCase
'week_start' => Weekday::Sunday, 'week_start' => Weekday::Sunday,
]); ]);
$organization = Organization::factory()->withOwner($user)->create(); $organization = Organization::factory()->withOwner($user)->create();
$organization->users()->attach($user, [ $member = Member::factory()->forUser($user)->forOrganization($organization)->role(Role::Owner)->create();
'role' => Role::Owner->value,
]);
$project1 = Project::factory()->forOrganization($organization)->create(); $project1 = Project::factory()->forOrganization($organization)->create();
$project2 = Project::factory()->forOrganization($organization)->create(); $project2 = Project::factory()->forOrganization($organization)->create();
$timeEntry1Project1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->forProject($project1)->create([ $timeEntry1Project1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->forProject($project1)->create([
// Note: At the start of the week // Note: At the start of the week
'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(),
'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(),
]); ]);
$timeEntry2Project1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->forProject($project1)->create([ $timeEntry2Project1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->forProject($project1)->create([
// Note: At the end of the week // Note: At the end of the week
'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), 'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(),
'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), 'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(),
]); ]);
$timeEntry1Project2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->forProject($project2)->create([ $timeEntry1Project2 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->forProject($project2)->create([
// Note: At the start of the week // Note: At the start of the week
'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(),
'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(),
]); ]);
$timeEntry2Project2 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->forProject($project2)->create([ $timeEntry2Project2 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->forProject($project2)->create([
// Note: At the end of the week // Note: At the end of the week
'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), 'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(),
'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), 'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(),
]); ]);
$timeEntry1WithoutProject = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1WithoutProject = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: At the start of the week // Note: At the start of the week
'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(),
'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(),
]); ]);
$timeEntry2WithoutProject = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry2WithoutProject = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: At the end of the week // Note: At the end of the week
'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), 'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(),
'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), 'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(),
]); ]);
$timeEntry1WithoutProjectOutsideOfWeek = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1WithoutProjectOutsideOfWeek = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: Outside of week // Note: Outside of week
'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->subSecond()->utc(), 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->subSecond()->utc(),
'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(39)->utc(), 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(39)->utc(),
@@ -312,7 +311,7 @@ class DashboardServiceTest extends TestCase
$result = $this->dashboardService->weeklyProjectOverview($user, $organization); $result = $this->dashboardService->weeklyProjectOverview($user, $organization);
// Assert // Assert
$this->assertSame([ $this->assertEqualsCanonicalizing([
[ [
'value' => 80, 'value' => 80,
'id' => $project1->getKey(), 'id' => $project1->getKey(),
@@ -345,18 +344,18 @@ class DashboardServiceTest extends TestCase
'timezone' => 'Europe/Vienna', 'timezone' => 'Europe/Vienna',
'week_start' => Weekday::Sunday, 'week_start' => Weekday::Sunday,
]); ]);
$user->organizations()->attach($organization); $member = Member::factory()->forUser($user)->forOrganization($organization)->create();
$timeEntry1WithoutProject = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1WithoutProject = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: At the start of the week // Note: At the start of the week
'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->utc(),
'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(),
]); ]);
$timeEntry2WithoutProject = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry2WithoutProject = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: At the end of the week // Note: At the end of the week
'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(), 'start' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->utc(),
'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(), 'end' => $now->endOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(40)->utc(),
]); ]);
$timeEntry1WithoutProjectOutsideOfWeek = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1WithoutProjectOutsideOfWeek = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
// Note: Outside of week // Note: Outside of week
'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->subSecond()->utc(), 'start' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->subSecond()->utc(),
'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(39)->utc(), 'end' => $now->startOfWeek(Weekday::Sunday->carbonWeekDay())->addSeconds(39)->utc(),
@@ -386,7 +385,7 @@ class DashboardServiceTest extends TestCase
'timezone' => 'Europe/Vienna', 'timezone' => 'Europe/Vienna',
'week_start' => Weekday::Sunday, 'week_start' => Weekday::Sunday,
]); ]);
$user->organizations()->attach($organization); $member = Member::factory()->forUser($user)->forOrganization($organization)->create();
// Act // Act
$result = $this->dashboardService->weeklyProjectOverview($user, $organization); $result = $this->dashboardService->weeklyProjectOverview($user, $organization);
@@ -406,31 +405,31 @@ class DashboardServiceTest extends TestCase
{ {
// Arrange // Arrange
$organization = Organization::factory()->create(); $organization = Organization::factory()->create();
$user1 = User::factory()->create(); $member1 = Member::factory()->forOrganization($organization)->create();
$user2 = User::factory()->create(); $member2 = Member::factory()->forOrganization($organization)->create();
$user3 = User::factory()->create(); $member3 = Member::factory()->forOrganization($organization)->create();
$user4 = User::factory()->create(); $member4 = Member::factory()->forOrganization($organization)->create();
$user5 = User::factory()->create(); $member5 = Member::factory()->forOrganization($organization)->create();
$task1 = Task::factory()->forOrganization($organization)->create(); $task1 = Task::factory()->forOrganization($organization)->create();
$timeEntry1 = TimeEntry::factory()->forUser($user1)->forOrganization($organization)->active()->create([ $timeEntry1 = TimeEntry::factory()->forMember($member1)->forOrganization($organization)->active()->create([
'start' => now()->subMinutes(10), 'start' => now()->subMinutes(10),
]); ]);
$timeEntry2 = TimeEntry::factory()->forUser($user2)->forOrganization($organization)->create([ $timeEntry2 = TimeEntry::factory()->forMember($member2)->forOrganization($organization)->create([
'start' => now()->subMinutes(20), 'start' => now()->subMinutes(20),
]); ]);
$timeEntry3 = TimeEntry::factory()->forUser($user3)->forOrganization($organization)->forTask($task1)->create([ $timeEntry3 = TimeEntry::factory()->forMember($member3)->forOrganization($organization)->forTask($task1)->create([
'description' => '', 'description' => '',
'start' => now()->subMinutes(30), 'start' => now()->subMinutes(30),
]); ]);
$timeEntry4 = TimeEntry::factory()->forUser($user4)->forOrganization($organization)->forTask($task1)->create([ $timeEntry4 = TimeEntry::factory()->forMember($member4)->forOrganization($organization)->forTask($task1)->create([
'description' => 'TEST 123', 'description' => 'TEST 123',
'start' => now()->subMinutes(40), 'start' => now()->subMinutes(40),
]); ]);
$timeEntry5 = TimeEntry::factory()->forUser($user4)->forOrganization($organization)->forTask($task1)->create([ $timeEntry5 = TimeEntry::factory()->forMember($member4)->forOrganization($organization)->forTask($task1)->create([
'description' => 'TEST 321', 'description' => 'TEST 321',
'start' => now()->subMinutes(50), 'start' => now()->subMinutes(50),
]); ]);
$timeEntry6 = TimeEntry::factory()->forUser($user5)->forOrganization($organization)->forTask($task1)->create([ $timeEntry6 = TimeEntry::factory()->forMember($member5)->forOrganization($organization)->forTask($task1)->create([
'description' => 'TEST 321', 'description' => 'TEST 321',
'start' => now()->subMinutes(60), 'start' => now()->subMinutes(60),
]); ]);
@@ -441,32 +440,32 @@ class DashboardServiceTest extends TestCase
// Assert // Assert
$this->assertSame([ $this->assertSame([
[ [
'user_id' => $user1->getKey(), 'member_id' => $member1->getKey(),
'name' => $user1->name, 'name' => $member1->user->name,
'description' => $timeEntry1->description, 'description' => $timeEntry1->description,
'time_entry_id' => $timeEntry1->getKey(), 'time_entry_id' => $timeEntry1->getKey(),
'task_id' => null, 'task_id' => null,
'status' => true, 'status' => true,
], ],
[ [
'user_id' => $user2->getKey(), 'member_id' => $member2->getKey(),
'name' => $user2->name, 'name' => $member2->user->name,
'description' => $timeEntry2->description, 'description' => $timeEntry2->description,
'time_entry_id' => $timeEntry2->getKey(), 'time_entry_id' => $timeEntry2->getKey(),
'task_id' => null, 'task_id' => null,
'status' => false, 'status' => false,
], ],
[ [
'user_id' => $user3->getKey(), 'member_id' => $member3->getKey(),
'name' => $user3->name, 'name' => $member3->user->name,
'description' => $timeEntry3->description, 'description' => $timeEntry3->description,
'time_entry_id' => $timeEntry3->getKey(), 'time_entry_id' => $timeEntry3->getKey(),
'task_id' => $task1->getKey(), 'task_id' => $task1->getKey(),
'status' => false, 'status' => false,
], ],
[ [
'user_id' => $user4->getKey(), 'member_id' => $member4->getKey(),
'name' => $user4->name, 'name' => $member4->user->name,
'description' => $timeEntry4->description, 'description' => $timeEntry4->description,
'time_entry_id' => $timeEntry4->getKey(), 'time_entry_id' => $timeEntry4->getKey(),
'task_id' => $task1->getKey(), 'task_id' => $task1->getKey(),
@@ -480,25 +479,26 @@ class DashboardServiceTest extends TestCase
// Arrange // Arrange
$organization = Organization::factory()->create(); $organization = Organization::factory()->create();
$user = User::factory()->create(); $user = User::factory()->create();
$member = Member::factory()->forUser($user)->forOrganization($organization)->create();
$task1 = Task::factory()->forOrganization($organization)->create(); $task1 = Task::factory()->forOrganization($organization)->create();
$task2 = Task::factory()->forOrganization($organization)->create(); $task2 = Task::factory()->forOrganization($organization)->create();
$task3 = Task::factory()->forOrganization($organization)->create(); $task3 = Task::factory()->forOrganization($organization)->create();
$task4 = Task::factory()->forOrganization($organization)->create(); $task4 = Task::factory()->forOrganization($organization)->create();
$task5 = Task::factory()->forOrganization($organization)->create(); $task5 = Task::factory()->forOrganization($organization)->create();
$timeEntry1Task1 = TimeEntry::factory()->forTask($task1)->forUser($user)->forOrganization($organization)->create([ $timeEntry1Task1 = TimeEntry::factory()->forTask($task1)->forMember($member)->forOrganization($organization)->create([
'start' => now()->subMinutes(20), 'start' => now()->subMinutes(20),
]); ]);
$timeEntry1Task2 = TimeEntry::factory()->forTask($task2)->forUser($user)->forOrganization($organization)->create([ $timeEntry1Task2 = TimeEntry::factory()->forTask($task2)->forMember($member)->forOrganization($organization)->create([
'start' => now()->subMinutes(30), 'start' => now()->subMinutes(30),
]); ]);
$timeEntry1Task3 = TimeEntry::factory()->forTask($task3)->forUser($user)->forOrganization($organization)->create([ $timeEntry1Task3 = TimeEntry::factory()->forTask($task3)->forMember($member)->forOrganization($organization)->create([
'start' => now()->subMinutes(40), 'start' => now()->subMinutes(40),
]); ]);
$timeEntry1Task4 = TimeEntry::factory()->forTask($task4)->forUser($user)->forOrganization($organization)->create([ $timeEntry1Task4 = TimeEntry::factory()->forTask($task4)->forMember($member)->forOrganization($organization)->create([
'start' => now()->subMinutes(50), 'start' => now()->subMinutes(50),
]); ]);
$timeEntry1Task5 = TimeEntry::factory()->forTask($task5)->forUser($user)->forOrganization($organization)->create([ $timeEntry1Task5 = TimeEntry::factory()->forTask($task5)->forMember($member)->forOrganization($organization)->create([
'start' => now()->subMinutes(60), 'start' => now()->subMinutes(60),
]); ]);
@@ -543,15 +543,16 @@ class DashboardServiceTest extends TestCase
$user = User::factory()->create([ $user = User::factory()->create([
'timezone' => 'Europe/Vienna', 'timezone' => 'Europe/Vienna',
]); ]);
$timeEntryOverWholePeriod = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $member = Member::factory()->forUser($user)->forOrganization($organization)->create();
$timeEntryOverWholePeriod = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
'start' => now('Europe/Vienna')->subDays(7)->startOfDay()->utc(), 'start' => now('Europe/Vienna')->subDays(7)->startOfDay()->utc(),
'end' => now('Europe/Vienna')->endOfDay()->addSecond()->utc(), // TODO: fix problem with last second 'end' => now('Europe/Vienna')->endOfDay()->addSecond()->utc(), // TODO: fix problem with last second
]); ]);
$timeEntryOverWholePeriodWithoutEnd = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntryOverWholePeriodWithoutEnd = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
'start' => now('Europe/Vienna')->subDays(7)->startOfDay()->utc(), 'start' => now('Europe/Vienna')->subDays(7)->startOfDay()->utc(),
'end' => null, 'end' => null,
]); ]);
$timeEntry1Task1 = TimeEntry::factory()->forUser($user)->forOrganization($organization)->create([ $timeEntry1Task1 = TimeEntry::factory()->forMember($member)->forOrganization($organization)->create([
'start' => now('Europe/Vienna')->subMinutes(30)->utc(), 'start' => now('Europe/Vienna')->subMinutes(30)->utc(),
'end' => now('Europe/Vienna')->subMinutes(20)->utc(), 'end' => now('Europe/Vienna')->subMinutes(20)->utc(),
]); ]);

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Unit\Service; namespace Tests\Unit\Service;
use App\Enums\Role;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use App\Service\PermissionStore; use App\Service\PermissionStore;
@@ -20,7 +21,7 @@ class PermissionStoreTest extends TestCase
// Arrange // Arrange
$organization = Organization::factory()->create(); $organization = Organization::factory()->create();
$user = User::factory()->create(); $user = User::factory()->create();
$organization->users()->attach($user, ['role' => 'employee']); $organization->users()->attach($user, ['role' => Role::Employee->value]);
$permissionStore = new PermissionStore(); $permissionStore = new PermissionStore();
// Act // Act
@@ -50,7 +51,7 @@ class PermissionStoreTest extends TestCase
// Arrange // Arrange
$organization = Organization::factory()->create(); $organization = Organization::factory()->create();
$user = User::factory()->create(); $user = User::factory()->create();
$organization->users()->attach($user, ['role' => 'employee']); $organization->users()->attach($user, ['role' => Role::Employee->value]);
$permissionStore = new PermissionStore(); $permissionStore = new PermissionStore();
$this->actingAs($user); $this->actingAs($user);
@@ -66,7 +67,7 @@ class PermissionStoreTest extends TestCase
// Arrange // Arrange
$organization = Organization::factory()->create(); $organization = Organization::factory()->create();
$user = User::factory()->create(); $user = User::factory()->create();
$organization->users()->attach($user, ['role' => 'employee']); $organization->users()->attach($user, ['role' => Role::Employee->value]);
$permissionStore = new PermissionStore(); $permissionStore = new PermissionStore();
$this->actingAs($user); $this->actingAs($user);
@@ -82,7 +83,7 @@ class PermissionStoreTest extends TestCase
// Arrange // Arrange
$organization = Organization::factory()->create(); $organization = Organization::factory()->create();
$user = User::factory()->create(); $user = User::factory()->create();
$organization->users()->attach($user, ['role' => 'employee']); $organization->users()->attach($user, ['role' => Role::Employee->value]);
$permissionStore = new PermissionStore(); $permissionStore = new PermissionStore();
// Act // Act
@@ -111,7 +112,7 @@ class PermissionStoreTest extends TestCase
// Arrange // Arrange
$organization = Organization::factory()->create(); $organization = Organization::factory()->create();
$user = User::factory()->create(); $user = User::factory()->create();
$organization->users()->attach($user, ['role' => 'employee']); $organization->users()->attach($user, ['role' => Role::Employee->value]);
$permissionStore = new PermissionStore(); $permissionStore = new PermissionStore();
$this->actingAs($user); $this->actingAs($user);
@@ -119,6 +120,6 @@ class PermissionStoreTest extends TestCase
$result = $permissionStore->getPermissions($organization); $result = $permissionStore->getPermissions($organization);
// Assert // Assert
$this->assertSame(Jetstream::findRole('employee')->permissions, $result); $this->assertSame(Jetstream::findRole(Role::Employee->value)->permissions, $result);
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Tests\Unit\Service; namespace Tests\Unit\Service;
use App\Enums\Role; use App\Enums\Role;
use App\Models\Membership; use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectMember; use App\Models\ProjectMember;
@@ -27,10 +27,13 @@ class UserServiceTest extends TestCase
$otherUser = User::factory()->create(); $otherUser = User::factory()->create();
$fromUser = User::factory()->create(); $fromUser = User::factory()->create();
$toUser = User::factory()->create(); $toUser = User::factory()->create();
TimeEntry::factory()->forOrganization($organization)->forUser($otherUser)->createMany(3); $otherUserMember = Member::factory()->forOrganization($organization)->forUser($otherUser)->create();
TimeEntry::factory()->forOrganization($organization)->forUser($fromUser)->createMany(3); $fromUserMember = Member::factory()->forOrganization($organization)->forUser($fromUser)->create();
ProjectMember::factory()->forProject($project)->forUser($otherUser)->create(); $toUserMember = Member::factory()->forOrganization($organization)->forUser($toUser)->create();
ProjectMember::factory()->forProject($project)->forUser($fromUser)->create(); TimeEntry::factory()->forOrganization($organization)->forMember($otherUserMember)->createMany(3);
TimeEntry::factory()->forOrganization($organization)->forMember($fromUserMember)->createMany(3);
ProjectMember::factory()->forProject($project)->forMember($otherUserMember)->create();
ProjectMember::factory()->forProject($project)->forMember($fromUserMember)->create();
// Act // Act
/** @var UserService $userService */ /** @var UserService $userService */
@@ -66,7 +69,7 @@ class UserServiceTest extends TestCase
// Assert // Assert
$this->assertSame($newOwner->getKey(), $organization->refresh()->user_id); $this->assertSame($newOwner->getKey(), $organization->refresh()->user_id);
$this->assertSame(Role::Owner->value, Membership::whereBelongsTo($newOwner)->whereBelongsTo($organization)->firstOrFail()->role); $this->assertSame(Role::Owner->value, Member::whereBelongsTo($newOwner)->whereBelongsTo($organization)->firstOrFail()->role);
$this->assertSame(Role::Admin->value, Membership::whereBelongsTo($oldOwner)->whereBelongsTo($organization)->firstOrFail()->role); $this->assertSame(Role::Admin->value, Member::whereBelongsTo($oldOwner)->whereBelongsTo($organization)->firstOrFail()->role);
} }
} }