Merge branch 'main' into #20-vacation-requests

# Conflicts:
#	app/Architecture/Providers/ObserverServiceProvider.php
#	app/Domain/Role.php
#	app/Domain/Rules/ApprovedVacationDaysInSameRange.php
#	app/Domain/Rules/DoesNotExceedLimitRule.php
#	app/Domain/Rules/MinimumOneVacationDayRule.php
#	app/Domain/Rules/PendingVacationRequestInSameRange.php
#	app/Domain/Rules/UsedVacationDaysInSameRange.php
#	app/Domain/Rules/VacationRequestRule.php
#	app/Domain/VacationRequestState.php
#	app/Domain/VacationRequestStateManager.php
#	app/Domain/VacationRequestValidator.php
#	app/Domain/VacationType.php
#	app/Domain/VacationTypeConfigRetriever.php
#	app/Eloquent/Models/User.php
#	app/Infrastructure/Http/Controllers/UserController.php
#	app/Infrastructure/Http/Kernel.php
#	app/Infrastructure/Http/Requests/UserRequest.php
#	database/factories/UserFactory.php
#	database/seeders/DatabaseSeeder.php
#	routes/web.php
#	tests/Feature/UserTest.php
This commit is contained in:
Adrian Hopek
2022-02-01 11:10:59 +01:00
106 changed files with 333 additions and 303 deletions

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Enums;
enum EmploymentForm: string
{
case EMPLOYMENT_CONTRACT = "employment_contract";
case COMMISSION_CONTRACT = "commission_contract";
case B2B_CONTRACT = "b2b_contract";
case BOARD_MEMBER_CONTRACT = "board_member_contract";
public function label(): string
{
return __($this->value);
}
public static function casesToSelect(): array
{
$cases = collect(EmploymentForm::cases());
return $cases->map(
fn(EmploymentForm $enum) => [
"label" => $enum->label(),
"value" => $enum->value,
],
)->toArray();
}
}

30
app/Domain/Enums/Role.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Enums;
enum Role: string
{
case EMPLOYEE = "employee";
case ADMINISTRATOR = "administrator";
case TECHNICAL_APPROVER = "technical_approver";
case ADMINISTRATIVE_APPROVER = "administrative_approver";
public function label(): string
{
return __($this->value);
}
public static function casesToSelect(): array
{
$cases = collect(Role::cases());
return $cases->map(
fn(Role $enum) => [
"label" => $enum->label(),
"value" => $enum->value,
],
)->toArray();
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Enums;
enum VacationRequestState: string
{
case CREATED = "created";
case CANCELED = "canceled";
case REJECTED = "rejected";
case APPROVED = "approved";
case WAITING_FOR_TECHNICAL = "waiting_for_technical";
case WAITING_FOR_ADMINISTRATIVE = "waiting_for_administrative";
case ACCEPTED_BY_TECHNICAL = "accepted_by_technical";
case ACCEPTED_BY_ADMINSTRATIVE = "accepted_by_administrative";
public function label(): string
{
return __($this->value);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Enums;
enum VacationType: string
{
case VACATION = "vacation";
case VACATION_ON_REQUEST = "vacation_on_request";
case SPECIAL_VACATION = "special_vacation";
case CHILDCARE_VACATION = "childcare_vacation";
case TRAINING_VACATION = "training_vacation";
case UNPAID_VACATION = "unpaid_vacation";
case VOLUNTEERING_VACATION = "volunteering_vacation";
case TIME_IN_LIEU = "time_in_lieu";
case SICK_VACATION = "sick_vacation";
public function label(): string
{
return __($this->value);
}
public static function casesToSelect(): array
{
$cases = collect(VacationType::cases());
return $cases->map(
fn(VacationType $enum) => [
"label" => $enum->label(),
"value" => $enum->value,
],
)->toArray();
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestAcceptedByAdministrative
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestAcceptedByTechnical
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestApproved
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestCreated
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestStateChanged
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
public ?VacationRequestState $from,
public VacationRequestState $to,
public ?User $user = null,
) {
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestStateChanged;
class CreateVacationRequestActivity
{
public function handle(VacationRequestStateChanged $event): void
{
$event->vacationRequest->activities()->create([
"from" => $event->from,
"to" => $event->to,
"user_id" => $event->user?->id,
]);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
use Toby\Domain\VacationRequestStateManager;
class HandleAcceptedByAdministrativeVacationRequest
{
public function __construct(
protected VacationRequestStateManager $stateManager,
) {
}
public function handle(VacationRequestAcceptedByAdministrative $event): void
{
$this->stateManager->approve($event->vacationRequest);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
class HandleAcceptedByTechnicalVacationRequest
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationRequestStateManager $stateManager,
) {
}
public function handle(VacationRequestAcceptedByTechnical $event): void
{
$vacationRequest = $event->vacationRequest;
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
$this->stateManager->waitForAdministrative($vacationRequest);
return;
}
$this->stateManager->approve($vacationRequest);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Illuminate\Support\Facades\Log;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Domain\VacationTypeConfigRetriever;
class HandleApprovedVacationRequest
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
) {
}
public function handle(VacationRequestApproved $event): void
{
$vacationRequest = $event->vacationRequest;
Log::info("approved! {$vacationRequest->id}");
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
class HandleCreatedVacationRequest
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationRequestStateManager $stateManager,
) {
}
public function handle(VacationRequestCreated $event): void
{
$vacationRequest = $event->vacationRequest;
if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) {
$this->stateManager->waitForTechnical($vacationRequest);
return;
}
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
$this->stateManager->waitForAdministrative($vacationRequest);
return;
}
$this->stateManager->approve($vacationRequest);
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Eloquent\Models\YearPeriod;
use Yasumi\Holiday;
use Yasumi\Yasumi;
class PolishHolidaysRetriever
{
protected const PROVIDER_KEY = "Poland";
protected const LANG_KEY = "pl";
public function getForYearPeriod(YearPeriod $yearPeriod): Collection
{
$polishProvider = Yasumi::create(static::PROVIDER_KEY, $yearPeriod->year);
$holidays = $polishProvider->getHolidays();
return $this->prepareHolidays($holidays);
}
protected function prepareHolidays(array $holidays): Collection
{
return collect($holidays)->map(fn(Holiday $holiday) => [
"name" => $holiday->getName([static::LANG_KEY]),
"date" => Carbon::createFromTimestamp($holiday->getTimestamp()),
])->values();
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Contracts\Events\Dispatcher;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestStateManager
{
public function __construct(
protected Auth $auth,
protected Dispatcher $dispatcher,
) {
}
public function markAsCreated(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::CREATED);
$this->dispatcher->dispatch(new VacationRequestCreated($vacationRequest));
}
public function approve(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::APPROVED);
$this->dispatcher->dispatch(new VacationRequestApproved($vacationRequest));
}
public function reject(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::REJECTED);
}
public function cancel(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::CANCELED);
}
public function acceptAsTechnical(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::ACCEPTED_BY_TECHNICAL);
$this->dispatcher->dispatch(new VacationRequestAcceptedByTechnical($vacationRequest));
}
public function acceptAsAdministrative(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::ACCEPTED_BY_ADMINSTRATIVE);
$this->dispatcher->dispatch(new VacationRequestAcceptedByAdministrative($vacationRequest));
}
public function waitForTechnical(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::WAITING_FOR_TECHNICAL);
}
public function waitForAdministrative(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::WAITING_FOR_ADMINISTRATIVE);
}
protected function changeState(VacationRequest $vacationRequest, VacationRequestState $state): void
{
$vacationRequest->changeStateTo($state);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Contracts\Config\Repository;
use Toby\Domain\Enums\VacationType;
class VacationTypeConfigRetriever
{
public const KEY_TECHNICAL_APPROVAL = "technical_approval";
public const KEY_ADMINISTRATIVE_APPROVAL = "administrative_approval";
public const KEY_BILLABLE = "billable";
public const KEY_HAS_LIMIT = "has_limit";
public function __construct(
protected Repository $config,
) {
}
public function needsTechnicalApproval(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_TECHNICAL_APPROVAL];
}
public function needsAdministrativeApproval(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_ADMINISTRATIVE_APPROVAL];
}
public function isBillable(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_BILLABLE];
}
public function hasLimit(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_HAS_LIMIT];
}
protected function getConfigFor(VacationType $type): array
{
return $this->config->get("vacation_types.{$type->value}");
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class ApprovedVacationDaysInSameRange implements VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class DoesNotExceedLimitRule implements VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class MinimumOneVacationDayRule implements VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class PendingVacationRequestInSameRange implements VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class UsedVacationDaysInSameRange
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
interface VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next);
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation;
use Illuminate\Pipeline\Pipeline;
use Toby\Domain\Validation\Rules\ApprovedVacationDaysInSameRange;
use Toby\Domain\Validation\Rules\DoesNotExceedLimitRule;
use Toby\Domain\Validation\Rules\MinimumOneVacationDayRule;
use Toby\Domain\Validation\Rules\PendingVacationRequestInSameRange;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestValidator
{
protected array $rules = [
MinimumOneVacationDayRule::class,
DoesNotExceedLimitRule::class,
PendingVacationRequestInSameRange::class,
ApprovedVacationDaysInSameRange::class,
];
public function __construct(
protected Pipeline $pipeline,
) {
}
public function validate(VacationRequest $vacationRequest): void
{
$this->pipeline
->send($vacationRequest)
->through($this->rules)
->via("check");
}
}