- checkoutSession() keeps pending purchases in step with the cart: a buyer
who cancels on Stripe and edits the cart pays for (and is invoiced) the
cart as it is, not the first attempt.
- checkout.session.completed is idempotent and locked per payment: a
redelivered event, a concurrent charge.succeeded or the success page
record the payment once. An unpaid (async) session creates the order but
records no payment until Stripe settles it.
- New StripeCheckoutConfirmation + WS cart.confirm {session_id}: the
success page settles a paid session through the webhook's code path, so
the order exists the moment the buyer is back.
- CartService::guest() never hands a converted cart back to its session.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds the reusable pieces a headless WS-first shop needs, so every consumer
(immohost, nirioci, future shops) shares one implementation instead of
re-inventing it in app code:
- CartService::adopt() — resolve + re-bind a guest cart to a stable connection
id (socket/session), so the cart follows a stateless client across reconnects.
- CartService::mergeGuestIntoUser() — actually honour shop.cart.merge_on_login,
which was dead config: re-parent guest items onto the user's cart at login
(preserves claimed stock and resolved prices; no re-add side effects).
- Cart facade passthrough checkoutSessionLink() for the {redirect} checkout shape.
- CartResource / CartItemResource: a stable, presentation-agnostic wire shape
(money in integer minor units + explicit currency; best-effort image).
- Blax\Shop\Websocket\Controllers\{Cart,Auth}Controller: thin, config-resolved,
translatable base WS controllers a host exposes via one-line subclass stubs.
laravel-websockets stays a soft (suggest) dependency; the Websocket dir is
excluded from coverage since its logic lives in the covered CartService.
- shop:: translation namespace (en) for the controller messages.
Tests: tests/Unit/Cart/CartAdoptMergeTest.php (7). Full suite green (1468).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>