85 lines
3.3 KiB
PHP
85 lines
3.3 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Cart\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Lunar\Models\Cart;
|
|
use Modules\Core\Cart\Filament\Resources\CartResource;
|
|
use Modules\Core\Recovery\Events\CartAbandoned;
|
|
use Modules\Core\Recovery\Events\CheckoutAbandoned;
|
|
|
|
/**
|
|
* "Abandoned" is a derived state (Cart::updated_at older than
|
|
* config('core.cart.abandoned_after')) — nothing transitions a cart into it
|
|
* via a normal Eloquent write, so there's no model-event hook to dispatch
|
|
* CartAbandoned/CheckoutAbandoned from directly. This command is the only
|
|
* place that moment gets detected; run it on a schedule (see docs/cart.md).
|
|
*
|
|
* Splits Cart::scopeActive()'s two branches into their own events —
|
|
* see CartAbandoned/CheckoutAbandoned's docblocks for why they're distinct,
|
|
* not one combined "abandoned" state: a cart with no order at all is a much
|
|
* weaker purchase-intent signal than one with a draft order that was never
|
|
* placed.
|
|
*
|
|
* Deliberately does NOT write anything to Cart/Order — dispatch only. An
|
|
* earlier version recorded an "already notified" marker on Cart::meta/
|
|
* Order::meta, but that write bumped updated_at as an Eloquent side effect,
|
|
* which un-staled the very cart being marked abandoned (the same field
|
|
* abandonment staleness is computed from) — see docs/cart.md's former
|
|
* "Known bug" note. Cart/Checkout must have no way of writing abandonment
|
|
* state at all; every cart still matching the query below refires its event
|
|
* on every run until Recovery (not yet built — see
|
|
* docs/recovery-strategies.md) owns its own dedup/tracking table.
|
|
*/
|
|
class DetectAbandonedCarts extends Command
|
|
{
|
|
protected $signature = 'boboko:cart:detect-abandoned';
|
|
|
|
protected $description = 'Dispatch CartAbandoned/CheckoutAbandoned for carts that just crossed the abandonment threshold.';
|
|
|
|
public function handle(): void
|
|
{
|
|
$cutoff = CartResource::abandonedCutoff();
|
|
|
|
$cartsAbandoned = 0;
|
|
$checkoutsAbandoned = 0;
|
|
|
|
Cart::query()
|
|
->whereDoesntHave('orders')
|
|
->where('updated_at', '<=', $cutoff)
|
|
->with('lines')
|
|
->chunkById(200, function ($carts) use (&$cartsAbandoned) {
|
|
foreach ($carts as $cart) {
|
|
if ($cart->lines->isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
Event::dispatch(new CartAbandoned($cart));
|
|
|
|
$cartsAbandoned++;
|
|
}
|
|
});
|
|
|
|
Cart::query()
|
|
->whereHas('orders', fn ($query) => $query->whereNull('placed_at'))
|
|
->where('updated_at', '<=', $cutoff)
|
|
->with(['orders' => fn ($query) => $query->whereNull('placed_at')])
|
|
->chunkById(200, function ($carts) use (&$checkoutsAbandoned) {
|
|
foreach ($carts as $cart) {
|
|
$order = $cart->orders->first();
|
|
|
|
if ($order === null) {
|
|
continue;
|
|
}
|
|
|
|
Event::dispatch(new CheckoutAbandoned($cart, $order));
|
|
|
|
$checkoutsAbandoned++;
|
|
}
|
|
});
|
|
|
|
$this->components->info("Dispatched CartAbandoned for {$cartsAbandoned} cart(s), CheckoutAbandoned for {$checkoutsAbandoned} checkout(s).");
|
|
}
|
|
}
|