get('database.driver', 'sqlite'); $this->sqlite = $driver !== 'mysql'; if ($this->sqlite) { $directory = BASE_PATH . '/storage/database'; if (!is_dir($directory)) { mkdir($directory, 0775, true); } $this->pdo = new PDO('sqlite:' . $directory . '/store.sqlite'); $this->pdo->exec('PRAGMA foreign_keys = ON'); $this->pdo->exec('PRAGMA journal_mode = WAL'); } else { $dsn = sprintf( 'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $config->get('database.mysql_host'), (int) $config->get('database.mysql_port', 3306), $config->get('database.mysql_database') ); $this->pdo = new PDO( $dsn, (string) $config->get('database.mysql_username'), (string) $config->get('database.mysql_password') ); $this->pdo->exec("SET time_zone = '+00:00'"); } $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); } public function pdo(): PDO { return $this->pdo; } public function isSqlite(): bool { return $this->sqlite; } public function migrate(): void { if (!$this->sqlite) { return; } $schema = <<<'SQL' CREATE TABLE IF NOT EXISTS app_migrations ( id TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, full_name TEXT NOT NULL, phone TEXT NOT NULL DEFAULT '', street_address TEXT NOT NULL DEFAULT '', city TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT '', postal_code TEXT NOT NULL DEFAULT '', role TEXT NOT NULL DEFAULT 'customer', newsletter_opt_in INTEGER NOT NULL DEFAULT 1, two_factor_enabled INTEGER NOT NULL DEFAULT 0, email_verified_at TEXT, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS login_codes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, code_hash TEXT NOT NULL, purpose TEXT NOT NULL, expires_at TEXT NOT NULL, used_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS menu_categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, description TEXT NOT NULL DEFAULT '', display_order INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS menu_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, description TEXT NOT NULL DEFAULT '', price_cents INTEGER NOT NULL, compare_at_cents INTEGER, image_url TEXT NOT NULL DEFAULT '', dietary_tags TEXT NOT NULL DEFAULT '[]', featured INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1, display_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES menu_categories(id) ); CREATE TABLE IF NOT EXISTS specials ( id INTEGER PRIMARY KEY AUTOINCREMENT, menu_item_id INTEGER, title TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', badge TEXT NOT NULL DEFAULT 'Special', price_cents INTEGER, starts_at TEXT, ends_at TEXT, active INTEGER NOT NULL DEFAULT 1, display_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (menu_item_id) REFERENCES menu_items(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS tax_rates ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, jurisdiction TEXT NOT NULL, rate_basis_points INTEGER NOT NULL, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS carts ( id INTEGER PRIMARY KEY AUTOINCREMENT, token TEXT NOT NULL UNIQUE, user_id INTEGER, status TEXT NOT NULL DEFAULT 'active', subtotal_cents INTEGER NOT NULL DEFAULT 0, last_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, converted_at TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS cart_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, cart_id INTEGER NOT NULL, menu_item_id INTEGER NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, unit_price_cents INTEGER NOT NULL, notes TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(cart_id, menu_item_id, notes), FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE, FOREIGN KEY (menu_item_id) REFERENCES menu_items(id) ); CREATE TABLE IF NOT EXISTS customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, email TEXT NOT NULL UNIQUE, full_name TEXT NOT NULL, phone TEXT NOT NULL DEFAULT '', active INTEGER NOT NULL DEFAULT 1, internal_notes TEXT NOT NULL DEFAULT '', order_count INTEGER NOT NULL DEFAULT 0, lifetime_value_cents INTEGER NOT NULL DEFAULT 0, first_order_at TEXT, last_order_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_number TEXT NOT NULL UNIQUE, user_id INTEGER, customer_id INTEGER, cart_id INTEGER, tracking_token TEXT NOT NULL DEFAULT '', customer_name TEXT NOT NULL, customer_email TEXT NOT NULL, customer_phone TEXT NOT NULL, order_type TEXT NOT NULL DEFAULT 'pickup', status TEXT NOT NULL DEFAULT 'pending', payment_status TEXT NOT NULL DEFAULT 'paid', payment_provider TEXT NOT NULL DEFAULT 'demo', payment_reference TEXT NOT NULL DEFAULT '', subtotal_cents INTEGER NOT NULL, tax_cents INTEGER NOT NULL, total_cents INTEGER NOT NULL, special_instructions TEXT NOT NULL DEFAULT '', pickup_time TEXT, placed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL, FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS order_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, menu_item_id INTEGER, item_name TEXT NOT NULL, quantity INTEGER NOT NULL, unit_price_cents INTEGER NOT NULL, line_total_cents INTEGER NOT NULL, notes TEXT NOT NULL DEFAULT '', FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (menu_item_id) REFERENCES menu_items(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS order_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, user_id INTEGER, event_type TEXT NOT NULL, details TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT '', note TEXT NOT NULL DEFAULT '', customer_visible INTEGER NOT NULL DEFAULT 0, email_sent INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS refunds ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, staff_user_id INTEGER, amount_cents INTEGER NOT NULL, reason TEXT NOT NULL DEFAULT '', provider_reference TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (staff_user_id) REFERENCES users(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS visitor_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, user_id INTEGER, route TEXT NOT NULL, referrer TEXT NOT NULL DEFAULT '', user_agent TEXT NOT NULL DEFAULT '', ip_hash TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS newsletters ( id INTEGER PRIMARY KEY AUTOINCREMENT, subject TEXT NOT NULL, content_html TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft', recipient_count INTEGER NOT NULL DEFAULT 0, created_by INTEGER, sent_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ); CREATE INDEX IF NOT EXISTS idx_menu_items_category ON menu_items(category_id, active); CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status, placed_at); CREATE INDEX IF NOT EXISTS idx_carts_status ON carts(status, last_seen_at); CREATE INDEX IF NOT EXISTS idx_visitors_created ON visitor_events(created_at); CREATE INDEX IF NOT EXISTS idx_users_newsletter ON users(newsletter_opt_in, active); SQL; $this->pdo->exec($schema); $this->ensureSqliteColumn('users', 'two_factor_enabled', 'INTEGER NOT NULL DEFAULT 0'); $this->ensureSqliteColumn('orders', 'customer_id', 'INTEGER'); $this->ensureSqliteColumn('orders', 'tracking_token', "TEXT NOT NULL DEFAULT ''"); $this->ensureSqliteColumn('order_events', 'status', "TEXT NOT NULL DEFAULT ''"); $this->ensureSqliteColumn('order_events', 'note', "TEXT NOT NULL DEFAULT ''"); $this->ensureSqliteColumn('order_events', 'customer_visible', 'INTEGER NOT NULL DEFAULT 0'); $this->ensureSqliteColumn('order_events', 'email_sent', 'INTEGER NOT NULL DEFAULT 0'); $this->backfillOrderTrackingTokens(); $this->runMigration('20260614_order_status_crm', function (): void { $this->normalizeLegacyOrderStatuses(); $this->syncCustomersFromOrders(); }); $this->runMigration('20260614_lakeside_orders_demo', function (): void { $this->applyLakesideDemoBranding(); }); $this->pdo->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_orders_tracking_token ON orders(tracking_token)'); $this->pdo->exec('CREATE INDEX IF NOT EXISTS idx_orders_customer ON orders(customer_id, placed_at)'); $this->pdo->exec('CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(active, last_order_at)'); $this->seed(); } private function ensureSqliteColumn(string $table, string $column, string $definition): void { $columns = $this->pdo->query("PRAGMA table_info(`{$table}`)")->fetchAll(); foreach ($columns as $existing) { if (($existing['name'] ?? '') === $column) { return; } } $this->pdo->exec("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}"); } private function runMigration(string $id, callable $migration): void { $statement = $this->pdo->prepare('SELECT id FROM app_migrations WHERE id = ?'); $statement->execute([$id]); if ($statement->fetchColumn() !== false) { return; } $this->pdo->beginTransaction(); try { $migration(); $this->pdo->prepare('INSERT INTO app_migrations (id) VALUES (?)')->execute([$id]); $this->pdo->commit(); } catch (\Throwable $error) { $this->pdo->rollBack(); throw $error; } } private function backfillOrderTrackingTokens(): void { $orders = $this->pdo->query( "SELECT id FROM orders WHERE tracking_token IS NULL OR tracking_token = ''" )->fetchAll(); $update = $this->pdo->prepare('UPDATE orders SET tracking_token = ? WHERE id = ?'); foreach ($orders as $order) { $update->execute([bin2hex(random_bytes(24)), (int) $order['id']]); } } private function normalizeLegacyOrderStatuses(): void { $this->pdo->exec("UPDATE orders SET status = 'pending' WHERE status = 'paid'"); $this->pdo->exec("UPDATE orders SET status = 'processing' WHERE status = 'preparing'"); $this->pdo->exec("UPDATE orders SET status = 'declined' WHERE status = 'cancelled'"); } private function syncCustomersFromOrders(): void { $orders = $this->pdo->query( 'SELECT id, user_id, customer_name, customer_email, customer_phone FROM orders ORDER BY placed_at, id' )->fetchAll(); $findCustomer = $this->pdo->prepare('SELECT id, user_id FROM customers WHERE email = ?'); $findUser = $this->pdo->prepare('SELECT id FROM users WHERE email = ?'); $insert = $this->pdo->prepare( 'INSERT INTO customers (user_id, email, full_name, phone) VALUES (?, ?, ?, ?)' ); $update = $this->pdo->prepare( 'UPDATE customers SET user_id = ?, full_name = ?, phone = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ); $linkOrder = $this->pdo->prepare('UPDATE orders SET customer_id = ? WHERE id = ?'); $customerIds = []; foreach ($orders as $order) { $email = strtolower(trim((string) $order['customer_email'])); if ($email === '') { continue; } $findCustomer->execute([$email]); $customer = $findCustomer->fetch(); $userId = (int) ($order['user_id'] ?? 0) ?: null; if ($userId === null) { $findUser->execute([$email]); $userId = (int) ($findUser->fetchColumn() ?: 0) ?: null; } if ($customer === false) { $insert->execute([$userId, $email, $order['customer_name'], $order['customer_phone']]); $customerId = (int) $this->pdo->lastInsertId(); } else { $customerId = (int) $customer['id']; $update->execute([ $userId ?: ((int) ($customer['user_id'] ?? 0) ?: null), $order['customer_name'], $order['customer_phone'], $customerId, ]); } $linkOrder->execute([$customerId, (int) $order['id']]); $customerIds[$customerId] = true; } $refresh = $this->pdo->prepare( 'UPDATE customers SET order_count = (SELECT COUNT(*) FROM orders WHERE customer_id = ?), lifetime_value_cents = COALESCE((SELECT SUM(total_cents) FROM orders WHERE customer_id = ?), 0), first_order_at = (SELECT MIN(placed_at) FROM orders WHERE customer_id = ?), last_order_at = (SELECT MAX(placed_at) FROM orders WHERE customer_id = ?), updated_at = CURRENT_TIMESTAMP WHERE id = ?' ); foreach (array_keys($customerIds) as $customerId) { $refresh->execute([$customerId, $customerId, $customerId, $customerId, $customerId]); } } private function applyLakesideDemoBranding(): void { $categories = [ [1, 'Dockside Burgers', 'dockside-burgers', 'Flame-grilled favorites stacked for hungry lake days.'], [2, 'Picnic Baskets', 'picnic-baskets', 'Wraps, bowls, and handhelds made to travel well.'], [3, 'Lakehouse Plates', 'lakehouse-plates', 'Relaxed comfort food for a proper table by the water.'], [4, 'Boardwalk Sides', 'boardwalk-sides', 'Shareable extras for the dock, blanket, or patio.'], [5, 'Cold Drinks', 'cold-drinks', 'Fresh pours for sunny afternoons.'], ]; $categoryStatement = $this->pdo->prepare( 'UPDATE menu_categories SET name = ?, slug = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ); foreach ($categories as [$id, $name, $slug, $description]) { $categoryStatement->execute([$name, $slug, $description, $id]); } $items = [ [1, 1, 'Lakeside Double', 'lakeside-double', 'Two grilled patties, cheddar, lettuce, tomato, pickles, and dock sauce.', 1299, '/assets/images/lakeside-burger.jpg', '["House favorite"]', 1], [2, 1, 'Dockmaster Bacon Burger', 'dockmaster-bacon-burger', 'Two patties, smoked bacon, cheddar, crisp lettuce, tomato, and onion.', 1399, '/assets/images/lakeside-burger.jpg', '[]', 1], [3, 1, 'Sunset Mushroom Melt', 'sunset-mushroom-melt', 'Grilled beef, Swiss, mushrooms, caramelized onions, and peppercorn mayo.', 1299, '/assets/images/lakeside-burger.jpg', '[]', 0], [4, 2, 'Marina Chicken Wrap', 'marina-chicken-wrap', 'Chicken, spinach, tomato, cucumber, and creamy herb dressing in a soft wrap.', 1199, '/assets/images/picnic-wrap.jpg', '["Picnic pick"]', 1], [5, 2, 'Shoreline Falafel Bowl', 'shoreline-falafel-bowl', 'Falafel, seasoned rice, bright slaw, peppers, herbs, lemon, and green tahini.', 1249, '/assets/images/shoreline-bowl.jpg', '["Vegetarian"]', 1], [6, 3, 'Lakehouse Chicken Plate', 'lakehouse-chicken-plate', 'Seasoned chicken legs with a creamy picnic side and rotating vegetables.', 1499, '/assets/images/lakehouse-chicken.jpg', '[]', 1], [7, 3, 'Picnic Club Wrap', 'picnic-club-wrap', 'Sliced chicken, greens, tomato, smoky bacon, and herb dressing.', 1299, '/assets/images/picnic-wrap.jpg', '[]', 0], [8, 4, 'Boardwalk Fries', 'boardwalk-fries', 'Golden fries finished with flaky salt and your choice of dipping sauce.', 599, '/assets/images/boardwalk-fries.jpg', '["Vegetarian"]', 1], [9, 4, 'Garden Side Bowl', 'garden-side-bowl', 'Rice, chopped vegetables, herbs, lemon, and creamy green dressing.', 649, '/assets/images/shoreline-bowl.jpg', '["Vegetarian"]', 0], [10, 5, 'Lemon Iced Tea', 'lemon-iced-tea', 'Fresh-brewed iced tea served with lemon.', 299, '/assets/images/lemon-iced-tea.jpg', '[]', 0], [11, 5, 'Lakeside Lemonade', 'lakeside-lemonade', 'Bright house lemonade poured over ice.', 329, '/assets/images/lemon-iced-tea.jpg', '[]', 0], ]; $itemStatement = $this->pdo->prepare( 'UPDATE menu_items SET category_id = ?, name = ?, slug = ?, description = ?, price_cents = ?, image_url = ?, dietary_tags = ?, featured = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ); foreach ($items as [$id, $categoryId, $name, $slug, $description, $price, $image, $tags, $featured]) { $itemStatement->execute([ $categoryId, $name, $slug, $description, $price, $image, $tags, $featured, $id, ]); } $specialStatement = $this->pdo->prepare( 'UPDATE specials SET menu_item_id = ?, title = ?, description = ?, badge = ?, price_cents = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ); $specialStatement->execute([ 1, 'Sunset Burger Pick', 'The Lakeside Double at its easygoing online demo price.', 'Dock Deal', 1299, 1, ]); $specialStatement->execute([ 8, 'Boardwalk Break', 'A hot order of Boardwalk Fries for the table or picnic blanket.', 'Picnic Pick', 599, 2, ]); $this->pdo->exec("UPDATE tax_rates SET name = 'Demo State Sales Tax' WHERE id = 1"); $this->pdo->exec("UPDATE tax_rates SET name = 'Lakeview Demo Tax' WHERE id = 2"); $this->pdo->exec( "UPDATE users SET email = 'admin@lakesideorders.test' WHERE email = 'admin@fatbottomgrille.com' AND NOT EXISTS ( SELECT 1 FROM users AS existing WHERE existing.email = 'admin@lakesideorders.test' )" ); } private function seed(): void { $categoryCount = (int) $this->pdo->query('SELECT COUNT(*) FROM menu_categories')->fetchColumn(); if ($categoryCount > 0) { return; } $this->pdo->beginTransaction(); try { $categories = [ ['Dockside Burgers', 'dockside-burgers', 'Flame-grilled favorites stacked for hungry lake days.', 10], ['Picnic Baskets', 'picnic-baskets', 'Wraps, bowls, and handhelds made to travel well.', 20], ['Lakehouse Plates', 'lakehouse-plates', 'Relaxed comfort food for a proper table by the water.', 30], ['Boardwalk Sides', 'boardwalk-sides', 'Shareable extras for the dock, blanket, or patio.', 40], ['Cold Drinks', 'cold-drinks', 'Fresh pours for sunny afternoons.', 50], ]; $categoryStatement = $this->pdo->prepare( 'INSERT INTO menu_categories (name, slug, description, display_order) VALUES (?, ?, ?, ?)' ); foreach ($categories as $category) { $categoryStatement->execute($category); } $categoryIds = []; foreach ($this->pdo->query('SELECT id, slug FROM menu_categories')->fetchAll() as $category) { $categoryIds[$category['slug']] = (int) $category['id']; } $items = [ ['dockside-burgers', 'Lakeside Double', 'lakeside-double', 'Two grilled patties, cheddar, lettuce, tomato, pickles, and dock sauce.', 1299, '/assets/images/lakeside-burger.jpg', 1, '["House favorite"]'], ['dockside-burgers', 'Dockmaster Bacon Burger', 'dockmaster-bacon-burger', 'Two patties, smoked bacon, cheddar, crisp lettuce, tomato, and onion.', 1399, '/assets/images/lakeside-burger.jpg', 1, '[]'], ['dockside-burgers', 'Sunset Mushroom Melt', 'sunset-mushroom-melt', 'Grilled beef, Swiss, mushrooms, caramelized onions, and peppercorn mayo.', 1299, '/assets/images/lakeside-burger.jpg', 0, '[]'], ['picnic-baskets', 'Marina Chicken Wrap', 'marina-chicken-wrap', 'Chicken, spinach, tomato, cucumber, and creamy herb dressing in a soft wrap.', 1199, '/assets/images/picnic-wrap.jpg', 1, '["Picnic pick"]'], ['picnic-baskets', 'Shoreline Falafel Bowl', 'shoreline-falafel-bowl', 'Falafel, seasoned rice, bright slaw, peppers, herbs, lemon, and green tahini.', 1249, '/assets/images/shoreline-bowl.jpg', 1, '["Vegetarian"]'], ['lakehouse-plates', 'Lakehouse Chicken Plate', 'lakehouse-chicken-plate', 'Seasoned chicken legs with a creamy picnic side and rotating vegetables.', 1499, '/assets/images/lakehouse-chicken.jpg', 1, '[]'], ['lakehouse-plates', 'Picnic Club Wrap', 'picnic-club-wrap', 'Sliced chicken, greens, tomato, smoky bacon, and herb dressing.', 1299, '/assets/images/picnic-wrap.jpg', 0, '[]'], ['boardwalk-sides', 'Boardwalk Fries', 'boardwalk-fries', 'Golden fries finished with flaky salt and your choice of dipping sauce.', 599, '/assets/images/boardwalk-fries.jpg', 1, '["Vegetarian"]'], ['boardwalk-sides', 'Garden Side Bowl', 'garden-side-bowl', 'Rice, chopped vegetables, herbs, lemon, and creamy green dressing.', 649, '/assets/images/shoreline-bowl.jpg', 0, '["Vegetarian"]'], ['cold-drinks', 'Lemon Iced Tea', 'lemon-iced-tea', 'Fresh-brewed iced tea served with lemon.', 299, '/assets/images/lemon-iced-tea.jpg', 0, '[]'], ['cold-drinks', 'Lakeside Lemonade', 'lakeside-lemonade', 'Bright house lemonade poured over ice.', 329, '/assets/images/lemon-iced-tea.jpg', 0, '[]'], ]; $itemStatement = $this->pdo->prepare( 'INSERT INTO menu_items (category_id, name, slug, description, price_cents, image_url, featured, dietary_tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' ); foreach ($items as $item) { $itemStatement->execute([ $categoryIds[$item[0]], $item[1], $item[2], $item[3], $item[4], $item[5], $item[6], $item[7], ]); } $lakesideDoubleId = (int) $this->pdo->query( "SELECT id FROM menu_items WHERE slug = 'lakeside-double'" )->fetchColumn(); $boardwalkFriesId = (int) $this->pdo->query( "SELECT id FROM menu_items WHERE slug = 'boardwalk-fries'" )->fetchColumn(); $specialStatement = $this->pdo->prepare( 'INSERT INTO specials (menu_item_id, title, description, badge, price_cents, display_order) VALUES (?, ?, ?, ?, ?, ?)' ); $specialStatement->execute([ $lakesideDoubleId, 'Sunset Burger Pick', 'The Lakeside Double at its easygoing online demo price.', 'Dock Deal', 1299, 10, ]); $specialStatement->execute([ $boardwalkFriesId, 'Boardwalk Break', 'A hot order of Boardwalk Fries for the table or picnic blanket.', 'Picnic Pick', 599, 20, ]); $taxStatement = $this->pdo->prepare( 'INSERT INTO tax_rates (name, jurisdiction, rate_basis_points) VALUES (?, ?, ?)' ); $taxStatement->execute(['Demo State Sales Tax', 'state', 600]); $taxStatement->execute(['Lakeview Demo Tax', 'city', 100]); $adminStatement = $this->pdo->prepare( 'INSERT INTO users (email, password_hash, full_name, phone, role, newsletter_opt_in, two_factor_enabled, email_verified_at) VALUES (?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP)' ); $adminStatement->execute([ 'admin@lakesideorders.test', password_hash('ChangeMe123!', PASSWORD_DEFAULT), 'Store Administrator', '', 'admin', 0, ]); $this->pdo->commit(); } catch (\Throwable $error) { $this->pdo->rollBack(); throw new RuntimeException('Unable to seed the store database.', 0, $error); } } public function fetchAll(string $sql, array $parameters = []): array { $statement = $this->pdo->prepare($sql); $statement->execute($parameters); return $statement->fetchAll(); } public function fetch(string $sql, array $parameters = []): ?array { $statement = $this->pdo->prepare($sql); $statement->execute($parameters); $row = $statement->fetch(); return $row === false ? null : $row; } public function execute(string $sql, array $parameters = []): bool { $statement = $this->pdo->prepare($sql); return $statement->execute($parameters); } }