cart->currentOrCreate()->setShippingAddress($address); Event::dispatch(new ShippingAddressSet($cart, $address)); return $cart; } public function setBillingAddress(array|Addressable $address): Cart { $cart = $this->cart->currentOrCreate()->setBillingAddress($address); Event::dispatch(new BillingAddressSet($cart, $address)); return $cart; } /** * Every shipping option currently available for the cart — already * fully backed by the merged Shipping-Carriers work: this runs every * registered Lunar\Shipping\Interfaces\ShippingRateInterface driver * (ACS/Box Now live-rate quoting alongside table-rate-shipping's own * flat-rate/free-shipping/collection drivers) through * ShippingManifest's pipeline. No rate-resolution logic lives here — * this is a thin pass-through. * * @return Collection */ public function getShippingOptions(): Collection { return ShippingManifest::getOptions($this->cart->currentOrCreate()); } /** * @throws InvalidShippingOptionException if $identifier doesn't resolve * to a real, currently-available option for the cart */ public function selectShippingOption(string $identifier): Cart { $cartBefore = $this->cart->currentOrCreate(); $option = ShippingManifest::getOption($cartBefore, $identifier); if ($option === null) { throw new InvalidShippingOptionException($identifier); } $cart = $cartBefore->setShippingOption($option); Event::dispatch(new ShippingOptionSelected($cart, $option)); return $cart; } /** * Every payment method currently offered to the storefront, ordered by * Modules\Core\Payment\Models\PaymentMethod::position — a row is * offered only when ALL three checks pass, each meaning something * different to an admin diagnosing why a method isn't showing up (see * docs/payments.md): * 1. `enabled` — an admin turned it on. * 2. its `driver` still resolves via PaymentDriverRegistry — the * driver class hasn't been removed (see the `payment:sync-drivers` * command, which sets `driver_missing_at` when this fails; a row * with that set is excluded here regardless of `enabled`, so a * vanished driver can never silently look "available"). * 3. the resolved driver reports Configurable::isConfigured() — its * own runtime requirements (e.g. an API key) are met. * * @return Collection */ public function getPaymentMethods(): Collection { return $this->paymentMethods->all() ->filter(fn (PaymentMethod $method) => $method->enabled && $method->driver_missing_at === null) ->filter(fn (PaymentMethod $method) => $this->paymentDrivers->resolve($method->driver)?->isConfigured() ?? false) ->values(); } /** * Records which payment type the shopper picked (Cart::meta * ['payment_method']) — read by e.g. Modules\Core\Payment\Pipelines\ * Cart\ApplyCashOnDeliveryFee to add that type's own cart-total * adjustments before recalculation. * * Also snapshots Cart::fingerprint() into meta, *after* saving the * chosen type — the fingerprint has to reflect the final total * including any payment-type-specific adjustment (e.g. a COD * surcharge), which only exists once payment_method is set and the * cart recalculates. Captured here, server-side, rather than asked of * the storefront: this is the last moment before initiatePayment() that * the shopper's reviewed total is known, and initiatePayment() reads it * back internally instead of taking a fingerprint parameter — a * storefront should never need to know Cart::fingerprint() exists. * * Does not itself call a payment driver — selecting a method and * initiating payment against it are deliberately separate steps, same * as selecting a shipping option happens before placing the order. * * @throws UnknownPaymentTypeException if $type isn't currently offered * — see getPaymentMethods() for what that means */ public function selectPaymentMethod(string $type): Cart { if (! $this->getPaymentMethods()->contains('type', $type)) { throw new UnknownPaymentTypeException($type); } $cart = $this->cart->currentOrCreate(); $cart->meta = [...($cart->meta?->toArray() ?? []), 'payment_method' => $type]; $cart->save(); $cart = $cart->calculate(); $cart->meta = [...($cart->meta?->toArray() ?? []), 'checkout_fingerprint' => $cart->fingerprint()]; $cart->save(); Event::dispatch(new PaymentMethodSelected($cart, $type)); return $cart; } /** * The one storefront-facing "place this order and pay for it" call — * the point where Checkout hands off to Payment. Ensures a draft * Order exists (Cart::createOrder() — confirmed idempotent against a * cart's own pre-existing, not-yet-placed-at draft; see * vendor/lunarphp/core/src/Actions/Carts/CreateOrder.php), then * resolves the payment method selected by selectPaymentMethod() and * calls pay() or authorize() on its driver, per that method's own * `capture_mode` column. * * Returns the driver's own PaymentResult UNCHANGED — this method does * not wait for or resolve anything past what pay()/authorize() itself * returns synchronously. A Pending result (an async gateway like * Stripe requiring 3-D Secure/a redirect) is a normal, expected * outcome, not an error — the caller (a storefront controller) is * responsible for whatever the gateway needs next. * * The draft order's own $order->total (not the Cart's) is what gets * passed as $amount — Order::$total is Lunar's own Price-cast * attribute, already resolving the correct Currency via the order's * own currency_code, and is the authoritative total once the draft * row exists. * * $context passed to the driver is {cart_id, order_id} — the exact * keys Modules\Core\Payment\Drivers\StripePaymentDriver:: * rememberIntent() already reads. * * Same fingerprint precondition the old placeOrder() had: mandatory, * not optional, checked before the draft is created. * * @param array $data passed through untouched to * the driver's pay()/authorize() — e.g. Stripe's payment_method * token. * * @throws UnknownPaymentTypeException if the cart's selected * payment_method (from selectPaymentMethod()) is no longer offered * — re-checked here, not just at selection time, since a method * could be disabled (or its driver removed) in between * @throws FingerprintMismatchException * @throws CartException */ public function initiatePayment(string $fingerprint, array $data = []): PaymentResult { $cart = $this->cart->currentOrCreate(); $cart->checkFingerprint($fingerprint); $type = $cart->meta['payment_method'] ?? null; $method = $type !== null ? $this->getPaymentMethods()->firstWhere('type', $type) : null; if ($method === null) { throw new UnknownPaymentTypeException((string) $type); } $order = $cart->createOrder(); $driver = $this->paymentDrivers->resolve($method->driver); $context = ['cart_id' => $cart->id, 'order_id' => $order->id]; return $method->capture_mode === 'authorize' ? $driver->authorize($type, $order->total, $data, $context) : $driver->pay($type, $order->total, $data, $context); } }