16235369cb
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.
148 lines
5.3 KiB
PHP
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]
|
|
);
|
|
}
|
|
}
|