Feature: Minor Updates to Order Shipping And Order Statuses

This commit is contained in:
2026-09-10 22:50:40 +03:00
parent 99e55902ac
commit 78bbd8390a
8 changed files with 207 additions and 5 deletions
@@ -0,0 +1,36 @@
<?php
namespace Modules\Core\Order\Listeners;
use Modules\Core\Order\Events\OrderDelivered;
/**
* Writes Order.status to 'completed' once a carrier confirms delivery —
* the terminal status a carrier-fulfilled order reaches on its own,
* without a human picking it from the dropdown, mirroring the store-pickup
* order's own terminal transition (Modules\Core\Shipping\Extensions\
* OrderViewExtension::markPickedUpAction()).
*
* Kept separate from Modules\Core\Order\Listeners\
* DeriveOrderDeliveredFromShipment, which only ever dispatches
* OrderDelivered — see that event's own docblock ("Order.status itself is
* left untouched here") for why deriving "was this delivered" and acting
* on it by writing status are deliberately two different listeners, same
* separation Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus
* already has from the Payment* events it reacts to.
*
* Guarded to only fire from 'dispatched' — a checkpoint arriving out of
* order, or against an order some other status flow has already moved
* past, shouldn't silently force it to 'completed'.
*/
class CompleteOrderOnDelivered
{
public function handle(OrderDelivered $event): void
{
if ($event->order->status !== 'dispatched') {
return;
}
$event->order->update(['status' => 'completed']);
}
}
@@ -0,0 +1,61 @@
<?php
namespace Modules\Core\Order\Notifications;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Notification as NotificationFacade;
use Modules\Core\Notification\BaseNotification;
use Modules\Core\Order\Events\OrderStatusUpdated;
/**
* "Your order is ready to collect" — fires on the same OrderStatusUpdated
* event Modules\Core\Order\Notifications\OrderStatusUpdatedNotification
* listens to, but only for the 'ready-for-pickup' transition; that other
* notification suppresses itself for this same transition (see its own
* via()) so a customer gets this richer, pickup-specific email instead of
* the generic "order updated" one, not both.
*/
class OrderPickupReadyNotification extends BaseNotification
{
public function __construct(private readonly OrderStatusUpdated $event) {}
public static function getKey(): string
{
return 'order.pickup_ready.customer.mail';
}
public static function listensTo(): string
{
return OrderStatusUpdated::class;
}
public function via(object $notifiable): array
{
if ($this->event->newStatus !== 'ready-for-pickup') {
return [];
}
return ['mail'];
}
public function notifiable(): AnonymousNotifiable
{
$order = $this->event->order;
$email = $order->billingAddress?->contact_email ?? $order->shippingAddress?->contact_email;
return NotificationFacade::route('mail', $email);
}
public function toMail(object $notifiable): MailMessage
{
$order = $this->event->order;
return (new MailMessage)
->subject(__('Your order :reference is ready for pickup', ['reference' => $order->reference]))
->view('core::order.notifications.pickup-ready', [
'reference' => $order->reference,
]);
}
}
@@ -22,8 +22,20 @@ class OrderStatusUpdatedNotification extends BaseNotification
return OrderStatusUpdated::class;
}
/**
* 'ready-for-pickup' has its own, richer notification
* (Modules\Core\Order\Notifications\OrderPickupReadyNotification) —
* both listen to the same OrderStatusUpdated event via
* 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.
*/
public function via(object $notifiable): array
{
if ($this->event->newStatus === 'ready-for-pickup') {
return [];
}
return ['mail'];
}