#20 - vacation requests (#35)

* #20 - wip

* #20 - wip

* #20 - fix

* #20 - wip

* #20 - fix

* #20 - fix
This commit is contained in:
Adrian Hopek
2022-02-03 08:39:09 +01:00
committed by GitHub
parent f6d59f8bfb
commit 067b343f24
73 changed files with 5750 additions and 3576 deletions

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Toby\Domain;
namespace Toby\Domain\Enums;
enum EmploymentForm: string
{

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,56 @@
<?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);
}
public static function pendingStates(): array
{
return [
self::CREATED,
self::WAITING_FOR_TECHNICAL,
self::WAITING_FOR_ADMINISTRATIVE,
self::ACCEPTED_BY_TECHNICAL,
self::ACCEPTED_BY_ADMINSTRATIVE,
];
}
public static function successStates(): array
{
return [self::APPROVED];
}
public static function failedStates(): array
{
return [
self::REJECTED,
self::CANCELED,
];
}
public static function filterByStatus(string $filter): array
{
return match ($filter) {
"pending" => VacationRequestState::pendingStates(),
"success" => VacationRequestState::successStates(),
"failed" => VacationRequestState::failedStates(),
default => VacationRequestState::cases(),
};
}
}

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,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");
}
}