diff --git a/app/Architecture/ExceptionHandler.php b/app/Architecture/ExceptionHandler.php index b286bd1..a43fcab 100644 --- a/app/Architecture/ExceptionHandler.php +++ b/app/Architecture/ExceptionHandler.php @@ -16,7 +16,6 @@ class ExceptionHandler extends Handler "password", "password_confirmation", ]; - protected array $handleByInertia = [ Response::HTTP_INTERNAL_SERVER_ERROR, Response::HTTP_SERVICE_UNAVAILABLE, diff --git a/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php b/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php index 109eef9..90b5507 100644 --- a/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php +++ b/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php @@ -99,4 +99,3 @@ class VacationRequestWaitsForApprovalNotification extends Notification ]); } } - diff --git a/app/Domain/Validation/Rules/VacationRequestRule.php b/app/Domain/Validation/Rules/VacationRequestRule.php index 07af8d2..f7e3c6d 100644 --- a/app/Domain/Validation/Rules/VacationRequestRule.php +++ b/app/Domain/Validation/Rules/VacationRequestRule.php @@ -9,5 +9,6 @@ use Toby\Eloquent\Models\VacationRequest; interface VacationRequestRule { public function check(VacationRequest $vacationRequest): bool; + public function errorMessage(): string; } diff --git a/app/Eloquent/Models/Holiday.php b/app/Eloquent/Models/Holiday.php index be8ae49..466065c 100644 --- a/app/Eloquent/Models/Holiday.php +++ b/app/Eloquent/Models/Holiday.php @@ -21,7 +21,6 @@ class Holiday extends Model use HasFactory; protected $guarded = []; - protected $casts = [ "date" => "date", ]; diff --git a/app/Eloquent/Models/Profile.php b/app/Eloquent/Models/Profile.php index df237e9..b8b3dc4 100644 --- a/app/Eloquent/Models/Profile.php +++ b/app/Eloquent/Models/Profile.php @@ -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", diff --git a/app/Eloquent/Models/User.php b/app/Eloquent/Models/User.php index 4bf2891..24bb49c 100644 --- a/app/Eloquent/Models/User.php +++ b/app/Eloquent/Models/User.php @@ -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", ]; diff --git a/app/Eloquent/Models/VacationRequest.php b/app/Eloquent/Models/VacationRequest.php index 46bd084..cb77f1c 100644 --- a/app/Eloquent/Models/VacationRequest.php +++ b/app/Eloquent/Models/VacationRequest.php @@ -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) diff --git a/app/Eloquent/Models/VacationRequestActivity.php b/app/Eloquent/Models/VacationRequestActivity.php index 942bb96..2064e9e 100644 --- a/app/Eloquent/Models/VacationRequestActivity.php +++ b/app/Eloquent/Models/VacationRequestActivity.php @@ -22,7 +22,6 @@ class VacationRequestActivity extends Model use HasFactory; protected $guarded = []; - protected $casts = [ "from" => VacationRequestState::class, "to" => VacationRequestState::class, diff --git a/app/Infrastructure/Console/Commands/SendVacationRequestRemindersToApprovers.php b/app/Infrastructure/Console/Commands/SendVacationRequestRemindersToApprovers.php new file mode 100644 index 0000000..3d0fb36 --- /dev/null +++ b/app/Infrastructure/Console/Commands/SendVacationRequestRemindersToApprovers.php @@ -0,0 +1,76 @@ +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)); + } + } +} diff --git a/app/Infrastructure/Http/Kernel.php b/app/Infrastructure/Http/Kernel.php index 5c6a238..5b1e42f 100644 --- a/app/Infrastructure/Http/Kernel.php +++ b/app/Infrastructure/Http/Kernel.php @@ -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, diff --git a/tests/Unit/SendVacationRequestRemindersTest.php b/tests/Unit/SendVacationRequestRemindersTest.php new file mode 100644 index 0000000..a7ebc57 --- /dev/null +++ b/tests/Unit/SendVacationRequestRemindersTest.php @@ -0,0 +1,142 @@ +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); + } +}