generated from boboko/starter
129 lines
4.4 KiB
PHP
129 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Checkout;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
use Lunar\Exceptions\Carts\CartException;
|
|
use Lunar\Models\CartLine;
|
|
use Lunar\Models\ProductVariant;
|
|
use Modules\Core\Cart\Exceptions\InvalidCouponException;
|
|
use Modules\Core\Cart\Services\CartService;
|
|
|
|
/**
|
|
* Thin storefront cart endpoints for the checkout module. Every action mutates
|
|
* the session cart via boboko-core's CartService and returns the same
|
|
* server-rendered `cart-body` partial — the drawer's Stimulus controller swaps
|
|
* that fragment in place (no JSON, no client-side templating). $cart / $lines
|
|
* for the partial come from the view composer in CheckoutModuleServiceProvider.
|
|
*/
|
|
class CartController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly CartService $cart,
|
|
) {}
|
|
|
|
/**
|
|
* CartException here is Lunar's own add_to_cart validation pipeline
|
|
* (CartLineQuantity/CartLineStock) rejecting the line — most commonly
|
|
* "not enough stock at this quantity" for a tracked (purchasable =
|
|
* in_stock) variant. Its own message is an untranslated, hardcoded
|
|
* English string not meant for storefront display, so this returns our
|
|
* own translated one instead rather than passing it through — a
|
|
* storefront.* key rather than checkout.*, since this is a catalog/stock
|
|
* concern the storefront owns, not something specific to the portable
|
|
* checkout module.
|
|
*/
|
|
public function add(string $locale, Request $request): View|JsonResponse
|
|
{
|
|
$data = $request->validate([
|
|
'purchasable_id' => ['required', 'integer'],
|
|
'quantity' => ['nullable', 'integer', 'min:1'],
|
|
]);
|
|
|
|
$variant = ProductVariant::findOrFail($data['purchasable_id']);
|
|
|
|
try {
|
|
$this->cart->addLine($variant, $data['quantity'] ?? 1);
|
|
} catch (CartException) {
|
|
return $this->stockError($variant);
|
|
}
|
|
|
|
return view('checkout::partials.cart-body');
|
|
}
|
|
|
|
public function updateLine(string $locale, Request $request, int $line): View|JsonResponse
|
|
{
|
|
$quantity = (int) $request->validate([
|
|
'quantity' => ['required', 'integer', 'min:0'],
|
|
])['quantity'];
|
|
|
|
try {
|
|
$quantity === 0
|
|
? $this->cart->removeLine($line)
|
|
: $this->cart->updateLine($line, $quantity);
|
|
} catch (CartException) {
|
|
$variant = CartLine::find($line)?->purchasable;
|
|
|
|
return $this->stockError($variant instanceof ProductVariant ? $variant : null);
|
|
}
|
|
|
|
return view('checkout::partials.cart-body');
|
|
}
|
|
|
|
/**
|
|
* getTotalInventory() is the same number canBeFulfilledAtQuantity()
|
|
* checked against (stock, for a tracked in_stock variant) — telling the
|
|
* shopper how many are actually left beats a generic "not enough stock"
|
|
* they'd otherwise have to guess around by trial and error.
|
|
*/
|
|
private function stockError(?ProductVariant $variant): JsonResponse
|
|
{
|
|
$available = $variant?->getTotalInventory() ?? 0;
|
|
|
|
return response()->json([
|
|
'error' => trans_choice('storefront.product.add_to_cart_failed', $available, ['count' => $available]),
|
|
], 422);
|
|
}
|
|
|
|
public function remove(string $locale, int $line): View
|
|
{
|
|
$this->cart->removeLine($line);
|
|
|
|
return view('checkout::partials.cart-body');
|
|
}
|
|
|
|
/**
|
|
* A bad code is a normal, expected outcome here (typo, expired code), not
|
|
* an error state for the request — it re-renders the same cart-body
|
|
* partial with $couponError set, rather than a 4xx/redirect, so the fetch
|
|
* + swap in bbk-cart-controller stays the one code path for every cart
|
|
* mutation.
|
|
*/
|
|
public function applyCoupon(string $locale, Request $request): View
|
|
{
|
|
$code = $request->validate([
|
|
'code' => ['required', 'string'],
|
|
])['code'];
|
|
|
|
$couponError = false;
|
|
|
|
try {
|
|
$this->cart->applyCoupon($code);
|
|
} catch (InvalidCouponException) {
|
|
$couponError = true;
|
|
}
|
|
|
|
return view('checkout::partials.cart-body', ['couponError' => $couponError]);
|
|
}
|
|
|
|
public function removeCoupon(string $locale): View
|
|
{
|
|
$this->cart->removeCoupon();
|
|
|
|
return view('checkout::partials.cart-body');
|
|
}
|
|
}
|