-
-
Notifications
You must be signed in to change notification settings - Fork 144
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added mail to inform users about still running time entries
- Loading branch information
Showing
10 changed files
with
347 additions
and
1 deletion.
There are no files selected for viewing
70 changes: 70 additions & 0 deletions
70
app/Console/Commands/TimeEntry/TimeEntrySendStillRunningMailsCommand.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Console\Commands\TimeEntry; | ||
|
||
use App\Mail\TimeEntryStillRunningMail; | ||
use App\Models\TimeEntry; | ||
use Illuminate\Console\Command; | ||
use Illuminate\Database\Eloquent\Collection; | ||
use Illuminate\Support\Carbon; | ||
use Illuminate\Support\Facades\Mail; | ||
|
||
class TimeEntrySendStillRunningMailsCommand extends Command | ||
{ | ||
/** | ||
* The name and signature of the console command. | ||
* | ||
* @var string | ||
*/ | ||
protected $signature = 'time-entry:send-still-running-mails '. | ||
' { --dry-run : Do not actually send emails or save anything to the database, just output what would happen }'; | ||
|
||
/** | ||
* The console command description. | ||
* | ||
* @var string | ||
*/ | ||
protected $description = 'Sends emails to users who have running time entries for more than 8 hours.'; | ||
|
||
/** | ||
* Execute the console command. | ||
*/ | ||
public function handle(): int | ||
{ | ||
$this->comment('Sending still running time entry emails...'); | ||
$dryRun = (bool) $this->option('dry-run'); | ||
if ($dryRun) { | ||
$this->comment('Running in dry-run mode. No emails will be sent and nothing will be saved to the database.'); | ||
} | ||
|
||
$sentMails = 0; | ||
TimeEntry::query() | ||
->whereNull('end') | ||
->where('start', '<', now()->subHours(8)) | ||
->whereNull('still_active_email_sent_at') | ||
->with([ | ||
'user', | ||
]) | ||
->orderBy('created_at', 'asc') | ||
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails) { | ||
/** @var Collection<int, TimeEntry> $timeEntries */ | ||
foreach ($timeEntries as $timeEntry) { | ||
$user = $timeEntry->user; | ||
$this->info('Start sending email to user "'.$user->email.'" ('.$user->getKey().') for time entry '.$timeEntry->getKey()); | ||
$sentMails++; | ||
if (! $dryRun) { | ||
Mail::to($user->email) | ||
->queue(new TimeEntryStillRunningMail($timeEntry, $user)); | ||
$timeEntry->still_active_email_sent_at = Carbon::now(); | ||
$timeEntry->save(); | ||
} | ||
} | ||
}); | ||
|
||
$this->comment('Finished sending '.$sentMails.' still running time entry emails...'); | ||
|
||
return self::SUCCESS; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Mail; | ||
|
||
use App\Models\TimeEntry; | ||
use App\Models\User; | ||
use Illuminate\Bus\Queueable; | ||
use Illuminate\Mail\Mailable; | ||
use Illuminate\Queue\SerializesModels; | ||
use Illuminate\Support\Facades\URL; | ||
|
||
class TimeEntryStillRunningMail extends Mailable | ||
{ | ||
use Queueable, SerializesModels; | ||
|
||
public TimeEntry $timeEntry; | ||
|
||
public User $user; | ||
|
||
/** | ||
* Create a new message instance. | ||
* | ||
* @return void | ||
*/ | ||
public function __construct(TimeEntry $timeEntry, User $user) | ||
{ | ||
$this->timeEntry = $timeEntry; | ||
$this->user = $user; | ||
} | ||
|
||
/** | ||
* Build the message. | ||
*/ | ||
public function build(): self | ||
{ | ||
return $this->markdown('emails.time-entry-still-running', [ | ||
'dashboardUrl' => URL::route('dashboard'), | ||
]) | ||
->subject(__('Your Time Tracker is still running!')); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
return [ | ||
|
||
'tasks' => [ | ||
'time_entry_send_still_running_mails' => (bool) env('SCHEDULING_TASK_TIME_ENTRY_SEND_STILL_RUNNING_MAILS', true), | ||
], | ||
]; |
30 changes: 30 additions & 0 deletions
30
...ase/migrations/2024_07_18_080906_add_still_active_email_sent_at_to_time_entries_table.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
use Illuminate\Database\Migrations\Migration; | ||
use Illuminate\Database\Schema\Blueprint; | ||
use Illuminate\Support\Facades\Schema; | ||
|
||
return new class extends Migration | ||
{ | ||
/** | ||
* Run the migrations. | ||
*/ | ||
public function up(): void | ||
{ | ||
Schema::table('time_entries', function (Blueprint $table) { | ||
$table->dateTime('still_active_email_sent_at')->nullable(); | ||
}); | ||
} | ||
|
||
/** | ||
* Reverse the migrations. | ||
*/ | ||
public function down(): void | ||
{ | ||
Schema::table('time_entries', function (Blueprint $table) { | ||
$table->dropColumn('still_active_email_sent_at'); | ||
}); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
@component('mail::message') | ||
@if(empty($timeEntry->description)) | ||
{{ __('Your currently running time entry is now running for more than 8 hours!') }} | ||
@else | ||
{{ __('Your currently running time entry ":description" is now running for more than 8 hours!', ['description' => $timeEntry->description]) }} | ||
@endif | ||
|
||
{{ __('If you forgot to stop the Time Tracker you do that in solidtime:') }} | ||
|
||
@component('mail::button', ['url' => $dashboardUrl]) | ||
{{ __('Go to solidtime') }} | ||
@endcomponent | ||
|
||
@endcomponent |
140 changes: 140 additions & 0 deletions
140
tests/Unit/Console/Commands/TimeEntry/TimeEntrySendStillRunningMailsCommandTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Tests\Unit\Console\Commands\TimeEntry; | ||
|
||
use App\Console\Commands\TimeEntry\TimeEntrySendStillRunningMailsCommand; | ||
use App\Mail\TimeEntryStillRunningMail; | ||
use App\Models\TimeEntry; | ||
use Illuminate\Console\Command; | ||
use Illuminate\Support\Carbon; | ||
use Illuminate\Support\Facades\Artisan; | ||
use Illuminate\Support\Facades\Mail; | ||
use PHPUnit\Framework\Attributes\CoversClass; | ||
use PHPUnit\Framework\Attributes\UsesClass; | ||
use Tests\TestCaseWithDatabase; | ||
|
||
#[CoversClass(TimeEntrySendStillRunningMailsCommand::class)] | ||
#[UsesClass(TimeEntrySendStillRunningMailsCommand::class)] | ||
class TimeEntrySendStillRunningMailsCommandTest extends TestCaseWithDatabase | ||
{ | ||
public function test_sends_mails_for_still_running_time_entries(): void | ||
{ | ||
// Arrange | ||
$user = $this->createUserWithPermission(); | ||
$timeEntryRunningLongerThanThreshold = TimeEntry::factory()->forMember($user->member)->create([ | ||
'start' => Carbon::now()->subHours(8)->subSecond(), | ||
'end' => null, | ||
]); | ||
|
||
// Act | ||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails'); | ||
|
||
// Assert | ||
Mail::assertQueued(TimeEntryStillRunningMail::class, function ($mail) use ($user, $timeEntryRunningLongerThanThreshold) { | ||
return $mail->hasTo($user->user->email) && | ||
$mail->timeEntry->is($timeEntryRunningLongerThanThreshold) && | ||
$mail->user->is($user->user); | ||
}); | ||
$timeEntryRunningLongerThanThreshold->refresh(); | ||
$this->assertNotNull($timeEntryRunningLongerThanThreshold->still_active_email_sent_at); | ||
$this->assertSame(Command::SUCCESS, $exitCode); | ||
$output = Artisan::output(); | ||
$this->assertSame("Sending still running time entry emails...\n". | ||
'Start sending email to user "'.$user->user->email.'" ('.$user->user->getKey().') for time entry '.$timeEntryRunningLongerThanThreshold->getKey()."\n". | ||
"Finished sending 1 still running time entry emails...\n", $output); | ||
|
||
} | ||
|
||
public function test_does_not_send_emails_for_not_running_time_entries(): void | ||
{ | ||
// Arrange | ||
$user = $this->createUserWithPermission(); | ||
$timeEntry = TimeEntry::factory()->forMember($user->member)->create([ | ||
'start' => Carbon::now()->subHours(8)->subSecond(), | ||
'end' => Carbon::now(), | ||
]); | ||
|
||
// Act | ||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails'); | ||
|
||
// Assert | ||
Mail::assertNothingOutgoing(); | ||
$timeEntry->refresh(); | ||
$this->assertNull($timeEntry->still_active_email_sent_at); | ||
$this->assertSame(Command::SUCCESS, $exitCode); | ||
$output = Artisan::output(); | ||
$this->assertSame("Sending still running time entry emails...\n". | ||
"Finished sending 0 still running time entry emails...\n", $output); | ||
} | ||
|
||
public function test_does_not_send_emails_for_running_time_entries_that_are_short_than_the_threshold(): void | ||
{ | ||
// Arrange | ||
$user = $this->createUserWithPermission(); | ||
$timeEntry = TimeEntry::factory()->forMember($user->member)->create([ | ||
'start' => Carbon::now()->subHours(8)->addMinute(), | ||
'end' => null, | ||
]); | ||
|
||
// Act | ||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails'); | ||
|
||
// Assert | ||
Mail::assertNothingOutgoing(); | ||
$timeEntry->refresh(); | ||
$this->assertNull($timeEntry->still_active_email_sent_at); | ||
$this->assertSame(Command::SUCCESS, $exitCode); | ||
$output = Artisan::output(); | ||
$this->assertSame("Sending still running time entry emails...\n". | ||
"Finished sending 0 still running time entry emails...\n", $output); | ||
} | ||
|
||
public function test_does_not_send_emails_for_running_time_entries_that_are_longer_than_the_threshold_but_already_received_the_email(): void | ||
{ | ||
// Arrange | ||
$user = $this->createUserWithPermission(); | ||
$timeEntry = TimeEntry::factory()->forMember($user->member)->create([ | ||
'start' => Carbon::now()->subHours(8)->subMinute(), | ||
'end' => null, | ||
'still_active_email_sent_at' => Carbon::now()->subMinute(), | ||
]); | ||
|
||
// Act | ||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails'); | ||
|
||
// Assert | ||
Mail::assertNothingOutgoing(); | ||
$timeEntry->refresh(); | ||
$this->assertNotNull($timeEntry->still_active_email_sent_at); | ||
$this->assertSame(Command::SUCCESS, $exitCode); | ||
$output = Artisan::output(); | ||
$this->assertSame("Sending still running time entry emails...\n". | ||
"Finished sending 0 still running time entry emails...\n", $output); | ||
} | ||
|
||
public function test_dry_run_option_does_not_send_mails_but_outputs_what_would_happen(): void | ||
{ | ||
// Arrange | ||
$user = $this->createUserWithPermission(); | ||
$timeEntryRunningLongerThanThreshold = TimeEntry::factory()->forMember($user->member)->create([ | ||
'start' => Carbon::now()->subHours(8)->subSecond(), | ||
'end' => null, | ||
]); | ||
|
||
// Act | ||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails --dry-run'); | ||
|
||
// Assert | ||
Mail::assertNothingOutgoing(); | ||
$timeEntryRunningLongerThanThreshold->refresh(); | ||
$this->assertNull($timeEntryRunningLongerThanThreshold->still_active_email_sent_at); | ||
$this->assertSame(Command::SUCCESS, $exitCode); | ||
$output = Artisan::output(); | ||
$this->assertSame("Sending still running time entry emails...\n". | ||
"Running in dry-run mode. No emails will be sent and nothing will be saved to the database.\n". | ||
'Start sending email to user "'.$user->user->email.'" ('.$user->user->getKey().') for time entry '.$timeEntryRunningLongerThanThreshold->getKey()."\n". | ||
"Finished sending 1 still running time entry emails...\n", $output); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Tests\Unit\Mail; | ||
|
||
use App\Mail\TimeEntryStillRunningMail; | ||
use App\Models\TimeEntry; | ||
use PHPUnit\Framework\Attributes\CoversClass; | ||
use PHPUnit\Framework\Attributes\UsesClass; | ||
use Tests\TestCaseWithDatabase; | ||
|
||
#[CoversClass(TimeEntryStillRunningMail::class)] | ||
#[UsesClass(TimeEntryStillRunningMail::class)] | ||
class TimeEntryStillRunningMailTest extends TestCaseWithDatabase | ||
{ | ||
public function test_mail_renders_content_correctly(): void | ||
{ | ||
// Arrange | ||
$user = $this->createUserWithPermission(); | ||
$timeEntry = TimeEntry::factory()->create([ | ||
'description' => 'TEST 123', | ||
]); | ||
$mail = new TimeEntryStillRunningMail($timeEntry, $user->user); | ||
|
||
// Act | ||
$rendered = $mail->render(); | ||
|
||
// Assert | ||
$this->assertStringContainsString('Your currently running time entry "TEST 123"', $rendered); | ||
} | ||
} |