@php /** * The cart page's line list and its order summary — ONE source of markup for both the full page * render and the AJAX repaint. * * Why this partial exists * ----------------------- * Two faults, one cause. `/cart` was rendered entirely server-side with no repaint path, so an * upsell added from the "goes well with your order" band posted successfully and changed nothing on * screen: the diner saw a stale line list and a stale total until they reloaded. And the summary * derived its own figures from `restaurants.delivery_fee` instead of the resolver that had already * decided them, so the free-delivery meter directly above could promise free delivery while the row * beneath still charged — and a QR diner sitting at a table was quoted a delivery fee. * * The fix for both is the same and is the rule the storefront already follows elsewhere: the SERVER * renders, the client swaps the result in. `qm-upsell.js` already works this way and says why — * "the cart drawer already lost its delivery-fee row once to a second copy of markup living in * JavaScript". This partial is what stops a third copy being written. * * Every money figure comes from CartController::totals(), which resolves the delivery zone, honours * Restaurant::deliversNow() and computes tax. Nothing here recomputes any of it. * * @var array $items * @var float $subtotal * @var array|null $restaurant * @var array $totals CartController::totals() */ $deliversNow = $restaurant && \App\Models\Restaurant::deliversNow($restaurant); $feeShown = (float) ($totals['delivery_fee'] ?? 0); $taxShown = (float) ($totals['tax'] ?? 0); $grandTotal = (float) ($totals['total'] ?? $subtotal); @endphp
@if ($restaurant)
{!! restaurant_logo_html($restaurant, 'md') !!}
{{ $restaurant['name'] }}
{{ $restaurant['city'] ?? '' }}
{{ t_raw('cart.add_more') }}

@endif
@foreach ($items as $key => $line) @php $qty = (int) $line['qty']; @endphp @endforeach
{{ t_raw('cart.table_caption') }}
{{ t_raw('cart.col_image') }} {{ t_raw('cart.col_item') }} {{ t_raw('common.quantity_aria') }} {{ t_raw('cart.col_line_total') }} {{ t_raw('common.actions') }}
{{ $line['name'] }}
@if (!empty($line['option']))
{{ $line['option'] }}
@endif @if (!empty($line['ingredient_label']))
{{ $line['ingredient_label'] }}
@endif @php /* The line note belongs here for the same reason the option and the ingredient label do: it is part of what the diner asked for, and Cart::add() folds it into the line KEY, so two otherwise identical dishes with different notes are two separate rows. Left unrendered they were byte-identical on screen and the diner could not tell which note they were about to delete. Quoted exactly as OrderService::lineOptionsText() quotes it on the order itself. */ @endphp @if (trim((string) ($line['notes'] ?? '')) !== '')
“{{ trim((string) $line['notes']) }}”
@endif
{{ t_raw('cart.unit_each', [':amount' => money($line['unit_price'])]) }}
@csrf {{ t_raw('cart.quantity_value', [':n' => $qty]) }}
{{ money($line['unit_price'] * $line['qty']) }}
@csrf

{{ t_raw('cart.summary') }}

@php /* Above the numbers, because it is an invitation to change them rather than a line of the bill. Its terms now come from the SAME totals() call that set the fee below, so the bar can never promise free delivery while the row beneath still charges. */ if ($restaurant && $deliversNow) { echo partial('free-delivery-meter', [ 'threshold' => $totals['free_delivery_threshold'] ?? 0, 'remaining' => $totals['free_delivery_remaining'] ?? null, 'applied' => !empty($totals['free_delivery_applied']), ]); } @endphp
{{ t_raw('cart.subtotal') }}{{ money($subtotal) }}
@php /* Only when this restaurant actually delivers, and only at the RESOLVED fee — a waived one is legitimately 0 and is shown as such rather than as the raw column value. A pickup-only venue shows no row at all. */ @endphp @if ($restaurant && $deliversNow)
{{ t_raw('checkout.delivery_fee') }}{{ money($feeShown) }}
@endif @if ($taxShown > 0)
{{ $totals['tax_label'] ?? t_raw('checkout.tax') }}{{ money($taxShown) }}
@endif
{{ t_raw('checkout.total') }}{{ money($grandTotal) }}
@if ($restaurant && \App\Services\Tax::isEnabled($restaurant))

{!! \App\Services\Tax::isInclusive($restaurant) ? t('cart.tax_note_inclusive', [':label' => \App\Services\Tax::label($restaurant)]) : t('cart.tax_note_exclusive', [':label' => \App\Services\Tax::label($restaurant)]) !!}

@endif
@php /* The minimum is a DELIVERY / PICKUP rule. CheckoutController::place() states the scope itself — "a dine-in guest ordering a single item at the table must never be blocked by a delivery minimum" — and checkout.js mirrors it. This page did not, so the cart was the one surface that still stopped that diner: a QR guest one item below the minimum found the Checkout button disabled for an order the server would have taken. The gate is the same one the checkout page uses to OFFER dine-in at all: the cart is bound to a scanned table and this restaurant has QR ordering on. */ $dineIn = $restaurant && \App\Services\Cart::tableId() > 0 && (int) ($restaurant['qr_ordering_enabled'] ?? 1) === 1; $belowMin = $restaurant && !$dineIn && $subtotal < (float) $restaurant['min_order']; // Ordering is a customer / guest activity — a signed-in non-customer // (vendor/staff/admin/driver) is told to use a customer account (matches // the server-side guard in CheckoutController). $nonCustomer = \App\Support\Auth::check() && \App\Support\Auth::role() !== 'customer'; @endphp @if ($nonCustomer) {{ t_raw('cart.checkout') }} @elseif ($belowMin) {{ t_raw('cart.checkout') }} @else {{ t_raw('cart.checkout') }} @endif