feat: finish removing console kernel, add expiring invite notification

Signed-off-by: Miguel Nogueira <me@nogueira.codes>
This commit is contained in:
2025-08-09 20:38:50 +01:00
parent 9e77205820
commit 627c619abf
6 changed files with 304 additions and 74 deletions

View File

@@ -1,74 +0,0 @@
<?php
/*
* Copyright © 2020 Miguel Nogueira
*
* This file is part of Raspberry Staff Manager.
*
* Raspberry Staff Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Raspberry Staff Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Console;
use App\Jobs\ProcessDueSuspensions;
use App\Jobs\ProcessExpiredAbsences;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('vote:evaluate')
->daily();
// Production value: Every day
$schedule->job(new ProcessDueSuspensions)
->daily();
// Production value: Every day
// Development value: Every minute
$schedule->job(new ProcessExpiredAbsences)
->daily();
// Production value: Every day
// Development value: Every minute
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Jobs;
use App\Invitation;
use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;
use App\Mail\InviteExpiringSoon;
class ExpiredInviteCleanup implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
// 1. Notify invites expiring within the next 24 hours that haven't been notified yet
Invitation::where('status', 'pending')
->where('notified', false)
->whereBetween('expiration', [Carbon::now(), Carbon::now()->addDay()])
->chunkById(100, function ($invites) {
foreach ($invites as $invite) {
Mail::to($invite->requestor_email)
->send(new InviteExpiringSoon($invite));
$invite->notified = true;
$invite->save();
}
});
// 2. Delete invites that have actually expired
Invitation::where('status', 'pending')
->where('expiration', '<', Carbon::now())
->chunkById(100, function ($invites) {
foreach ($invites as $invite) {
$invite->delete();
}
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Jobs;
use App\Invitation;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
// General Housekeeping Job: Drop Approved and Completed Invites
class InviteLifecycleCleanup implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
Invitation::whereIn('status', ['approved', 'completed'])
->chunkById(100, function ($invites) {
foreach ($invites as $invite) {
$invite->delete();
}
});
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Mail;
use App\Invitation;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class InviteExpiringSoon extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public Invitation $invite;
/**
* Create a new message instance.
*/
public function __construct($invite)
{
$this->invite = $invite;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Your invitation to . ' . config('app.name') . ' is expiring soon',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'mail.invite-expiring',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -0,0 +1,150 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Invite request expiring soon - {{ config('app.name') }} </title>
<style>
/* -------------------------------------
INLINED WITH htmlemail.io/inline
------------------------------------- */
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Your invite request to {{ config('app.name') }} has is expiring in 24 hours</span>
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hello there! We're writing to let you know that your invite request to {{ config('app.name') }} is expiring in approximately 24 hours as of this email.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Be aware that, once your invite expires, you won't be able to activate it and therefore create a new account with it.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you want to receive another invite (it's OK, emails get lost all the time), you may request another one day after it expires (ex. if it expired on Saturday, you can request another on Sunday).</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you believe you received this message in error, you can ignore it safely; otherwise, feel free to unsubscribe at the bottom of this email.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Kind regards,</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">The {{ config('app.name') }} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;">Staff Manager</span>
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@@ -19,10 +19,22 @@
* along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>. * along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>.
*/ */
use App\Jobs\ExpiredInviteCleanup;
use App\Jobs\InviteLifecycleCleanup;
use App\Jobs\ProcessDueSuspensions;
use App\Jobs\ProcessExpiredAbsences;
use Illuminate\Foundation\Inspiring; use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () { Artisan::command('inspire', function () {
$this->comment(Inspiring::quote()); $this->comment(Inspiring::quote());
})->describe('Display an inspiring quote'); })->describe('Display an inspiring quote');
Schedule::command('votes:evaluate')->daily();
Schedule::job(new ProcessDueSuspensions())->daily();
Schedule::job(new ProcessExpiredAbsences())->daily();
Schedule::job(new InviteLifecycleCleanup())->daily();
Schedule::job(new ExpiredInviteCleanup())->daily();