Skip to content

Potential bug in scopeActive and CartSessionManager login logic #2397

@vencious

Description

@vencious

I've encountered an issue where a merged cart can be returned during login, and scopeActive might leak carts from other users due to missing query grouping.

  • Lunar version: 1.2.1
  • Laravel Version: 12.48.1
  • PHP Version: 8.4.15
  • Database MySQL 8.4.2

Expected Behaviour:

When a user logs in (auth_policy=merge), the guest cart should be merged into the user's active cart and the session should resolve the unmerged "target" cart on subsequent requests/logins. Merged source carts (merged_id != null) should not be returned as the current cart.

Actual Behaviour:

After merging carts (a source cart gets merged_id set), subsequent login (without creating a new guest cart) can resolve the merged source cart as the "current" cart, resulting in missing lines/items in the frontend/session.

This seems to happen because:

  1. Some call sites resolve the user's cart using ->active()->first() without excluding merged carts (->unmerged()), so a merged source cart can be returned.
  2. Cart::scopeActive() uses whereDoesntHave()->orWhereHas() without grouping, which can cause the OR clause to escape other constraints (e.g. relationship constraints like user_id).

Steps To Reproduce:

  1. Ensure config:
    • lunar.cart.auth_policy = 'merge'
    • lunar.cart_session.auto_create = false
  2. Log in as a user and create/ensure a user cart exists (add an item).
  3. Log out (ensure cart is not deleted on logout; e.g. CartSession::forget(false)).
  4. As guest, add an item (creates a guest cart in session).
  5. Log in (merge happens; source cart gets merged_id pointing to target cart).
  6. Log out and log in again WITHOUT adding new guest items.
  7. Observe that the resolved "current" cart can be the merged source cart (merged_id != null), rather than the target unmerged cart.

Notes / Proposed Fix:

  • Ensure session/user cart resolution excludes merged carts, in CartSessionManager
$cartId = $user->carts()
    ->unmerged()
    ->active()
    ->latest('id')
    ->value('id');
  • Group OR conditions in Cart::scopeActive():
public function scopeActive(Builder $query): Builder
{
    return $query->where(function ($q) {
        $q->whereDoesntHave('orders')
          ->orWhereHas('orders', function ($sub) {
              $sub->whereNull('placed_at');
          });
    });
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions