new: send stock alert email when offer stock crosses threshold
When a purchase causes an offer's ``stock_current`` to drop to or below its ``minimum_ondemand`` threshold, an email is sent to ``commande@jardinenvie.com`` using an editable mail template (Spatie ``MailTemplate``). The check runs in ``OfferStocks::decreaseStock()`` after updating stock. Only threshold-crossing events trigger the alert (not every low-stock sale). Failures are caught and logged to avoid disrupting the order flow.
This commit is contained in:
43
app/Mail/AlerteStock.php
Normal file
43
app/Mail/AlerteStock.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\Core\Mail\MailTemplate;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Spatie\MailTemplates\TemplateMailable;
|
||||||
|
|
||||||
|
class AlerteStock extends TemplateMailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $subject;
|
||||||
|
|
||||||
|
public $article;
|
||||||
|
|
||||||
|
public $offre;
|
||||||
|
|
||||||
|
public $stock_restant;
|
||||||
|
|
||||||
|
public $seuil;
|
||||||
|
|
||||||
|
protected static $templateModelClass = MailTemplate::class;
|
||||||
|
|
||||||
|
public function __construct($offer)
|
||||||
|
{
|
||||||
|
$this->article = $offer->article->title ?? 'Article #'.$offer->article_id;
|
||||||
|
$this->offre = $offer->id;
|
||||||
|
$this->stock_restant = $offer->stock_current;
|
||||||
|
$this->seuil = $offer->minimum_ondemand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function envelope()
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
from: new Address('boutique@jardinenvie.com', 'Jardin\'en\'Vie'),
|
||||||
|
subject: $this->subject,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,19 +2,62 @@
|
|||||||
|
|
||||||
namespace App\Repositories\Shop;
|
namespace App\Repositories\Shop;
|
||||||
|
|
||||||
|
use App\Mail\AlerteStock;
|
||||||
use App\Models\Shop\Offer;
|
use App\Models\Shop\Offer;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
class OfferStocks
|
class OfferStocks
|
||||||
{
|
{
|
||||||
public static function decreaseStock($item)
|
public static function decreaseStock($item)
|
||||||
{
|
{
|
||||||
$offer = Offers::get($item['offer_id']);
|
$offer = Offers::get($item['offer_id']);
|
||||||
|
$previousStock = $offer->stock_current;
|
||||||
$offer->stock_current = $offer->stock_current - $item['quantity'];
|
$offer->stock_current = $offer->stock_current - $item['quantity'];
|
||||||
if ($offer->stock_current <= 0) {
|
if ($offer->stock_current <= 0) {
|
||||||
$offer->stock_current = 0;
|
$offer->stock_current = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $offer->save();
|
$saved = $offer->save();
|
||||||
|
|
||||||
|
if ($saved) {
|
||||||
|
self::checkStockAlert($offer, $previousStock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function checkStockAlert($offer, $previousStock)
|
||||||
|
{
|
||||||
|
$threshold = (float) $offer->minimum_ondemand;
|
||||||
|
if ($threshold <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$crossedThreshold = $previousStock > $threshold
|
||||||
|
&& $offer->stock_current <= $threshold;
|
||||||
|
|
||||||
|
if (! $crossedThreshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$offer->load('article');
|
||||||
|
Mail::to('commande@jardinenvie.com')
|
||||||
|
->send(new AlerteStock($offer));
|
||||||
|
Log::info('Stock alert email sent', [
|
||||||
|
'offer_id' => $offer->id,
|
||||||
|
'article' => $offer->article->name ?? $offer->article_id,
|
||||||
|
'stock_current' => $offer->stock_current,
|
||||||
|
'threshold' => $threshold,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Failed to send stock alert email', [
|
||||||
|
'offer_id' => $offer->id,
|
||||||
|
'stock_current' => $offer->stock_current,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getStockCurrent($id)
|
public static function getStockCurrent($id)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('mail_templates')->insert([
|
||||||
|
'mailable' => 'App\\Mail\\AlerteStock',
|
||||||
|
'subject' => json_encode(['fr' => '[Stock bas] {{article}} — {{stock_restant}} unités restantes'], JSON_UNESCAPED_UNICODE),
|
||||||
|
'html_template' => json_encode(['fr' => $this->getHtmlTemplate()], JSON_UNESCAPED_UNICODE),
|
||||||
|
'text_template' => json_encode(['fr' => $this->getTextTemplate()], JSON_UNESCAPED_UNICODE),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::table('mail_templates')
|
||||||
|
->where('mailable', 'App\\Mail\\AlerteStock')
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHtmlTemplate(): string
|
||||||
|
{
|
||||||
|
return '<table style="min-width: 100%;" width="100%" cellspacing="0" cellpadding="0">'
|
||||||
|
.'<tbody><tr><td>'
|
||||||
|
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||||
|
.'<tbody><tr>'
|
||||||
|
.'<td style="margin: 0px auto; padding: 32px 0px 24px 4px; vertical-align: top;" align="center">'
|
||||||
|
.'<a href="https://www.jardinenvie.com/" style="text-decoration: none; color: #000000;">'
|
||||||
|
.'<img alt="Jardin\'enVie" src="https://boutique.jardinenvie.com/img/logo.png" style="margin: 0px auto; display: block;" width="300" height="138" border="0" /></a></td>'
|
||||||
|
.'</tr></tbody></table>'
|
||||||
|
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||||
|
.'<tbody><tr><td style="margin: 0 auto; padding: 0px; vertical-align: top;">'
|
||||||
|
.'<table style="border-collapse: collapse; margin: 0 auto; width: 512px;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||||
|
.'<tbody>'
|
||||||
|
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 40px; color: #000000; font-size: 28px; padding: 20px 0px 10px 0px; font-weight: 800; text-align: center;">'
|
||||||
|
.'Alerte stock bas</td></tr>'
|
||||||
|
.'<tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; line-height: 28px; color: #000000; font-size: 16px; text-align: center; padding: 10px 0 30px 0;">'
|
||||||
|
.'<p>Le stock de l\'article <strong>{{article}}</strong> (offre n°{{offre}}) '
|
||||||
|
.'a atteint le seuil d\'alerte.</p>'
|
||||||
|
.'<p style="font-size: 24px; font-weight: bold; color: #c0392b; padding: 10px 0;">{{stock_restant}} unités restantes</p>'
|
||||||
|
.'<p>Seuil d\'alerte configuré : {{seuil}} unités</p>'
|
||||||
|
.'<p style="padding-top: 15px; color: #666;">Pensez à réapprovisionner cet article.</p>'
|
||||||
|
.'</td></tr>'
|
||||||
|
.'</tbody></table>'
|
||||||
|
.'</td></tr></tbody></table>'
|
||||||
|
.'<table style="border-collapse: collapse; background-color: #ffffff; margin: 0 auto; width: 600px; text-align: center;" cellspacing="0" cellpadding="0" border="0" align="center">'
|
||||||
|
.'<tbody><tr><td style="font-family: \'Nunito Sans\', Arial, sans-serif; font-size: 12px; font-weight: 600; line-height: 20px; padding: 16px; color: #999999;">'
|
||||||
|
.'Jardin\'enVie Artisan Semencier<br />429 route des chaux, 26500 Bourg les Valence - Drôme'
|
||||||
|
.'</td></tr></tbody></table>'
|
||||||
|
.'</td></tr></tbody></table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTextTemplate(): string
|
||||||
|
{
|
||||||
|
return "ALERTE STOCK BAS\n\n"
|
||||||
|
."Article : {{article}} (offre n°{{offre}})\n"
|
||||||
|
."Stock restant : {{stock_restant}} unités\n"
|
||||||
|
."Seuil d'alerte : {{seuil}} unités\n\n"
|
||||||
|
."Pensez à réapprovisionner cet article.\n\n"
|
||||||
|
."Jardin'enVie Artisan Semencier\n"
|
||||||
|
.'429 route des chaux, 26500 Bourg les Valence - Drôme';
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user