What charge.succeeded does, for shops that get no webhooks (local dev, a
broken endpoint): the order remembers its charge id and the charge's balance
transactions reach the ledger. Idempotent; never fails the confirmation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- 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>
Shipping rates, promotion codes and tax chosen on the Stripe Checkout page
only exist on the session. checkout.session.completed now copies
amount_subtotal, total_details.amount_shipping/amount_discount/amount_tax and
amount_total onto the (not yet paid) order before recording the payment, so
the order's totals match what was charged and the order is settled. The
chosen shipping rate id goes to meta.stripe_shipping_rate. The shipping
address is also read from collected_information.shipping_details (Stripe API
2025-03-31+), falling back from shipping_details.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Webhook handlers stored euros in cent columns (charge.succeeded,
charge.refunded, refund.created, payment_intent.succeeded, the success
redirect, dispute/invoice notes). All amounts are cents now; purchases get
their own line amount as amount_paid.
- One Stripe refund arrives as refund.created AND charge.refunded, in either
order. New Order::applyStripeRefund() / applyStripeRefundedTotal() raise the
order's Stripe refund total (meta.stripe_refunded, meta.stripe_refunds) to
what Stripe confirmed, so nothing is counted twice. refund.created now finds
Checkout orders by payment_intent.
- New ShopService::refundOrder($order, $cents, $reason) refunds through the
Stripe API (idempotency key per order/amount/state) and records it, plus
refundableAmount(). Bind \Stripe\StripeClient to swap the client.
- charge.succeeded remembers meta.stripe_charge_id; with
shop.ledger.sync_on_webhook (default off) charge/refund/dispute webhooks
upsert the balance transactions into stripe_transactions.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
OrderPaid was defined but never dispatched. Order::booted() now fires it from
the `updated` hook exactly when paid_at goes null -> value (recordPayment()
reaching amount_total, the Stripe checkout webhook, or a direct update). Later
saves of a paid order, partial payments and overpayments do not re-fire it, and
a host subclass configured via shop.models.order gets it with its own instance.
StripeWebhookController, ShopService and Cart::checkout used the base
Blax\Shop\Models\Order statically (createFromCart, find, where, scopes,
aggregates), so a host app's configured Order subclass — with its own boot
hooks — was bypassed on the real webhook path. They now resolve
config('shop.models.order') through a small orderModel() helper. Return types
are unchanged (subclass instances satisfy Order); Order::formatMoney stays a
pure static.
Tests: OrderPaidEventTest (once-only on recordPayment, partial payments,
direct paid_at update, host subclass) and StripeWebhookOrderModelTest
(handleCheckoutSessionCompleted builds the configured subclass and its boot
hook runs; webhook/ShopService/Cart finders return it). Suite: 1485 tests green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- lang/de and lang/pl for cart.php and auth.php (informal register), every
key of lang/en translated; README documents the `shop::` namespace and
the lang/vendor/shop/<locale>/ override path.
- StripeWebhookController::handleCheckoutSessionCompleted now persists the
session's customer_details as Order.billing_address and shipping_details
as Order.shipping_address (falls back to billing when Stripe sent no
shipping block). Snapshot shape: name, email, phone, line1, line2,
postal_code, city, state, country, uid (first tax id). Only empty columns
are filled; a session without either block leaves the order untouched.
Optional host-added customer_email/customer_name columns are filled when
present and empty. Accepts Stripe objects, stdClass and arrays.
- Controller formatted with pint (composer lint).
Tests: tests/Unit/Stripe/StripeWebhookAddressTest.php (5),
tests/Feature/ShopServiceProvider/TranslationsTest.php (4). Full suite
green (1477, 7 skipped).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>