Files
Ty Clifford 03348cad79 - Implemented the full order lifecycle, secure customer tracker, automatic acceptance emails, configurable status templates, and guest/account CRM management.
Key areas: 
[OrderStatusService.php](/Users/tyemeclifford/Documents/GH/fatbottomgrille/app/Services/OrderStatusService.php), 
[AdminController.php](/Users/tyemeclifford/Documents/GH/fatbottomgrille/app/Controllers/AdminController.php), 
and 
[Database.php](/Users/tyemeclifford/Documents/GH/fatbottomgrille/app/Core/Database.php).
SQLite migration applied successfully. PHP/JS syntax, integration 
workflows, rendered pages, database integrity, and tracker authorization 
all passed. Invalid tracker tokens correctly return 403.
2026-06-14 13:35:31 -04:00

121 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
namespace FatBottom\Services;
use FatBottom\Core\Database;
use RuntimeException;
final class OrderService
{
public function __construct(
private readonly Database $db,
private readonly PaymentGateway $payments,
private readonly CustomerService $customers
) {
}
public function totals(array $cart): array
{
$subtotal = (int) $cart['subtotal_cents'];
$taxRate = (int) $this->db->fetch(
'SELECT COALESCE(SUM(rate_basis_points), 0) AS total_rate FROM tax_rates WHERE active = 1'
)['total_rate'];
$tax = (int) round($subtotal * $taxRate / 10000);
return [
'subtotal_cents' => $subtotal,
'tax_rate_basis_points' => $taxRate,
'tax_cents' => $tax,
'total_cents' => $subtotal + $tax,
];
}
public function place(array $cart, array $customer, ?int $userId, string $sourceId): array
{
if (empty($cart['items'])) {
throw new RuntimeException('Your cart is empty.');
}
$totals = $this->totals($cart);
$orderNumber = 'FBG-' . date('ymd') . '-'
. strtoupper(substr(hash('sha256', (string) $cart['token']), 0, 6));
$payment = $this->payments->charge($sourceId, $totals['total_cents'], $orderNumber);
$pdo = $this->db->pdo();
$pdo->beginTransaction();
try {
$customerId = $this->customers->capture($customer, $userId);
$trackingToken = bin2hex(random_bytes(24));
$statement = $pdo->prepare(
'INSERT INTO orders
(order_number, user_id, customer_id, cart_id, tracking_token,
customer_name, customer_email, customer_phone,
order_type, status, payment_status, payment_provider, payment_reference,
subtotal_cents, tax_cents, total_cents, special_instructions, pickup_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
);
$statement->execute([
$orderNumber,
$userId,
$customerId,
(int) $cart['id'],
$trackingToken,
$customer['customer_name'],
$customer['customer_email'],
$customer['customer_phone'],
'pickup',
'pending',
'paid',
$payment['provider'],
$payment['reference'],
$totals['subtotal_cents'],
$totals['tax_cents'],
$totals['total_cents'],
$customer['special_instructions'],
$customer['pickup_time'] ?: null,
]);
$orderId = (int) $pdo->lastInsertId();
$itemStatement = $pdo->prepare(
'INSERT INTO order_items
(order_id, menu_item_id, item_name, quantity, unit_price_cents, line_total_cents, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)'
);
foreach ($cart['items'] as $item) {
$itemStatement->execute([
$orderId,
(int) $item['menu_item_id'],
$item['name'],
(int) $item['quantity'],
(int) $item['unit_price_cents'],
(int) $item['quantity'] * (int) $item['unit_price_cents'],
$item['notes'],
]);
}
$pdo->prepare(
'INSERT INTO order_events
(order_id, user_id, event_type, details, status, customer_visible)
VALUES (?, ?, ?, ?, ?, 1)'
)->execute([
$orderId,
$userId,
'order_placed',
'Online pickup order paid successfully and is awaiting staff acceptance.',
'pending',
]);
$pdo->prepare(
"UPDATE carts SET status = 'converted', converted_at = CURRENT_TIMESTAMP WHERE id = ?"
)->execute([(int) $cart['id']]);
$this->customers->refresh($customerId);
$pdo->commit();
} catch (\Throwable $error) {
$pdo->rollBack();
throw $error;
}
return $this->db->fetch('SELECT * FROM orders WHERE id = ?', [$orderId]) ?? [];
}
}