From 936c9473a7c00954c10a4b905d24bc317b063a34 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Fri, 13 Feb 2026 07:13:56 +0100 Subject: [PATCH] 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. --- app/Mail/AlerteStock.php | 43 +++++++++++ app/Repositories/Shop/OfferStocks.php | 45 ++++++++++- ...0000_insert_mail_template_alerte_stock.php | 75 +++++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 app/Mail/AlerteStock.php create mode 100644 database/migrations/shop/2026_02_13_120000_insert_mail_template_alerte_stock.php diff --git a/app/Mail/AlerteStock.php b/app/Mail/AlerteStock.php new file mode 100644 index 00000000..6ad87b50 --- /dev/null +++ b/app/Mail/AlerteStock.php @@ -0,0 +1,43 @@ +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, + ); + } +} diff --git a/app/Repositories/Shop/OfferStocks.php b/app/Repositories/Shop/OfferStocks.php index 245c8ccf..b786debd 100644 --- a/app/Repositories/Shop/OfferStocks.php +++ b/app/Repositories/Shop/OfferStocks.php @@ -2,19 +2,62 @@ namespace App\Repositories\Shop; +use App\Mail\AlerteStock; use App\Models\Shop\Offer; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; class OfferStocks { public static function decreaseStock($item) { $offer = Offers::get($item['offer_id']); + $previousStock = $offer->stock_current; $offer->stock_current = $offer->stock_current - $item['quantity']; if ($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) diff --git a/database/migrations/shop/2026_02_13_120000_insert_mail_template_alerte_stock.php b/database/migrations/shop/2026_02_13_120000_insert_mail_template_alerte_stock.php new file mode 100644 index 00000000..be67a914 --- /dev/null +++ b/database/migrations/shop/2026_02_13_120000_insert_mail_template_alerte_stock.php @@ -0,0 +1,75 @@ +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 '' + .'
' + .'' + .'' + .'' + .'
' + .'' + .'Jardin\'enVie
' + .'' + .'
' + .'' + .'' + .'' + .'' + .'
' + .'Alerte stock bas
' + .'

Le stock de l\'article {{article}} (offre n°{{offre}}) ' + .'a atteint le seuil d\'alerte.

' + .'

{{stock_restant}} unités restantes

' + .'

Seuil d\'alerte configuré : {{seuil}} unités

' + .'

Pensez à réapprovisionner cet article.

' + .'
' + .'
' + .'' + .'
' + .'Jardin\'enVie Artisan Semencier
429 route des chaux, 26500 Bourg les Valence - Drôme' + .'
' + .'
'; + } + + 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'; + } +};