#126 - vacation request reminders

This commit is contained in:
Adrian Hopek 2022-04-25 15:18:48 +02:00
parent d60dc75f99
commit dcce188419
11 changed files with 227 additions and 12 deletions

View File

@ -16,7 +16,6 @@ class ExceptionHandler extends Handler
"password",
"password_confirmation",
];
protected array $handleByInertia = [
Response::HTTP_INTERNAL_SERVER_ERROR,
Response::HTTP_SERVICE_UNAVAILABLE,

View File

@ -99,4 +99,3 @@ class VacationRequestWaitsForApprovalNotification extends Notification
]);
}
}

View File

@ -9,5 +9,6 @@ use Toby\Eloquent\Models\VacationRequest;
interface VacationRequestRule
{
public function check(VacationRequest $vacationRequest): bool;
public function errorMessage(): string;
}

View File

@ -21,7 +21,6 @@ class Holiday extends Model
use HasFactory;
protected $guarded = [];
protected $casts = [
"date" => "date",
];

View File

@ -26,9 +26,7 @@ class Profile extends Model
use HasAvatar;
protected $primaryKey = "user_id";
protected $guarded = [];
protected $casts = [
"employment_form" => EmploymentForm::class,
"employment_date" => "date",

View File

@ -33,18 +33,15 @@ class User extends Authenticatable
use SoftDeletes;
protected $guarded = [];
protected $casts = [
"role" => Role::class,
"last_active_at" => "datetime",
"employment_form" => EmploymentForm::class,
"employment_date" => "date",
];
protected $hidden = [
"remember_token",
];
protected $with = [
"profile",
];

View File

@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Spatie\ModelStates\HasStates;
@ -41,7 +42,6 @@ class VacationRequest extends Model
use HasStates;
protected $guarded = [];
protected $casts = [
"type" => VacationType::class,
"state" => VacationRequestState::class,
@ -85,6 +85,13 @@ class VacationRequest extends Model
return $query->whereNotState("state", $states);
}
public function scopeType(Builder $query, VacationType|array $types): Builder
{
$types = Arr::wrap($types);
return $query->whereIn("type", $types);
}
public function scopeOverlapsWith(Builder $query, self $vacationRequest): Builder
{
return $query->where("from", "<=", $vacationRequest->to)

View File

@ -22,7 +22,6 @@ class VacationRequestActivity extends Model
use HasFactory;
protected $guarded = [];
protected $casts = [
"from" => VacationRequestState::class,
"to" => VacationRequestState::class,

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Toby\Domain\Enums\Role;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\Notifications\VacationRequestWaitsForApprovalNotification;
use Toby\Domain\States\VacationRequest\WaitingForAdministrative;
use Toby\Domain\States\VacationRequest\WaitingForTechnical;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class SendVacationRequestRemindersToApprovers extends Command
{
public const REMINDER_INTERVAL = 3;
protected $signature = "toby:send-vacation-request-reminders";
protected $description = "Sends vacation request reminders to approvers if they didn't approve";
public function handle(VacationTypeConfigRetriever $configRetriever): void
{
$vacationRequests = VacationRequest::query()
->type(VacationType::all()->filter(fn(VacationType $type) => $configRetriever->isVacation($type))->all())
->get();
/** @var VacationRequest $vacationRequest */
foreach ($vacationRequests as $vacationRequest) {
if (!$this->shouldNotify($vacationRequest)) {
continue;
}
if ($vacationRequest->state->equals(WaitingForTechnical::class)) {
$this->notifyTechnicalApprovers($vacationRequest);
}
if ($vacationRequest->state->equals(WaitingForAdministrative::class)) {
$this->notifyAdminApprovers($vacationRequest);
}
}
}
protected function shouldNotify(VacationRequest $vacationRequest): bool
{
$today = Carbon::today();
$diff = $vacationRequest->updated_at->diffInDays($today);
return $diff >= static::REMINDER_INTERVAL && ($diff % static::REMINDER_INTERVAL === 0);
}
protected function notifyAdminApprovers(VacationRequest $vacationRequest): void
{
$users = User::query()
->whereIn("role", [Role::AdministrativeApprover, Role::Administrator])
->get();
foreach ($users as $user) {
$user->notify(new VacationRequestWaitsForApprovalNotification($vacationRequest, $user));
}
}
protected function notifyTechnicalApprovers(VacationRequest $vacationRequest): void
{
$users = User::query()
->whereIn("role", [Role::TechnicalApprover, Role::Administrator])
->get();
foreach ($users as $user) {
$user->notify(new VacationRequestWaitsForApprovalNotification($vacationRequest, $user));
}
}
}

View File

@ -40,7 +40,6 @@ class Kernel extends HttpKernel
TrimStrings::class,
ConvertEmptyStringsToNull::class,
];
protected $middlewareGroups = [
"web" => [
EncryptCookies::class,
@ -58,7 +57,6 @@ class Kernel extends HttpKernel
SubstituteBindings::class,
],
];
protected $routeMiddleware = [
"auth" => Authenticate::class,
"auth.basic" => AuthenticateWithBasicAuth::class,

View File

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
use Tests\Traits\InteractsWithYearPeriods;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\Notifications\VacationRequestWaitsForApprovalNotification;
use Toby\Domain\States\VacationRequest\WaitingForAdministrative;
use Toby\Domain\States\VacationRequest\WaitingForTechnical;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Eloquent\Models\YearPeriod;
use Toby\Infrastructure\Console\Commands\SendVacationRequestRemindersToApprovers;
class SendVacationRequestRemindersTest extends TestCase
{
use DatabaseMigrations;
use InteractsWithYearPeriods;
protected function setUp(): void
{
parent::setUp();
$this->createCurrentYearPeriod();
Notification::fake();
}
public function testReminderIsSentIfItsBeenThreeDaysSinceTheUpdate(): void
{
$currentYearPeriod = YearPeriod::current();
$now = Carbon::today();
$this->travelTo($now);
$user = User::factory()->create();
$technicalApprover = User::factory()
->technicalApprover()
->create();
VacationRequest::factory([
"type" => VacationType::Vacation->value,
"state" => WaitingForTechnical::class,
])
->for($user)
->for($currentYearPeriod)
->create();
$this->travelTo($now->addDays(3));
$this->artisan(SendVacationRequestRemindersToApprovers::class);
Notification::assertSentTo([$technicalApprover], VacationRequestWaitsForApprovalNotification::class);
}
public function testReminderIsSentIfItsBeenAnotherThreeDaysSinceTheUpdate(): void
{
$currentYearPeriod = YearPeriod::current();
$now = Carbon::today();
$this->travelTo($now);
$user = User::factory()->create();
$technicalApprover = User::factory()
->technicalApprover()
->create();
VacationRequest::factory([
"type" => VacationType::Vacation->value,
"state" => WaitingForTechnical::class,
])
->for($user)
->for($currentYearPeriod)
->create();
$this->travelTo($now->addDays(6));
$this->artisan(SendVacationRequestRemindersToApprovers::class);
Notification::assertSentTo([$technicalApprover], VacationRequestWaitsForApprovalNotification::class);
}
public function testReminderIsNotSentIfItHasntBeenThreeDays(): void
{
$currentYearPeriod = YearPeriod::current();
$now = Carbon::today();
$this->travelTo($now);
$user = User::factory()->create();
$technicalApprover = User::factory()
->technicalApprover()
->create();
VacationRequest::factory([
"type" => VacationType::Vacation->value,
"state" => WaitingForTechnical::class,
])
->for($user)
->for($currentYearPeriod)
->create();
$this->travelTo($now->addDays(2));
$this->artisan(SendVacationRequestRemindersToApprovers::class);
Notification::assertNotSentTo([$technicalApprover], VacationRequestWaitsForApprovalNotification::class);
}
public function testReminderIsSentToProperApprover(): void
{
$currentYearPeriod = YearPeriod::current();
$now = Carbon::today();
$this->travelTo($now);
$user = User::factory()->create();
$adminApprover = User::factory()
->administrativeApprover()
->create();
$technicalApprover = User::factory()
->technicalApprover()
->create();
VacationRequest::factory([
"type" => VacationType::Vacation->value,
"state" => WaitingForAdministrative::class,
])
->for($user)
->for($currentYearPeriod)
->create();
$this->travelTo($now->addDays(3));
$this->artisan(SendVacationRequestRemindersToApprovers::class);
Notification::assertSentTo([$adminApprover], VacationRequestWaitsForApprovalNotification::class);
Notification::assertNotSentTo([$technicalApprover, $user], VacationRequestWaitsForApprovalNotification::class);
}
}