b14f4c1796
New licensed lake, picnic, dining, server, burger, and food photography. Rebuilt homepage gallery, logo, colors, copy, menu, specials, and exports. Added dashboard-configurable branding in [settings.php (line 18)](/Users/tyemeclifford/Documents/GH/fatbottomgrille/views/admin/settings.php:18). Added a public attribution page and full [image credits](/Users/tyemeclifford/Documents/GH/fatbottomgrille/public/assets/images/CREDITS.md). Migrated existing SQLite menu data and admin email to admin@lakesideorders.test. Verified 51 PHP files, JSON/JavaScript, storefront pages, database migration, admin settings, and menu PDF. All passed.
122 lines
4.6 KiB
PHP
122 lines
4.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace FatBottom\Services;
|
|
|
|
use FatBottom\Core\Config;
|
|
use FatBottom\Core\Database;
|
|
use RuntimeException;
|
|
|
|
final class OrderStatusService
|
|
{
|
|
public const STATUSES = ['pending', 'accepted', 'declined', 'processing', 'ready', 'completed'];
|
|
|
|
public function __construct(
|
|
private readonly Config $config,
|
|
private readonly Database $db,
|
|
private readonly Mailer $mailer
|
|
) {
|
|
}
|
|
|
|
public function update(
|
|
int $orderId,
|
|
string $status,
|
|
string $note,
|
|
bool $customerVisible,
|
|
bool $sendEmail,
|
|
int $staffUserId
|
|
): array {
|
|
if (!in_array($status, self::STATUSES, true) || $status === 'pending') {
|
|
throw new RuntimeException('Choose a valid order status.');
|
|
}
|
|
$order = $this->db->fetch('SELECT * FROM orders WHERE id = ?', [$orderId]);
|
|
if ($order === null) {
|
|
throw new RuntimeException('Order not found.');
|
|
}
|
|
|
|
$sendEmail = $sendEmail || $status === 'accepted';
|
|
$pdo = $this->db->pdo();
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$this->db->execute(
|
|
'UPDATE orders SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[$status, $orderId]
|
|
);
|
|
$this->db->execute(
|
|
'INSERT INTO order_events
|
|
(order_id, user_id, event_type, details, status, note, customer_visible, email_sent)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 0)',
|
|
[
|
|
$orderId,
|
|
$staffUserId,
|
|
'status_changed',
|
|
'Status changed to ' . order_status_label($status) . ($note !== '' ? '. ' . $note : '.'),
|
|
$status,
|
|
$note,
|
|
$customerVisible && $note !== '' ? 1 : 0,
|
|
]
|
|
);
|
|
$eventId = (int) $pdo->lastInsertId();
|
|
$pdo->commit();
|
|
} catch (\Throwable $error) {
|
|
$pdo->rollBack();
|
|
throw $error;
|
|
}
|
|
|
|
$emailError = null;
|
|
if ($sendEmail) {
|
|
$order['status'] = $status;
|
|
try {
|
|
$this->sendStatusEmail($order, $status, $customerVisible ? $note : '');
|
|
$this->db->execute('UPDATE order_events SET email_sent = 1 WHERE id = ?', [$eventId]);
|
|
} catch (\Throwable $error) {
|
|
$emailError = $error->getMessage();
|
|
}
|
|
}
|
|
|
|
return ['email_requested' => $sendEmail, 'email_error' => $emailError];
|
|
}
|
|
|
|
public function sendStatusEmail(array $order, string $status, string $visibleNote = ''): void
|
|
{
|
|
$subjectTemplate = (string) $this->config->get(
|
|
'order_emails.' . $status . '.subject',
|
|
'{{business_name}} order {{order_number}}: {{status_label}}'
|
|
);
|
|
$bodyTemplate = (string) $this->config->get(
|
|
'order_emails.' . $status . '.body',
|
|
"Hi {{customer_name}},\n\nYour order {{order_number}} is now {{status_label}}.\n\n{{note}}\n\n{{tracking_url}}"
|
|
);
|
|
$values = [
|
|
'{{business_name}}' => (string) $this->config->get('business.name', 'Lakeside Orders'),
|
|
'{{customer_name}}' => (string) $order['customer_name'],
|
|
'{{order_number}}' => (string) $order['order_number'],
|
|
'{{status_label}}' => order_status_label($status),
|
|
'{{note}}' => $visibleNote,
|
|
'{{rejection_reason}}' => $status === 'declined' ? $visibleNote : '',
|
|
'{{tracking_url}}' => $this->trackingUrl($order),
|
|
];
|
|
$subject = trim(strip_tags(strtr($subjectTemplate, $values)));
|
|
|
|
$html = nl2br(strtr(htmlspecialchars($bodyTemplate, ENT_QUOTES, 'UTF-8'), [
|
|
'{{business_name}}' => e($values['{{business_name}}']),
|
|
'{{customer_name}}' => e($values['{{customer_name}}']),
|
|
'{{order_number}}' => e($values['{{order_number}}']),
|
|
'{{status_label}}' => e($values['{{status_label}}']),
|
|
'{{note}}' => e($values['{{note}}']),
|
|
'{{rejection_reason}}' => e($values['{{rejection_reason}}']),
|
|
'{{tracking_url}}' => '<a href="' . e($values['{{tracking_url}}']) . '">View your order tracker</a>',
|
|
]));
|
|
|
|
$this->mailer->send((string) $order['customer_email'], $subject, '<p>' . $html . '</p>');
|
|
}
|
|
|
|
public function trackingUrl(array $order): string
|
|
{
|
|
return rtrim((string) $this->config->get('app.url'), '/')
|
|
. '/order/track?order=' . rawurlencode((string) $order['order_number'])
|
|
. '&token=' . rawurlencode((string) $order['tracking_token']);
|
|
}
|
|
}
|