Files
order/app/Services/CartService.php
T
Ty Clifford 16235369cb - Implemented the full Fat Bottom Grille storefront and staff dashboard.
Highlights include configurable menus, specials, cart/checkout, taxes, 
customer accounts with email 2FA, newsletters, analytics, refunds, 
PDF/CSV exports, Square/Mailgun adapters, and optional MySQL migration.

See README.md for setup and production configuration. Verified 
PHP/JavaScript syntax, responsive layouts, registration, 2FA, checkout, 
dashboard workflows, settings persistence, and PDF generation.
Real Square, Mailgun, and MySQL connections require credentials/server 
access. Square implementation follows the official Payments API and 
Refunds API.
2026-06-10 13:27:42 -04:00

148 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace FatBottom\Services;
use FatBottom\Core\Database;
final class CartService
{
public function __construct(private readonly Database $db)
{
}
public function current(?int $userId = null): array
{
$token = $_COOKIE['fatbottom_cart'] ?? '';
$cart = null;
if (is_string($token) && $token !== '') {
$cart = $this->db->fetch('SELECT * FROM carts WHERE token = ? AND status = ?', [$token, 'active']);
}
if ($cart === null) {
$token = bin2hex(random_bytes(24));
$this->db->execute(
'INSERT INTO carts (token, user_id, status) VALUES (?, ?, ?)',
[$token, $userId, 'active']
);
setcookie('fatbottom_cart', $token, [
'expires' => time() + 60 * 60 * 24 * 30,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax',
]);
$cart = $this->db->fetch('SELECT * FROM carts WHERE token = ?', [$token]);
} elseif ($userId !== null && empty($cart['user_id'])) {
$this->db->execute('UPDATE carts SET user_id = ? WHERE id = ?', [$userId, (int) $cart['id']]);
$cart['user_id'] = $userId;
}
$this->touch((int) $cart['id']);
return $cart;
}
public function add(int $menuItemId, int $quantity, string $notes, ?int $userId = null): bool
{
$item = $this->db->fetch('SELECT * FROM menu_items WHERE id = ? AND active = 1', [$menuItemId]);
if ($item === null) {
return false;
}
$special = $this->db->fetch(
"SELECT price_cents FROM specials
WHERE menu_item_id = ? AND active = 1 AND price_cents IS NOT NULL
AND (starts_at IS NULL OR starts_at <= CURRENT_TIMESTAMP)
AND (ends_at IS NULL OR ends_at >= CURRENT_TIMESTAMP)
ORDER BY price_cents ASC LIMIT 1",
[$menuItemId]
);
$unitPrice = $special !== null
? min((int) $item['price_cents'], (int) $special['price_cents'])
: (int) $item['price_cents'];
$cart = $this->current($userId);
$notes = trim(substr($notes, 0, 240));
$existing = $this->db->fetch(
'SELECT * FROM cart_items WHERE cart_id = ? AND menu_item_id = ? AND notes = ?',
[(int) $cart['id'], $menuItemId, $notes]
);
if ($existing) {
$this->db->execute(
'UPDATE cart_items SET quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[min(20, (int) $existing['quantity'] + max(1, $quantity)), (int) $existing['id']]
);
} else {
$this->db->execute(
'INSERT INTO cart_items (cart_id, menu_item_id, quantity, unit_price_cents, notes)
VALUES (?, ?, ?, ?, ?)',
[(int) $cart['id'], $menuItemId, min(20, max(1, $quantity)), $unitPrice, $notes]
);
}
$this->recalculate((int) $cart['id']);
return true;
}
public function update(array $quantities, ?int $userId = null): void
{
$cart = $this->current($userId);
foreach ($quantities as $itemId => $quantity) {
$itemId = (int) $itemId;
$quantity = (int) $quantity;
if ($quantity <= 0) {
$this->db->execute('DELETE FROM cart_items WHERE id = ? AND cart_id = ?', [$itemId, (int) $cart['id']]);
} else {
$this->db->execute(
'UPDATE cart_items SET quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND cart_id = ?',
[min(20, $quantity), $itemId, (int) $cart['id']]
);
}
}
$this->recalculate((int) $cart['id']);
}
public function remove(int $cartItemId, ?int $userId = null): void
{
$cart = $this->current($userId);
$this->db->execute('DELETE FROM cart_items WHERE id = ? AND cart_id = ?', [$cartItemId, (int) $cart['id']]);
$this->recalculate((int) $cart['id']);
}
public function details(?int $userId = null): array
{
$cart = $this->current($userId);
$items = $this->db->fetchAll(
'SELECT ci.*, mi.name, mi.description, mi.active
FROM cart_items ci
JOIN menu_items mi ON mi.id = ci.menu_item_id
WHERE ci.cart_id = ?
ORDER BY ci.id',
[(int) $cart['id']]
);
$cart['items'] = $items;
$cart['item_count'] = array_sum(array_map(static fn (array $item): int => (int) $item['quantity'], $items));
return $cart;
}
private function touch(int $cartId): void
{
$this->db->execute('UPDATE carts SET last_seen_at = CURRENT_TIMESTAMP WHERE id = ?', [$cartId]);
}
private function recalculate(int $cartId): void
{
$subtotal = (int) $this->db->fetch(
'SELECT COALESCE(SUM(quantity * unit_price_cents), 0) AS subtotal
FROM cart_items WHERE cart_id = ?',
[$cartId]
)['subtotal'];
$this->db->execute(
'UPDATE carts SET subtotal_cents = ?, last_seen_at = CURRENT_TIMESTAMP WHERE id = ?',
[$subtotal, $cartId]
);
}
}