- 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.
This commit is contained in:
Ty Clifford
2026-06-14 13:35:31 -04:00
parent 3417589a7c
commit 03348cad79
24 changed files with 1218 additions and 72 deletions
+45 -10
View File
@@ -11,8 +11,8 @@ use FatBottom\Core\Http;
use FatBottom\Core\Security;
use FatBottom\Core\View;
use FatBottom\Services\CartService;
use FatBottom\Services\Mailer;
use FatBottom\Services\OrderService;
use FatBottom\Services\OrderStatusService;
final class StoreController extends BaseController
{
@@ -22,7 +22,7 @@ final class StoreController extends BaseController
CartService $cart,
private readonly Database $db,
private readonly OrderService $orders,
private readonly Mailer $mailer
private readonly OrderStatusService $orderStatuses
) {
parent::__construct($config, $auth, $cart);
}
@@ -169,13 +169,11 @@ final class StoreController extends BaseController
$user ? (int) $user['id'] : null,
(string) ($_POST['square_token'] ?? '')
);
$this->mailer->send(
$input['customer_email'],
'We received order ' . $order['order_number'],
'<p>Thanks, ' . htmlspecialchars($input['customer_name'], ENT_QUOTES, 'UTF-8') . '.</p>'
. '<p>Your order <strong>' . htmlspecialchars((string) $order['order_number'], ENT_QUOTES, 'UTF-8')
. '</strong> has been paid and sent to the kitchen.</p>'
);
try {
$this->orderStatuses->sendStatusEmail($order, 'pending');
} catch (\Throwable) {
Http::flash('warning', 'Your order was placed, but the confirmation email could not be sent.');
}
setcookie('fatbottom_cart', '', ['expires' => time() - 3600, 'path' => '/']);
$_SESSION['last_order_id'] = (int) $order['id'];
Http::redirect('/order/complete');
@@ -198,6 +196,44 @@ final class StoreController extends BaseController
'title' => 'Order Received',
'order' => $order,
'items' => $items,
'trackingUrl' => $this->orderStatuses->trackingUrl($order),
]));
}
public function orderTracker(): void
{
$orderNumber = Security::clean((string) ($_GET['order'] ?? ''));
$token = (string) ($_GET['token'] ?? '');
$order = $this->db->fetch('SELECT * FROM orders WHERE order_number = ?', [$orderNumber]);
if ($order === null) {
Http::abort(404, 'Order not found.');
}
$user = $this->auth->user();
$ownsOrder = $user !== null && (
(int) ($order['user_id'] ?? 0) === (int) $user['id']
|| strtolower((string) $order['customer_email']) === strtolower((string) $user['email'])
);
$validToken = $token !== ''
&& (string) $order['tracking_token'] !== ''
&& hash_equals((string) $order['tracking_token'], $token);
if (!$ownsOrder && !$validToken) {
Http::abort(403, 'This tracking link is invalid.');
}
View::render('store/order-tracker', $this->shared([
'title' => 'Track ' . $order['order_number'],
'order' => $order,
'events' => $this->db->fetchAll(
"SELECT * FROM order_events
WHERE order_id = ? AND (status <> '' OR customer_visible = 1)
ORDER BY created_at, id",
[(int) $order['id']]
),
'items' => $this->db->fetchAll(
'SELECT * FROM order_items WHERE order_id = ? ORDER BY id',
[(int) $order['id']]
),
]));
}
@@ -228,4 +264,3 @@ final class StoreController extends BaseController
));
}
}