Feature: Creating Cart Service, Cart Events, Save For Later functionality, Adding Filament Views for viewing abandoned carts

This commit is contained in:
2026-08-28 13:14:47 +03:00
parent a8ddbb8056
commit f6ef0761d6
19 changed files with 935 additions and 23 deletions
+7 -5
View File
@@ -39,11 +39,13 @@ class CartResource extends Resource
}
/**
* Count only, not a fetch — no rows are loaded. Scoped to genuinely abandoned
* carts specifically (mirrors ListCarts::getTabs()'s "Abandoned" query, not
* "Ongoing"), since that's the number a staff member glancing at the sidebar
* actually wants: how many carts might need following up on, not the total
* including ones someone is actively shopping in right now.
* Count only, not a fetch — no rows are loaded. Combines BOTH abandoned
* states (`active()` already covers "no order at all" and "draft order,
* never placed" together — see ListCarts::getTabs()'s "Abandoned Cart" /
* "Abandoned Checkout" tabs for where they're split apart), not "Ongoing"
* — the badge is meant to answer "how many carts might need following up
* on," not the total including ones someone is actively shopping in right
* now.
*/
public static function getNavigationBadge(): ?string
{
@@ -17,19 +17,34 @@ class ListCarts extends ListRecords
* "Completed" instead means the cart has an order with `placed_at` set (a
* placed, not just drafted, order).
*
* "Ongoing" vs "Abandoned" both start from `Cart::scopeActive()` (not yet
* converted to an order) and split on `updated_at` against
* `CartResource::abandonedCutoff()` — Lunar has no time-based staleness signal
* of its own, so recent activity is the only thing distinguishing a cart
* someone is shopping in right now from one genuinely left behind.
* `Cart::scopeActive()` (not yet converted to an order) actually mixes two
* distinct states: no order started at all, vs. a draft order exists
* (`placed_at IS NULL`) but was never placed — checkout was started, not
* finished. That's a real difference in purchase intent (a cart with a
* draft order is a much stronger signal than one with no order at all) and
* in reachability (checkout usually captures an email even for a guest),
* so they get separate tabs rather than one combined "no order yet"
* bucket — same distinction Modules\Core\Recovery\Events\CartAbandoned /
* Modules\Core\Recovery\Events\CheckoutAbandoned draw.
*
* "Ongoing" vs the two abandoned tabs all split on `updated_at` against
* `CartResource::abandonedCutoff()` — Lunar has no time-based staleness
* signal of its own, so recent activity is the only thing distinguishing a
* cart someone is shopping in right now from one genuinely left behind.
*/
public function getTabs(): array
{
return [
'ongoing' => Tab::make('Ongoing')
->modifyQueryUsing(fn (Builder $query) => $query->active()->where('updated_at', '>', CartResource::abandonedCutoff())),
'abandoned' => Tab::make('Abandoned')
->modifyQueryUsing(fn (Builder $query) => $query->active()->where('updated_at', '<=', CartResource::abandonedCutoff())),
'abandoned_cart' => Tab::make('Abandoned Cart')
->modifyQueryUsing(fn (Builder $query) => $query
->whereDoesntHave('orders')
->where('updated_at', '<=', CartResource::abandonedCutoff())),
'abandoned_checkout' => Tab::make('Abandoned Checkout')
->modifyQueryUsing(fn (Builder $query) => $query
->whereHas('orders', fn (Builder $query) => $query->whereNull('placed_at'))
->where('updated_at', '<=', CartResource::abandonedCutoff())),
'completed' => Tab::make('Completed')
->modifyQueryUsing(fn (Builder $query) => $query->whereHas(
'orders',