$id, 'full_name' => $fullName, 'email' => $email, ]; } function store_is_logged_in(): bool { return store_current_user() !== null; } function store_user_first_name(string $fullName): string { $fullName = trim(preg_replace('/\s+/', ' ', $fullName) ?? ''); if ($fullName === '') { return 'Pelanggan'; } $parts = explode(' ', $fullName); return store_substr((string)$parts[0], 0, 24); } function store_set_user_session(array $user): void { $_SESSION[STORE_USER_KEY] = [ 'id' => (int)($user['id'] ?? 0), 'full_name' => store_sanitize_line((string)($user['full_name'] ?? ''), 120), 'email' => store_lower(trim((string)($user['email'] ?? ''))), ]; } function store_logout_user(): void { unset($_SESSION[STORE_USER_KEY], $_SESSION[STORE_PENDING_CART_ADD_KEY]); if (session_status() === PHP_SESSION_ACTIVE) { session_regenerate_id(true); } } function store_queue_cart_add_after_login(string $slug, int $quantity = 1, string $redirectTo = 'index.php'): bool { $product = store_product($slug); if (!$product) { return false; } $_SESSION[STORE_PENDING_CART_ADD_KEY] = [ 'slug' => (string)$product['slug'], 'quantity' => max(1, min(20, $quantity)), 'redirect_to' => store_safe_redirect($redirectTo, 'index.php'), ]; return true; } function store_pending_cart_add(): ?array { $pending = $_SESSION[STORE_PENDING_CART_ADD_KEY] ?? null; if (!is_array($pending)) { return null; } $slug = (string)($pending['slug'] ?? ''); if ($slug === '') { unset($_SESSION[STORE_PENDING_CART_ADD_KEY]); return null; } $product = store_product($slug); if (!$product) { unset($_SESSION[STORE_PENDING_CART_ADD_KEY]); return null; } return [ 'slug' => (string)$product['slug'], 'quantity' => max(1, min(20, (int)($pending['quantity'] ?? 1))), 'redirect_to' => store_safe_redirect((string)($pending['redirect_to'] ?? 'index.php'), 'index.php'), 'product' => $product, ]; } function store_consume_pending_cart_add(): ?array { $pending = store_pending_cart_add(); unset($_SESSION[STORE_PENDING_CART_ADD_KEY]); if (!$pending) { return null; } return [ 'slug' => (string)$pending['slug'], 'quantity' => (int)$pending['quantity'], 'redirect_to' => (string)$pending['redirect_to'], ]; } function store_auth_password_error(string $password): string { if (store_strlen($password) < 8) { return 'Password minimal 8 karakter.'; } return ''; } function store_register_user(array $source): array { store_ensure_schema(); $form = [ 'full_name' => store_sanitize_line((string)($source['full_name'] ?? ''), 120), 'email' => store_lower(trim((string)($source['email'] ?? ''))), ]; $password = (string)($source['password'] ?? ''); $confirmPassword = (string)($source['confirm_password'] ?? ''); $errors = []; if (store_strlen($form['full_name']) < 3) { $errors['full_name'] = 'Nama lengkap minimal 3 karakter.'; } if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) { $errors['email'] = 'Masukkan email yang valid.'; } $passwordError = store_auth_password_error($password); if ($passwordError !== '') { $errors['password'] = $passwordError; } if ($confirmPassword === '') { $errors['confirm_password'] = 'Ulangi password untuk konfirmasi.'; } elseif ($password !== $confirmPassword) { $errors['confirm_password'] = 'Konfirmasi password belum cocok.'; } if (!isset($errors['email'])) { $exists = db()->prepare('SELECT id FROM customer_users WHERE email = :email LIMIT 1'); $exists->bindValue(':email', $form['email']); $exists->execute(); if ($exists->fetch()) { $errors['email'] = 'Email sudah terdaftar. Silakan login.'; } } if ($errors) { return [ 'success' => false, 'message' => 'Pendaftaran belum berhasil. Periksa kembali data Anda.', 'errors' => $errors, 'form' => $form, ]; } $stmt = db()->prepare('INSERT INTO customer_users (full_name, email, password_hash, last_login_at) VALUES (:full_name, :email, :password_hash, NOW())'); $stmt->bindValue(':full_name', $form['full_name']); $stmt->bindValue(':email', $form['email']); $stmt->bindValue(':password_hash', password_hash($password, PASSWORD_DEFAULT)); $stmt->execute(); $user = [ 'id' => (int)db()->lastInsertId(), 'full_name' => $form['full_name'], 'email' => $form['email'], ]; if (session_status() === PHP_SESSION_ACTIVE) { session_regenerate_id(true); } store_set_user_session($user); return [ 'success' => true, 'message' => 'Akun berhasil dibuat. Anda sudah login.', 'user' => $user, 'form' => ['full_name' => '', 'email' => ''], ]; } function store_login_user(array $source): array { store_ensure_schema(); $form = [ 'email' => store_lower(trim((string)($source['email'] ?? ''))), ]; $password = (string)($source['password'] ?? ''); if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL) || $password === '') { $errors = []; if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) { $errors['email'] = 'Masukkan email yang valid.'; } if ($password === '') { $errors['password'] = 'Password wajib diisi.'; } return [ 'success' => false, 'message' => 'Masukkan email dan password yang benar.', 'errors' => $errors, 'form' => $form, ]; } $stmt = db()->prepare('SELECT id, full_name, email, password_hash FROM customer_users WHERE email = :email LIMIT 1'); $stmt->bindValue(':email', $form['email']); $stmt->execute(); $user = $stmt->fetch(); if (!$user || !password_verify($password, (string)($user['password_hash'] ?? ''))) { return [ 'success' => false, 'message' => 'Email atau password tidak cocok.', 'errors' => [], 'form' => $form, ]; } if (password_needs_rehash((string)($user['password_hash'] ?? ''), PASSWORD_DEFAULT)) { $rehash = db()->prepare('UPDATE customer_users SET password_hash = :password_hash WHERE id = :id LIMIT 1'); $rehash->bindValue(':password_hash', password_hash($password, PASSWORD_DEFAULT)); $rehash->bindValue(':id', (int)$user['id'], PDO::PARAM_INT); $rehash->execute(); } $touch = db()->prepare('UPDATE customer_users SET last_login_at = NOW() WHERE id = :id LIMIT 1'); $touch->bindValue(':id', (int)$user['id'], PDO::PARAM_INT); $touch->execute(); if (session_status() === PHP_SESSION_ACTIVE) { session_regenerate_id(true); } store_set_user_session($user); return [ 'success' => true, 'message' => 'Login berhasil. Selamat datang kembali.', 'user' => [ 'id' => (int)$user['id'], 'full_name' => (string)$user['full_name'], 'email' => (string)$user['email'], ], 'form' => ['email' => ''], ]; } function store_categories(): array { return [ 'all' => [ 'label' => 'Semua', 'description' => 'Seluruh katalog bakery untuk daily order dan pre-order.', ], 'signature-cakes' => [ 'label' => 'Signature Cakes', 'description' => 'Whole cake dan sliced cake untuk momen spesial.', ], 'artisan-bread' => [ 'label' => 'Artisan Bread', 'description' => 'Roti harian dengan tekstur bersih dan fermentasi lebih lama.', ], 'cookies-pastry' => [ 'label' => 'Cookies & Pastry', 'description' => 'Pilihan pastry dan cookies untuk coffee break dan hampers.', ], ]; } function store_products(): array { return [ 'burnt-cheesecake' => [ 'slug' => 'burnt-cheesecake', 'name' => 'Burnt Cheesecake', 'category' => 'signature-cakes', 'category_label' => 'Signature Cakes', 'price' => 185000.0, 'unit' => 'whole cake', 'short_description' => 'Cheesecake creamy dengan permukaan karamel gelap dan finish yang clean.', 'description' => 'Whole cake 18 cm dengan rasa vanilla-cream cheese yang lembut dan lapisan atas yang sedikit smoky. Cocok untuk 8–10 porsi dan ideal untuk dessert keluarga.', 'lead_time' => 'Pre-order 1 hari', 'serves' => '8–10 porsi', 'tone' => 'ink', 'visual_code' => 'BC', 'highlights' => ['18 cm', 'Best seller', 'Pre-order 1 hari'], ], 'matcha-roll-cake' => [ 'slug' => 'matcha-roll-cake', 'name' => 'Matcha Roll Cake', 'category' => 'signature-cakes', 'category_label' => 'Signature Cakes', 'price' => 158000.0, 'unit' => 'roll cake', 'short_description' => 'Sponge cake tipis dengan filling krim matcha yang ringan.', 'description' => 'Roll cake dengan sponge lembut, rasa matcha yang clean, dan krim yang tidak terlalu manis. Cocok untuk sharing di meeting kecil atau hadiah praktis.', 'lead_time' => 'Ready setiap pagi', 'serves' => '6–8 potong', 'tone' => 'stone', 'visual_code' => 'MR', 'highlights' => ['6–8 potong', 'Fresh daily', 'Light sweetness'], ], 'sesame-sourdough' => [ 'slug' => 'sesame-sourdough', 'name' => 'Sesame Sourdough Loaf', 'category' => 'artisan-bread', 'category_label' => 'Artisan Bread', 'price' => 52000.0, 'unit' => 'loaf', 'short_description' => 'Roti sourdough dengan crust renyah, crumb lembut, dan taburan wijen.', 'description' => 'Loaf fermentasi alami dengan aroma gandum yang lebih dalam. Enak untuk sandwich, toast sarapan, atau stok roti rumah selama 2–3 hari.', 'lead_time' => 'Ready setiap siang', 'serves' => '8 slice tebal', 'tone' => 'sand', 'visual_code' => 'SD', 'highlights' => ['Fermentasi alami', '8 slice', 'Crust renyah'], ], 'focaccia-olive' => [ 'slug' => 'focaccia-olive', 'name' => 'Focaccia Tomato Olive', 'category' => 'artisan-bread', 'category_label' => 'Artisan Bread', 'price' => 48000.0, 'unit' => 'tray', 'short_description' => 'Focaccia savory dengan olive oil, tomat, dan olive hitam.', 'description' => 'Roti focaccia bertekstur empuk dengan permukaan yang sedikit garing. Cocok untuk snack meeting, teman sup, atau dibuat sandwich terbuka.', 'lead_time' => 'Ready terbatas', 'serves' => '4–5 porsi', 'tone' => 'taupe', 'visual_code' => 'FO', 'highlights' => ['Savory', '4–5 porsi', 'Ready terbatas'], ], 'butter-croissant' => [ 'slug' => 'butter-croissant', 'name' => 'Butter Croissant', 'category' => 'cookies-pastry', 'category_label' => 'Cookies & Pastry', 'price' => 28000.0, 'unit' => 'pcs', 'short_description' => 'Croissant berlapis dengan aroma butter dan tekstur flaky.', 'description' => 'Pastry klasik dengan lapisan tipis yang renyah di luar dan ringan di dalam. Cocok untuk sarapan cepat atau pairing dengan kopi hitam.', 'lead_time' => 'Ready dari pagi', 'serves' => '1 pcs besar', 'tone' => 'stone', 'visual_code' => 'CR', 'highlights' => ['Flaky', 'Fresh baked', 'Coffee pairing'], ], 'almond-cookies-tin' => [ 'slug' => 'almond-cookies-tin', 'name' => 'Almond Cookies Tin', 'category' => 'cookies-pastry', 'category_label' => 'Cookies & Pastry', 'price' => 68000.0, 'unit' => 'tin', 'short_description' => 'Cookies renyah dengan potongan almond panggang dalam kemasan kaleng.', 'description' => 'Cookies butter dengan tekstur crisp dan rasa toasted almond yang hangat. Praktis untuk hampers kecil, pantry kantor, atau stok camilan rumah.', 'lead_time' => 'Ready stock', 'serves' => '20–24 pcs', 'tone' => 'ink', 'visual_code' => 'AC', 'highlights' => ['20–24 pcs', 'Giftable', 'Ready stock'], ], ]; } function store_product(string $slug): ?array { $products = store_products(); return $products[$slug] ?? null; } function store_filtered_products(string $category = 'all'): array { $products = array_values(store_products()); if ($category === 'all' || !isset(store_categories()[$category])) { return $products; } return array_values(array_filter( $products, static fn(array $product): bool => $product['category'] === $category )); } function store_related_products(string $slug, string $category, int $limit = 3): array { $products = array_values(array_filter( store_products(), static fn(array $product): bool => $product['slug'] !== $slug && $product['category'] === $category )); if (count($products) < $limit) { $fallback = array_values(array_filter( store_products(), static fn(array $product): bool => $product['slug'] !== $slug && $product['category'] !== $category )); $products = array_merge($products, $fallback); } return array_slice($products, 0, $limit); } function store_cart(): array { if (!isset($_SESSION[STORE_CART_KEY]) || !is_array($_SESSION[STORE_CART_KEY])) { $_SESSION[STORE_CART_KEY] = []; } return $_SESSION[STORE_CART_KEY]; } function store_set_cart(array $cart): void { $_SESSION[STORE_CART_KEY] = $cart; } function store_cart_count(): int { return array_sum(store_cart()); } function store_add_to_cart(string $slug, int $quantity = 1): bool { $product = store_product($slug); if (!$product) { return false; } $quantity = max(1, min(20, $quantity)); $cart = store_cart(); $cart[$slug] = min(20, ((int)($cart[$slug] ?? 0)) + $quantity); store_set_cart($cart); return true; } function store_resume_pending_cart_add(string $fallback = 'index.php'): string { $pending = store_consume_pending_cart_add(); if (!$pending) { return store_safe_redirect($fallback, 'index.php'); } $redirectTo = store_safe_redirect((string)($pending['redirect_to'] ?? $fallback), $fallback); $product = store_product((string)$pending['slug']); if (!$product) { store_flash('danger', 'Produk yang dipilih sudah tidak tersedia.'); return $redirectTo; } $quantity = (int)($pending['quantity'] ?? 1); if (store_add_to_cart((string)$pending['slug'], $quantity)) { $message = ($product['name'] ?? 'Produk') . ' ditambahkan ke keranjang.'; if ($quantity > 1) { $message = ($product['name'] ?? 'Produk') . ' ditambahkan ke keranjang sebanyak ' . $quantity . ' item.'; } store_flash('success', $message); } else { store_flash('danger', 'Produk tidak ditemukan.'); } return $redirectTo; } function store_update_cart(array $quantities): void { $products = store_products(); $updated = []; foreach ($quantities as $slug => $quantity) { if (!isset($products[$slug])) { continue; } $qty = (int)$quantity; if ($qty > 0) { $updated[$slug] = min(20, $qty); } } store_set_cart($updated); } function store_remove_from_cart(string $slug): void { $cart = store_cart(); unset($cart[$slug]); store_set_cart($cart); } function store_money(float $amount): string { return 'Rp ' . number_format($amount, 0, ',', '.'); } function store_substr(string $value, int $start, int $length): string { if (function_exists('mb_substr')) { return (string)mb_substr($value, $start, $length); } return (string)substr($value, $start, $length); } function store_strlen(string $value): int { if (function_exists('mb_strlen')) { return (int)mb_strlen($value); } return strlen($value); } function store_lower(string $value): string { if (function_exists('mb_strtolower')) { return (string)mb_strtolower($value); } return strtolower($value); } function store_cart_summary(): array { $products = store_products(); $lines = []; foreach (store_cart() as $slug => $quantity) { if (!isset($products[$slug])) { continue; } $product = $products[$slug]; $lineTotal = ((float)$product['price']) * (int)$quantity; $lines[] = [ 'slug' => $slug, 'quantity' => (int)$quantity, 'product' => $product, 'line_total' => $lineTotal, ]; } $subtotal = array_reduce( $lines, static fn(float $carry, array $line): float => $carry + (float)$line['line_total'], 0.0 ); $shippingFee = $subtotal <= 0 ? 0.0 : ($subtotal >= 250000 ? 0.0 : 18000.0); return [ 'lines' => $lines, 'subtotal' => $subtotal, 'shipping_fee' => $shippingFee, 'grand_total' => $subtotal + $shippingFee, ]; } function store_payment_methods(): array { return [ 'bank_transfer' => [ 'label' => 'Transfer Bank', 'description' => 'Instruksi pembayaran tampil setelah checkout selesai.', 'instruction' => 'Transfer ke rekening demo bakery lalu simpan bukti bayar untuk verifikasi admin.', ], 'qris' => [ 'label' => 'QRIS', 'description' => 'Praktis untuk mobile banking dan e-wallet.', 'instruction' => 'Gunakan QRIS demo yang diberikan admin setelah pesanan masuk ke sistem.', ], 'cod' => [ 'label' => 'Bayar di Tempat', 'description' => 'Tersedia untuk pengantaran area terdekat.', 'instruction' => 'Siapkan nominal pas saat kurir mengantarkan pesanan ke alamat Anda.', ], ]; } function store_status_steps(): array { return [ [ 'value' => 'Menunggu Pembayaran', 'label' => 'Menunggu Pembayaran', 'description' => 'Pesanan tersimpan dan menunggu pembayaran pelanggan.', ], [ 'value' => 'Diproses', 'label' => 'Diproses', 'description' => 'Kitchen sedang menyiapkan item sesuai pesanan.', ], [ 'value' => 'Dikirim', 'label' => 'Dikirim', 'description' => 'Pesanan sedang diantar ke alamat tujuan.', ], [ 'value' => 'Selesai', 'label' => 'Selesai', 'description' => 'Pesanan telah diterima pelanggan.', ], ]; } function store_status_index(string $status): int { foreach (store_status_steps() as $index => $step) { if ($step['value'] === $status) { return $index; } } return -1; } function store_status_class(string $status): string { return match ($status) { 'Menunggu Pembayaran' => 'status-pill status-pill--pending', 'Diproses' => 'status-pill status-pill--processing', 'Dikirim' => 'status-pill status-pill--shipping', 'Selesai' => 'status-pill status-pill--done', 'Batal' => 'status-pill status-pill--cancelled', default => 'status-pill', }; } function store_flash(string $type, string $message): void { $_SESSION[STORE_FLASH_KEY][] = [ 'type' => $type, 'message' => $message, ]; } function store_consume_flashes(): array { $flashes = $_SESSION[STORE_FLASH_KEY] ?? []; unset($_SESSION[STORE_FLASH_KEY]); return is_array($flashes) ? $flashes : []; } function store_input_class(array $errors, string $field): string { return isset($errors[$field]) ? ' is-invalid' : ''; } function store_checkout_defaults(): array { $currentUser = store_current_user(); return [ 'customer_name' => (string)($currentUser['full_name'] ?? ''), 'email' => (string)($currentUser['email'] ?? ''), 'phone' => '', 'address' => '', 'note' => '', 'payment_method' => 'bank_transfer', ]; } function store_sanitize_line(string $value, int $max = 255): string { $value = trim(preg_replace('/\s+/', ' ', $value) ?? ''); return store_substr($value, 0, $max); } function store_normalize_checkout_input(array $source): array { $defaults = store_checkout_defaults(); return [ 'customer_name' => store_sanitize_line((string)($source['customer_name'] ?? $defaults['customer_name']), 120), 'email' => store_lower(trim((string)($source['email'] ?? $defaults['email']))), 'phone' => store_sanitize_line((string)($source['phone'] ?? $defaults['phone']), 40), 'address' => trim(store_substr((string)($source['address'] ?? $defaults['address']), 0, 500)), 'note' => trim(store_substr((string)($source['note'] ?? $defaults['note']), 0, 500)), 'payment_method' => store_sanitize_line((string)($source['payment_method'] ?? $defaults['payment_method']), 40), ]; } function store_validate_checkout_input(array $data): array { $errors = []; $methods = store_payment_methods(); if (store_strlen($data['customer_name']) < 3) { $errors['customer_name'] = 'Nama minimal 3 karakter.'; } if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { $errors['email'] = 'Masukkan email yang valid.'; } $normalizedPhone = preg_replace('/[^0-9+]/', '', $data['phone']) ?? ''; if (strlen($normalizedPhone) < 8) { $errors['phone'] = 'Nomor telepon minimal 8 digit.'; } if (store_strlen($data['address']) < 10) { $errors['address'] = 'Alamat minimal 10 karakter.'; } if (!isset($methods[$data['payment_method']])) { $errors['payment_method'] = 'Pilih metode pembayaran yang tersedia.'; } return $errors; } function store_ensure_schema(): void { static $ready = false; if ($ready) { return; } db()->exec( "CREATE TABLE IF NOT EXISTS orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_number VARCHAR(30) NOT NULL UNIQUE, customer_name VARCHAR(120) NOT NULL, email VARCHAR(160) NOT NULL, phone VARCHAR(40) NOT NULL, address TEXT NOT NULL, note TEXT NULL, payment_method VARCHAR(40) NOT NULL, status VARCHAR(40) NOT NULL DEFAULT 'Menunggu Pembayaran', subtotal DECIMAL(12,2) NOT NULL DEFAULT 0, shipping_fee DECIMAL(12,2) NOT NULL DEFAULT 0, grand_total DECIMAL(12,2) NOT NULL DEFAULT 0, items_json LONGTEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_order_number (order_number), INDEX idx_email_created_at (email, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); db()->exec( "CREATE TABLE IF NOT EXISTS customer_users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, full_name VARCHAR(120) NOT NULL, email VARCHAR(160) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, last_login_at TIMESTAMP NULL DEFAULT NULL, INDEX idx_customer_users_email (email) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $ready = true; } function store_generate_order_number(): string { store_ensure_schema(); $pdo = db(); for ($attempt = 0; $attempt < 6; $attempt++) { $candidate = 'SB' . date('ymd') . '-' . (string)random_int(1000, 9999); $stmt = $pdo->prepare('SELECT id FROM orders WHERE order_number = :order_number LIMIT 1'); $stmt->execute(['order_number' => $candidate]); if (!$stmt->fetch()) { return $candidate; } } return 'SB' . date('ymdHis'); } function store_create_order(array $input): array { $data = store_normalize_checkout_input($input); $errors = store_validate_checkout_input($data); $summary = store_cart_summary(); if (empty($summary['lines'])) { $errors['cart'] = 'Keranjang masih kosong. Tambahkan produk terlebih dahulu.'; } if ($errors) { return [ 'success' => false, 'errors' => $errors, 'data' => $data, ]; } store_ensure_schema(); $items = array_map( static function (array $line): array { return [ 'slug' => $line['slug'], 'name' => $line['product']['name'], 'price' => (float)$line['product']['price'], 'quantity' => (int)$line['quantity'], 'line_total' => (float)$line['line_total'], ]; }, $summary['lines'] ); $orderNumber = store_generate_order_number(); $stmt = db()->prepare( 'INSERT INTO orders ( order_number, customer_name, email, phone, address, note, payment_method, status, subtotal, shipping_fee, grand_total, items_json ) VALUES ( :order_number, :customer_name, :email, :phone, :address, :note, :payment_method, :status, :subtotal, :shipping_fee, :grand_total, :items_json )' ); $stmt->bindValue(':order_number', $orderNumber); $stmt->bindValue(':customer_name', $data['customer_name']); $stmt->bindValue(':email', $data['email']); $stmt->bindValue(':phone', $data['phone']); $stmt->bindValue(':address', $data['address']); $stmt->bindValue(':note', $data['note'] === '' ? null : $data['note']); $stmt->bindValue(':payment_method', $data['payment_method']); $stmt->bindValue(':status', 'Menunggu Pembayaran'); $stmt->bindValue(':subtotal', $summary['subtotal']); $stmt->bindValue(':shipping_fee', $summary['shipping_fee']); $stmt->bindValue(':grand_total', $summary['grand_total']); $stmt->bindValue(':items_json', json_encode($items, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $stmt->execute(); $_SESSION[STORE_CART_KEY] = []; $_SESSION[STORE_LAST_ORDER_KEY] = [ 'order_number' => $orderNumber, 'email' => $data['email'], ]; return [ 'success' => true, 'order_number' => $orderNumber, 'data' => $data, ]; } function store_find_order(string $orderNumber, string $email = ''): ?array { $orderNumber = store_sanitize_line($orderNumber, 30); $email = trim(store_lower($email)); if ($orderNumber === '') { return null; } store_ensure_schema(); $sql = 'SELECT * FROM orders WHERE order_number = :order_number'; $params = ['order_number' => $orderNumber]; if ($email !== '') { $sql .= ' AND email = :email'; $params['email'] = $email; } $sql .= ' LIMIT 1'; $stmt = db()->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue(':' . $key, $value); } $stmt->execute(); $order = $stmt->fetch(); if (!$order) { return null; } $order['items'] = json_decode((string)$order['items_json'], true) ?: []; $paymentMethods = store_payment_methods(); $order['payment_method_label'] = $paymentMethods[$order['payment_method']]['label'] ?? $order['payment_method']; $order['payment_instruction'] = $paymentMethods[$order['payment_method']]['instruction'] ?? 'Tim admin akan menghubungi Anda untuk instruksi berikutnya.'; return $order; } function store_last_order_lookup(): array { $lookup = $_SESSION[STORE_LAST_ORDER_KEY] ?? []; return is_array($lookup) ? $lookup : []; } function store_safe_redirect(string $target, string $fallback = 'cart.php'): string { $target = trim($target); if ($target === '') { return $fallback; } $parts = parse_url($target); if ($parts === false || isset($parts['scheme']) || isset($parts['host'])) { return $fallback; } $path = $parts['path'] ?? ''; if ($path === '' || str_contains($path, '..')) { return $fallback; } $path = ltrim($path, '/'); if ($path === '') { $path = $fallback; } $query = isset($parts['query']) ? '?' . $parts['query'] : ''; return $path . $query; } function store_format_datetime(string $value): string { $timestamp = strtotime($value); if ($timestamp === false) { return $value; } $months = [ 1 => 'Jan', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mei', 6 => 'Jun', 7 => 'Jul', 8 => 'Agu', 9 => 'Sep', 10 => 'Okt', 11 => 'Nov', 12 => 'Des', ]; $month = $months[(int)date('n', $timestamp)] ?? date('M', $timestamp); return date('d', $timestamp) . ' ' . $month . ' ' . date('Y, H:i', $timestamp); } function store_nav_link(string $href, string $label, string $currentPath): string { $active = $currentPath === $href ? ' nav-link active' : ' nav-link'; return '