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; } /** * $fingerprint is mandatory, not optional — the caller must prove the * cart total the shopper last saw (Cart::fingerprint()) still matches * before an order is placed. Cart::checkFingerprint() throws Lunar's own * FingerprintMismatchException on a mismatch (a line's price changed, * stock adjusted the total, another tab modified the cart) rather than * silently placing an order at a different total than what was shown. * * Not called directly by a storefront — see confirmPayment(), which is * the only caller and supplies the fingerprint captured in * selectPaymentMethod(), not one the storefront has to obtain itself. * * No exception wrapping: Lunar\Validation\Cart\ValidateCartForOrderCreation * (run inside Cart::createOrder()) already throws * Lunar\Exceptions\Carts\CartException with a field-keyed MessageBag * ($exception->errors()) for address/shipping-option validation and the * duplicate-order guard — already the right shape for a storefront to * render as form errors directly. FingerprintMismatchException * propagates the same way, for the same reason. * * @throws FingerprintMismatchException * @throws CartException */ public function placeOrder(string $fingerprint): Order { $cart = $this->cart->currentOrCreate(); $cart->checkFingerprint($fingerprint); $order = $cart->createOrder(); Event::dispatch(new OrderPlaced($order)); return $order; } /** * Every payment type currently offered to the storefront — every key * in config('lunar.payments.types') that is BOTH administratively * enabled (Modules\Core\Payment\Models\PaymentMethod::enabled) AND * whose registered PaymentDriver reports itself usable right now * (PaymentDriver::isConfigured() — e.g. Stripe with no API key set is * never offered, regardless of the enabled toggle). A type with no * PaymentMethod row at all (never seeded) is treated as not offered, * same as disabled — nothing here creates one; see * InstallLunarCommand::seedPaymentMethods(). * * @return array */ public function getPaymentMethods(): array { return PaymentMethod::where('enabled', true) ->pluck('type') ->filter(fn (string $type) => $this->resolvePaymentDriver($type)?->isConfigured() ?? false) ->values() ->all(); } /** * 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 confirmPayment() that * the shopper's reviewed total is known, and confirmPayment() 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 PaymentDriver — selecting a method and * confirming 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 (registered, * administratively enabled, and its driver reports itself usable) */ public function selectPaymentMethod(string $type): Cart { if (! in_array($type, $this->getPaymentMethods(), true)) { 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; } /** * Resolves $type's registered PaymentDriver and calls confirm() — * the driver decides whether/when the order actually gets placed (see * Modules\Core\Checkout\Contracts\PaymentDriver's docblock). $data * carries whatever that driver needs (Stripe's payment_intent id, a * future redirect-based provider's callback payload). * * The fingerprint passed to the driver is the one captured by * selectPaymentMethod(), not supplied by the caller — see that * method's docblock. Throws the same FingerprintMismatchException a * caller-supplied one would if the cart's total has since changed; * missing entirely (selectPaymentMethod() was never called for this * cart) is treated the same as a mismatch, not a different error. * * @param array $data * * @throws UnknownPaymentTypeException if $type isn't currently offered * (see getPaymentMethods()) — re-checked here, not just in * selectPaymentMethod(), since a type could be disabled between * selection and confirmation * @throws \Lunar\Exceptions\FingerprintMismatchException * @throws \Lunar\Exceptions\Carts\CartException */ public function confirmPayment(string $type, array $data = []): Order { if (! in_array($type, $this->getPaymentMethods(), true)) { throw new UnknownPaymentTypeException($type); } $cart = $this->cart->currentOrCreate(); $fingerprint = $cart->meta['checkout_fingerprint'] ?? ''; return $this->resolvePaymentDriver($type)->confirm($cart, $type, $fingerprint, $data); } /** * Resolves $type's registered PaymentDriver, or null if $type has no * 'payment_driver' registered in config('lunar.payments.types.') * at all — deliberately non-throwing so getPaymentMethods() can filter * unresolvable types silently rather than treating "not registered" * as an error condition when just checking availability. */ private function resolvePaymentDriver(string $type): ?PaymentDriver { $driverClass = config("lunar.payments.types.{$type}.payment_driver"); return $driverClass ? app($driverClass) : null; } }