From a0b64e6cb3b5127ec2b32fc69d970f450ee41835 Mon Sep 17 00:00:00 2001 From: EwelinaLasowy Date: Fri, 11 Feb 2022 13:02:35 +0100 Subject: [PATCH] #41 - wip --- .env.example | 2 +- .../VacationRequestNotification.php | 67 ++++ .../VacationRequestNotificationSender.php | 31 ++ .../Observers/VacationRequestObserver.php | 9 + config/mail.php | 2 +- resources/lang/pl.json | 9 +- .../views/vendor/mail/html/themes/mail.css | 286 ++++++++++++++++++ .../Unit/VacationRequestNotificationTest.php | 94 ++++++ tests/Unit/VacationRequestStatesTest.php | 3 + 9 files changed, 500 insertions(+), 3 deletions(-) create mode 100644 app/Domain/Notifications/VacationRequestNotification.php create mode 100644 app/Domain/VacationRequestNotificationSender.php create mode 100644 resources/views/vendor/mail/html/themes/mail.css create mode 100644 tests/Unit/VacationRequestNotificationTest.php diff --git a/.env.example b/.env.example index 49c10a7..9fb0255 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_NAME="Toby HR applicaiton" +APP_NAME="Toby HR application" APP_ENV=local APP_KEY= APP_DEBUG=true diff --git a/app/Domain/Notifications/VacationRequestNotification.php b/app/Domain/Notifications/VacationRequestNotification.php new file mode 100644 index 0000000..a869edd --- /dev/null +++ b/app/Domain/Notifications/VacationRequestNotification.php @@ -0,0 +1,67 @@ +user = $user; + $this->vacationRequest = $vacationRequest; + } + + public function via(): array + { + return ["mail"]; + } + + /** + * @throws InvalidArgumentException + */ + public function toMail(): MailMessage + { + $url = route( + "vacation.requests.show", + [ + "vacationRequest" => $this->vacationRequest, + ], + ); + + return $this->buildMailMessage($url); + } + + protected function buildMailMessage(string $url): MailMessage + { + $title = $this->vacationRequest->name; + $state = $this->vacationRequest->state->label(); + + $user = $this->user->getFullNameAttribute(); + + return (new MailMessage()) + ->greeting(__("Hi :user!", [ + "user" => $user, + ])) + ->subject(__("Vacation request :title", [ + "title" => $title, + ])) + ->line(__("The vacation request :title has changed state to :state.", [ + "title" => $title, + "state" => $state, + ])) + ->action(__("Show vacation request"), $url); + } +} diff --git a/app/Domain/VacationRequestNotificationSender.php b/app/Domain/VacationRequestNotificationSender.php new file mode 100644 index 0000000..1b04974 --- /dev/null +++ b/app/Domain/VacationRequestNotificationSender.php @@ -0,0 +1,31 @@ +getUsersForNotifications() as $user) { + $user->notify(new VacationRequestNotification($user, $vacationRequest)); + } + + $vacationRequest->user->notify(new VacationRequestNotification($vacationRequest->user, $vacationRequest)); + } + + protected function getUsersForNotifications(): Collection + { + return User::query() + ->where("role", Role::TECHNICAL_APPROVER) + ->orWhere("role", Role::ADMINISTRATIVE_APPROVER) + ->get(); + } +} diff --git a/app/Eloquent/Observers/VacationRequestObserver.php b/app/Eloquent/Observers/VacationRequestObserver.php index eec46bc..3d56bf3 100644 --- a/app/Eloquent/Observers/VacationRequestObserver.php +++ b/app/Eloquent/Observers/VacationRequestObserver.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Auth\Factory as Auth; use Illuminate\Events\Dispatcher; use Toby\Domain\Enums\VacationRequestState; use Toby\Domain\Events\VacationRequestStateChanged; +use Toby\Domain\VacationRequestNotificationSender; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationRequest; @@ -16,6 +17,7 @@ class VacationRequestObserver public function __construct( protected Auth $auth, protected Dispatcher $dispatcher, + protected VacationRequestNotificationSender $vacationRequestNotificationSender, ) { } @@ -39,6 +41,13 @@ class VacationRequestObserver } } + public function updated(VacationRequest $vacationRequest): void + { + if ($vacationRequest->state !== VacationRequestState::CREATED) { + $this->vacationRequestNotificationSender->sendVacationRequestNotification($vacationRequest); + } + } + protected function fireStateChangedEvent( VacationRequest $vacationRequest, ?VacationRequestState $from, diff --git a/config/mail.php b/config/mail.php index 6461e38..0bbffd9 100644 --- a/config/mail.php +++ b/config/mail.php @@ -35,7 +35,7 @@ return [ "name" => env("MAIL_FROM_NAME", "Example"), ], "markdown" => [ - "theme" => "default", + "theme" => "mail", "paths" => [ resource_path("views/vendor/mail"), ], diff --git a/resources/lang/pl.json b/resources/lang/pl.json index 01cf187..ee3c80a 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -30,5 +30,12 @@ "You have approved vacation request in this range.": "Masz zaakceptowany wniosek urlopowy w tym zakresie dat.", "You have exceeded your vacation limit.": "Przekroczyłeś/aś limit urlopu.", "Vacation needs minimum one day.": "Urlop musi być co najmniej na jeden dzień.", - "The vacation request cannot be created at the turn of the year.": "Wniosek urlopowy nie może zostać złożony na przełomie roku." + "The vacation request cannot be created at the turn of the year.": "Wniosek urlopowy nie może zostać złożony na przełomie roku.", + "Hi :user!": "Cześć :user!", + "The vacation request :title has changed state to :state.": "Wniosek urlopowy :title zmienił status na :state.", + "Vacation request :title": "Wniosek urlopowy :title", + "Regards": "Z poważaniem", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Jeżeli masz problemy z kliknięciem przycisku \":actionText\", skopiuj i wklej poniższy adres w pasek przeglądarki:", + "All rights reserved.": "Wszelkie prawa zastrzeżone", + "Show vacation request": "Pokaż wniosek" } diff --git a/resources/views/vendor/mail/html/themes/mail.css b/resources/views/vendor/mail/html/themes/mail.css new file mode 100644 index 0000000..d2f67d3 --- /dev/null +++ b/resources/views/vendor/mail/html/themes/mail.css @@ -0,0 +1,286 @@ +/* Base */ + +body, body *:not(html):not(style):not(br):not(tr):not(code) { + font-family: Avenir, Helvetica, sans-serif; + box-sizing: border-box; +} + +body { + background-color: #f5f8fa; + color: #74787e; + height: 100%; + hyphens: auto; + line-height: 1.4; + margin: 0; + -moz-hyphens: auto; + -ms-word-break: break-all; + width: 100% !important; + -webkit-hyphens: auto; + -webkit-text-size-adjust: none; + word-break: break-all; + word-break: break-word; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: left; +} + +a { + color: #3c5f97; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #2F3133; + font-size: 19px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h2 { + color: #2F3133; + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h3 { + color: #2F3133; + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + color: #74787e; + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + background-color: #f5f8fa; + margin: 0; + padding: 0; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.content { + margin: 0; + padding: 0; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #3c5f97; + font-size: 19px; + font-weight: bold; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +/* Body */ + +.body { + background-color: #ffffff; + border-bottom: 1px solid #edeff2; + border-top: 1px solid #edeff2; + margin: 0; + padding: 0; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.inner-body { + background-color: #ffffff; + margin: 0 auto; + padding: 0; + width: 570px; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #edeff2; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 12px; +} + +/* Footer */ + +.footer { + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; +} + +.footer p { + color: #aeaeae; + font-size: 12px; + text-align: center; +} + +/* Tables */ + +.table table { + margin: 30px auto; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.table th { + color: #74787e; + border-bottom: 1px solid #edeff2; + padding-bottom: 8px; +} + +.table td { + color: #74787e; + font-size: 15px; + line-height: 18px; + padding: 10px 0; +} + +.content-cell { + padding: 35px; +} + +/* Buttons */ + +.action { + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.button { + border-radius: 3px; + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); + color: #ffffff; + display: inline-block; + text-decoration: none; + -webkit-text-size-adjust: none; +} + +.button-blue, .button-primary { + background-color: #3c5f97; + border-top: 10px solid #3c5f97; + border-right: 18px solid #3c5f97; + border-bottom: 10px solid #3c5f97; + border-left: 18px solid #3c5f97; +} + +.button-green, .button-success { + background-color: #22c55e; + border-top: 10px solid #22c55e; + border-right: 18px solid #22c55e; + border-bottom: 10px solid #22c55e; + border-left: 18px solid #22c55e; +} + +.button-red, .button-error { + background-color: #ef4444; + border-top: 10px solid #ef4444; + border-right: 18px solid #ef4444; + border-bottom: 10px solid #ef4444; + border-left: 18px solid #ef4444; +} + +/* Panels */ + +.panel { + margin: 0 0 21px; +} + +.panel-content { + background-color: #edeff2; + padding: 16px; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Promotions */ + +.promotion { + background-color: #FFFFFF; + border: 2px dashed #9BA2AB; + margin: 0; + margin-bottom: 25px; + margin-top: 25px; + padding: 24px; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.promotion h1 { + text-align: center; +} + +.promotion p { + font-size: 15px; + text-align: center; +} diff --git a/tests/Unit/VacationRequestNotificationTest.php b/tests/Unit/VacationRequestNotificationTest.php new file mode 100644 index 0000000..09b8bae --- /dev/null +++ b/tests/Unit/VacationRequestNotificationTest.php @@ -0,0 +1,94 @@ +stateManager = $this->app->make(VacationRequestStateManager::class); + + $this->createCurrentYearPeriod(); + } + + public function testAfterChangingVacationRequestStateNotificationAreSentToUsers() :void + { + Notification::fake(); + + $user = User::factory(["role" => Role::EMPLOYEE])->createQuietly(); + $technicalApprover = User::factory(["role" => Role::TECHNICAL_APPROVER])->createQuietly(); + $administrativeApprover = User::factory(["role" => Role::ADMINISTRATIVE_APPROVER])->createQuietly(); + + $currentYearPeriod = YearPeriod::current(); + + /** @var VacationRequest $vacationRequest */ + $vacationRequest = VacationRequest::factory([ + "type" => VacationType::VACATION->value, + "state" => VacationRequestState::CREATED, + "from" => Carbon::create($currentYearPeriod->year, 2, 1)->toDateString(), + "to" => Carbon::create($currentYearPeriod->year, 2, 4)->toDateString(), + "comment" => "Comment for the vacation request.", + ]) + ->for($user) + ->for($currentYearPeriod) + ->create(); + + $this->stateManager->waitForTechnical($vacationRequest); + + Notification::assertSentTo([$user, $technicalApprover, $administrativeApprover],VacationRequestNotification::class); + } + + public function testAfterChangingVacationRequestStateNotificationIsNotSentToAnotherEmployee(): void { + Notification::fake(); + + $user = User::factory(["role" => Role::EMPLOYEE])->createQuietly(); + $anotherUser = User::factory(["role" => Role::EMPLOYEE])->createQuietly(); + $technicalApprover = User::factory(["role" => Role::TECHNICAL_APPROVER])->createQuietly(); + $administrativeApprover = User::factory(["role" => Role::ADMINISTRATIVE_APPROVER])->createQuietly(); + + + $currentYearPeriod = YearPeriod::current(); + + /** @var VacationRequest $vacationRequest */ + $vacationRequest = VacationRequest::factory([ + "type" => VacationType::VACATION->value, + "state" => VacationRequestState::CREATED, + "from" => Carbon::create($currentYearPeriod->year, 2, 1)->toDateString(), + "to" => Carbon::create($currentYearPeriod->year, 2, 4)->toDateString(), + "comment" => "Comment for the vacation request.", + ]) + ->for($user) + ->for($currentYearPeriod) + ->create(); + + $this->stateManager->waitForTechnical($vacationRequest); + + Notification::assertSentTo([$user, $technicalApprover, $administrativeApprover],VacationRequestNotification::class); + Notification::assertNotSentTo([$anotherUser],VacationRequestNotification::class); + } +} + + diff --git a/tests/Unit/VacationRequestStatesTest.php b/tests/Unit/VacationRequestStatesTest.php index 02479c7..43069f0 100644 --- a/tests/Unit/VacationRequestStatesTest.php +++ b/tests/Unit/VacationRequestStatesTest.php @@ -5,11 +5,14 @@ declare(strict_types=1); namespace Tests\Unit; use Illuminate\Foundation\Testing\DatabaseMigrations; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Carbon; use Tests\TestCase; use Tests\Traits\InteractsWithYearPeriods; +use Toby\Domain\Enums\Role; use Toby\Domain\Enums\VacationRequestState; use Toby\Domain\Enums\VacationType; +use Toby\Domain\Notifications\VacationRequestNotification; use Toby\Domain\VacationRequestStateManager; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationRequest;