From 1bcc6acd2775ad0e36d56ea95e9ab23a759bc904 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Tue, 29 Sep 2026 01:01:39 +0300 Subject: [PATCH] Fix: Guarding against order status, fixing store details render in email --- .../OrderStatusUpdatedNotification.php | 22 +++++++++++++++++++ src/Store/Services/StoreDetailsService.php | 16 +++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Order/Notifications/OrderStatusUpdatedNotification.php b/src/Order/Notifications/OrderStatusUpdatedNotification.php index d51a85d..c89a2a4 100644 --- a/src/Order/Notifications/OrderStatusUpdatedNotification.php +++ b/src/Order/Notifications/OrderStatusUpdatedNotification.php @@ -7,6 +7,7 @@ use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Facades\Notification as NotificationFacade; use Modules\Core\Notification\BaseNotification; use Modules\Core\Order\Events\OrderStatusUpdated; +use Modules\Core\Order\Services\OrderStatusFlow; use Modules\Core\Order\Support\OrderReferenceDisplay; class OrderStatusUpdatedNotification extends BaseNotification @@ -30,6 +31,22 @@ class OrderStatusUpdatedNotification extends BaseNotification * NotificationRegistry, so without this the customer would get two * emails for that one transition. Returning no channels is the * standard Laravel way to suppress a notification outright. + * + * Also suppressed for the order's very first transition off + * 'awaiting_payment' — for every payment method except bank transfer, + * that transition happens in the SAME request as checkout itself + * (Modules\Core\Order\Services\OrderPaymentResolutionService:: + * resolveCaptureOrAuthorization()/resolveDeferredPayment(), both + * called synchronously alongside Checkout\Events\OrderPlaced), so it's + * not new information — the shopper was already told their order is + * confirmed by Modules\Core\Order\Notifications\ + * OrderPlacedNotification moments earlier. A bank transfer order is + * the one genuine exception: it sits at 'awaiting_payment' until + * staff separately mark it paid (Modules\Core\Order\Services\ + * OrderFulfillmentService::markPaid()), a real, later event the + * shopper does need to hear about — see OrderStatusFlow's own + * docblock for why bank transfer is the only payment method where + * placement and payment aren't the same moment. */ public function via(object $notifiable): array { @@ -37,6 +54,11 @@ class OrderStatusUpdatedNotification extends BaseNotification return []; } + if ($this->event->previousStatus === 'awaiting_payment' + && ! app(OrderStatusFlow::class)->isBankTransfer($this->event->order)) { + return []; + } + return ['mail']; } diff --git a/src/Store/Services/StoreDetailsService.php b/src/Store/Services/StoreDetailsService.php index 322fdb1..90c9931 100644 --- a/src/Store/Services/StoreDetailsService.php +++ b/src/Store/Services/StoreDetailsService.php @@ -2,6 +2,7 @@ namespace Modules\Core\Store\Services; +use Filament\Forms\Components\RichEditor\RichContentRenderer; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Event; use Lunar\Models\Language; @@ -45,6 +46,13 @@ class StoreDetailsService * arrived). Null also when the store hasn't filled the field in for * $locale, so the caller's own @if($bankTransferInstructions) guard * covers both cases identically. + * + * bank_transfer_instructions is a TranslatedRichEditor field (see + * ManageStoreDetails), so translate() returns Filament's Tiptap JSON + * document structure for that locale, not a plain string — + * RichContentRenderer::make() is Filament's own converter from that + * structure to sanitized HTML (the same one the admin panel itself + * uses to render a RichEditor's content read-only). */ public function bankTransferInstructionsFor(Order $order, string $locale): ?string { @@ -52,7 +60,13 @@ class StoreDetailsService return null; } - return $this->current()->translate('bank_transfer_instructions', $locale) ?: null; + $content = $this->current()->translate('bank_transfer_instructions', $locale); + + if (blank($content)) { + return null; + } + + return RichContentRenderer::make($content)->toHtml(); } public function update(array $attributes): StoreDetails