100000000) throw new CartError('invalid_request'); if ($tip && !($menu['tips_enabled'] ?? false)) throw new CartError('tips_unavailable'); $products = array_column($menu['products'], null, 'id'); $total = $tip + ($fulfillment === 'delivery' ? ($menu['delivery_fee'] ?? 0) : 0); foreach ($items as $line) { $total += unit_price($products[$line['product_id']] ?? null, $line['modifiers'] ?? []) * $line['quantity']; } if ($checkoutDiscount !== null) { $amount = $checkoutDiscount['amount'] ?? null; $scope = $checkoutDiscount['product_ids'] ?? null; $ids = array_column($items, 'product_id'); if (!is_int($amount) || $amount < 1 || $amount > 100000000 || ($scope !== null && (!$scope || count(array_unique($scope)) !== count($scope) || array_diff($scope, $ids)))) throw new CartError('invalid_checkout_discount'); $eligible = 0; foreach ($items as $line) { if ($scope === null || in_array($line['product_id'], $scope, true)) $eligible += unit_price($products[$line['product_id']], $line['modifiers'] ?? [], true) * $line['quantity']; } if ($amount > $eligible) throw new CartError('invalid_checkout_discount'); $total -= $amount; } return $total; } function choice_key(string $groupId, string $optionId): string { return json_encode([$groupId, $optionId]); } function unit_price(?array $product, array $modifiers, bool $baseOnly = false): int { if ($product === null || !($product['available'] ?? true)) throw new CartError('product_unavailable'); $selected = []; foreach ($modifiers as $choice) $selected[choice_key($choice['group_id'], $choice['option_id'])] = true; if (count($selected) !== count($modifiers)) throw new CartError('duplicate_modifier'); $groups = $product['modifier_groups'] ?? []; $known = []; foreach ($groups as $group) { foreach ($group['options'] as $option) $known[choice_key($group['id'], $option['id'])] = true; } if (array_diff_key($selected, $known)) throw new CartError('unknown_modifier'); // A product with variants is priced by the one variant matching all "variant" choices. $deposit = $product['deposit'] ?? 0; $unit = $product['price'] - ($product['discount'] ?? 0); if (!empty($product['variants'])) { $definingGroups = []; foreach ($groups as $group) { if (($group['kind'] ?? 'extra') === 'variant') $definingGroups[$group['id']] = true; } $defining = []; foreach ($modifiers as $choice) { if (isset($definingGroups[$choice['group_id']])) $defining[choice_key($choice['group_id'], $choice['option_id'])] = true; } $variant = null; foreach ($product['variants'] as $candidate) { $selections = []; foreach ($candidate['selections'] as $s) $selections[choice_key($s['group_id'], $s['option_id'])] = true; if ($selections == $defining) { $variant = $candidate; break; } } if ($variant === null || !($variant['available'] ?? true)) throw new CartError('variant_unavailable'); $deposit = $variant['deposit'] ?? 0; $unit = $variant['price'] - ($variant['discount'] ?? 0); } if ($baseOnly) return $unit; $unit += $deposit + ($product['packaging'] ?? 0); // Every group enforces its bounds; selected options add their price and deposit. foreach ($groups as $group) { $chosen = array_filter($group['options'], fn ($option) => isset($selected[choice_key($group['id'], $option['id'])])); if (count($chosen) < ($group['minimum'] ?? 0) || count($chosen) > ($group['maximum'] ?? 1)) { throw new CartError('modifier_selection_invalid'); } foreach ($chosen as $option) { if (!($option['available'] ?? true)) throw new CartError('modifier_unavailable'); $unit += $option['price'] + ($option['deposit'] ?? 0); } } return $unit; }