functions.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. <?php
  2. require_once __DIR__ . '/../config.php';
  3. /**
  4. * Read JSON file and return decoded data
  5. */
  6. function readJsonFile($file) {
  7. if (!file_exists($file)) {
  8. return [];
  9. }
  10. $content = file_get_contents($file);
  11. if (empty($content)) {
  12. return [];
  13. }
  14. $data = json_decode($content, true);
  15. return $data ? $data : [];
  16. }
  17. /**
  18. * Write data to JSON file
  19. */
  20. function writeJsonFile($file, $data) {
  21. $dir = dirname($file);
  22. if (!is_dir($dir)) {
  23. mkdir($dir, 0755, true);
  24. }
  25. file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
  26. }
  27. /**
  28. * Get all products
  29. */
  30. function getProducts() {
  31. $data = readJsonFile(PRODUCTS_FILE);
  32. return isset($data['products']) ? $data['products'] : [];
  33. }
  34. /**
  35. * Get product by ID
  36. */
  37. function getProductById($id) {
  38. $products = getProducts();
  39. foreach ($products as $product) {
  40. if ($product['id'] == $id) {
  41. return $product;
  42. }
  43. }
  44. return null;
  45. }
  46. /**
  47. * Save products
  48. */
  49. function saveProducts($products) {
  50. $data = ['products' => $products];
  51. writeJsonFile(PRODUCTS_FILE, $data);
  52. }
  53. /**
  54. * Get all reservations
  55. */
  56. function getReservations() {
  57. $data = readJsonFile(RESERVATIONS_FILE);
  58. return isset($data['reservations']) ? $data['reservations'] : [];
  59. }
  60. /**
  61. * Get reservation by order number
  62. */
  63. function getReservationByOrderNumber($orderNumber) {
  64. $reservations = getReservations();
  65. foreach ($reservations as $reservation) {
  66. if (isset($reservation['id']) && $reservation['id'] === $orderNumber && !isReservationHidden($reservation)) {
  67. return $reservation;
  68. }
  69. }
  70. return null;
  71. }
  72. /**
  73. * Get remembered order IDs for the current browser profile.
  74. */
  75. function getRememberedOrderIds(): array {
  76. return readSignedOrderHistoryCookie();
  77. }
  78. /**
  79. * Remember a newly created order ID in browser history cookie.
  80. */
  81. function rememberOrderId(string $orderId): void {
  82. if (!isValidOrderHistoryOrderId($orderId)) {
  83. return;
  84. }
  85. $existingIds = getRememberedOrderIds();
  86. $updatedIds = [$orderId];
  87. foreach ($existingIds as $existingId) {
  88. if ($existingId !== $orderId) {
  89. $updatedIds[] = $existingId;
  90. }
  91. }
  92. $maxIds = getOrderHistoryMaxIds();
  93. if (count($updatedIds) > $maxIds) {
  94. $updatedIds = array_slice($updatedIds, 0, $maxIds);
  95. }
  96. writeSignedOrderHistoryCookie($updatedIds);
  97. }
  98. /**
  99. * Read and validate signed browser order history cookie.
  100. */
  101. function readSignedOrderHistoryCookie(): array {
  102. $cookieName = getOrderHistoryCookieName();
  103. if (!isset($_COOKIE[$cookieName]) || !is_string($_COOKIE[$cookieName])) {
  104. return [];
  105. }
  106. $secret = getOrderHistorySecret();
  107. if ($secret === '') {
  108. return [];
  109. }
  110. $cookieValue = $_COOKIE[$cookieName];
  111. $parts = explode('.', $cookieValue, 2);
  112. if (count($parts) !== 2) {
  113. return [];
  114. }
  115. $encodedPayload = $parts[0];
  116. $signature = $parts[1];
  117. if ($encodedPayload === '' || $signature === '') {
  118. return [];
  119. }
  120. $expectedSignature = hash_hmac('sha256', $encodedPayload, $secret);
  121. if (!hash_equals($expectedSignature, $signature)) {
  122. return [];
  123. }
  124. $payloadJson = base64UrlDecode($encodedPayload);
  125. if ($payloadJson === null) {
  126. return [];
  127. }
  128. $payload = json_decode($payloadJson, true);
  129. if (!is_array($payload)) {
  130. return [];
  131. }
  132. $version = isset($payload['v']) ? (int)$payload['v'] : 0;
  133. if ($version !== 1) {
  134. return [];
  135. }
  136. $ids = isset($payload['ids']) && is_array($payload['ids']) ? $payload['ids'] : [];
  137. return sanitizeOrderHistoryIds($ids);
  138. }
  139. /**
  140. * Write signed browser order history cookie.
  141. */
  142. function writeSignedOrderHistoryCookie(array $ids): void {
  143. if (headers_sent()) {
  144. return;
  145. }
  146. $secret = getOrderHistorySecret();
  147. if ($secret === '') {
  148. return;
  149. }
  150. $sanitizedIds = sanitizeOrderHistoryIds($ids);
  151. $payload = [
  152. 'v' => 1,
  153. 'ids' => $sanitizedIds,
  154. 'iat' => time()
  155. ];
  156. $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
  157. if ($payloadJson === false) {
  158. return;
  159. }
  160. $encodedPayload = base64UrlEncode($payloadJson);
  161. $signature = hash_hmac('sha256', $encodedPayload, $secret);
  162. $cookieValue = $encodedPayload . '.' . $signature;
  163. $expires = time() + (getOrderHistoryTtlDays() * 86400);
  164. $success = setcookie(getOrderHistoryCookieName(), $cookieValue, [
  165. 'expires' => $expires,
  166. 'path' => getOrderHistoryCookiePath(),
  167. 'secure' => isHttpsRequest(),
  168. 'httponly' => true,
  169. 'samesite' => 'Lax'
  170. ]);
  171. if ($success) {
  172. $_COOKIE[getOrderHistoryCookieName()] = $cookieValue;
  173. }
  174. }
  175. /**
  176. * Build a safe, deduplicated order history ID list.
  177. */
  178. function sanitizeOrderHistoryIds(array $ids): array {
  179. $result = [];
  180. $seen = [];
  181. $maxIds = getOrderHistoryMaxIds();
  182. foreach ($ids as $id) {
  183. if (!is_string($id) || !isValidOrderHistoryOrderId($id)) {
  184. continue;
  185. }
  186. if (isset($seen[$id])) {
  187. continue;
  188. }
  189. $seen[$id] = true;
  190. $result[] = $id;
  191. if (count($result) >= $maxIds) {
  192. break;
  193. }
  194. }
  195. return $result;
  196. }
  197. /**
  198. * Check if order ID matches configured pattern.
  199. */
  200. function isValidOrderHistoryOrderId($orderId): bool {
  201. if (!is_string($orderId) || $orderId === '') {
  202. return false;
  203. }
  204. $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
  205. $pattern = '/^' . preg_quote($prefix, '/') . '-\d{4}-\d+$/';
  206. return preg_match($pattern, $orderId) === 1;
  207. }
  208. /**
  209. * Base64url encode helper.
  210. */
  211. function base64UrlEncode(string $data): string {
  212. return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
  213. }
  214. /**
  215. * Base64url decode helper.
  216. */
  217. function base64UrlDecode(string $data): ?string {
  218. $remainder = strlen($data) % 4;
  219. if ($remainder > 0) {
  220. $data .= str_repeat('=', 4 - $remainder);
  221. }
  222. $decoded = base64_decode(strtr($data, '-_', '+/'), true);
  223. if ($decoded === false) {
  224. return null;
  225. }
  226. return $decoded;
  227. }
  228. /**
  229. * Get cookie name for order history.
  230. */
  231. function getOrderHistoryCookieName(): string {
  232. return defined('ORDER_HISTORY_COOKIE_NAME') ? ORDER_HISTORY_COOKIE_NAME : 'order_history';
  233. }
  234. /**
  235. * Get secret for order history cookie signing.
  236. */
  237. function getOrderHistorySecret(): string {
  238. return defined('ORDER_HISTORY_COOKIE_SECRET') ? (string) ORDER_HISTORY_COOKIE_SECRET : '';
  239. }
  240. /**
  241. * Get maximum number of remembered order IDs.
  242. */
  243. function getOrderHistoryMaxIds(): int {
  244. $maxIds = defined('ORDER_HISTORY_MAX_IDS') ? (int) ORDER_HISTORY_MAX_IDS : 10;
  245. return $maxIds > 0 ? $maxIds : 10;
  246. }
  247. /**
  248. * Get order history cookie retention days.
  249. */
  250. function getOrderHistoryTtlDays(): int {
  251. $ttlDays = defined('ORDER_HISTORY_COOKIE_TTL_DAYS') ? (int) ORDER_HISTORY_COOKIE_TTL_DAYS : 365;
  252. return $ttlDays > 0 ? $ttlDays : 365;
  253. }
  254. /**
  255. * Get cookie path from SITE_URL.
  256. */
  257. function getOrderHistoryCookiePath(): string {
  258. $siteUrl = defined('SITE_URL') ? trim((string) SITE_URL) : '';
  259. if (strpos($siteUrl, '://') !== false) {
  260. $parsedPath = parse_url($siteUrl, PHP_URL_PATH);
  261. $siteUrl = is_string($parsedPath) ? $parsedPath : '';
  262. }
  263. if ($siteUrl === '' || $siteUrl === '/') {
  264. return '/';
  265. }
  266. if ($siteUrl[0] !== '/') {
  267. $siteUrl = '/' . $siteUrl;
  268. }
  269. return rtrim($siteUrl, '/');
  270. }
  271. /**
  272. * Detect whether current request uses HTTPS.
  273. */
  274. function isHttpsRequest(): bool {
  275. if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
  276. return true;
  277. }
  278. if (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) {
  279. return true;
  280. }
  281. if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
  282. return true;
  283. }
  284. return false;
  285. }
  286. /**
  287. * Check if reservation is hidden (spam/deleted)
  288. */
  289. function isReservationHidden($reservation) {
  290. return isset($reservation['is_hidden']) && $reservation['is_hidden'] === true;
  291. }
  292. /**
  293. * Save reservations
  294. */
  295. function saveReservations($reservations) {
  296. $data = ['reservations' => $reservations];
  297. writeJsonFile(RESERVATIONS_FILE, $data);
  298. }
  299. /**
  300. * Generate order number
  301. * Pattern: PREFIX-YEAR-SEQ
  302. */
  303. function generateReservationId() {
  304. $reservations = getReservations();
  305. $year = date('Y');
  306. $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
  307. $max = 0;
  308. $pattern = '/^' . preg_quote($prefix, '/') . '-\\d{4}-(\\d+)$/';
  309. foreach ($reservations as $reservation) {
  310. if (!isset($reservation['id'])) {
  311. continue;
  312. }
  313. if (preg_match($pattern, $reservation['id'], $matches)) {
  314. $num = (int)$matches[1];
  315. if ($num > $max) {
  316. $max = $num;
  317. }
  318. }
  319. }
  320. $next = $max + 1;
  321. return sprintf('%s-%s-%03d', $prefix, $year, $next);
  322. }
  323. /**
  324. * Check if product has enough stock
  325. * For apparel: checks stock for specific size
  326. * For merch: checks general stock
  327. */
  328. function checkStock($productId, $quantity, $size = null) {
  329. $product = getProductById($productId);
  330. if (!$product) {
  331. return false;
  332. }
  333. // For apparel with sizes, check stock per size
  334. if ($product['category'] === 'apparel' && !empty($product['sizes']) && $size !== null) {
  335. if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
  336. return false;
  337. }
  338. $sizeStock = isset($product['stock_by_size'][$size]) ? (int)$product['stock_by_size'][$size] : 0;
  339. return $sizeStock >= $quantity;
  340. }
  341. // For merch or apparel without size-specific stock, use general stock
  342. $stock = isset($product['stock']) ? (int)$product['stock'] : 0;
  343. return $stock >= $quantity;
  344. }
  345. /**
  346. * Get stock for a product (per size for apparel, general for merch)
  347. */
  348. function getStock($product, $size = null) {
  349. if ($product['category'] === 'apparel' && !empty($product['sizes']) && $size !== null) {
  350. if (isset($product['stock_by_size']) && is_array($product['stock_by_size'])) {
  351. return isset($product['stock_by_size'][$size]) ? (int)$product['stock_by_size'][$size] : 0;
  352. }
  353. }
  354. return isset($product['stock']) ? (int)$product['stock'] : 0;
  355. }
  356. /**
  357. * Get total stock for a product (sum of all sizes for apparel)
  358. */
  359. function getTotalStock($product) {
  360. if ($product['category'] === 'apparel' && isset($product['stock_by_size']) && is_array($product['stock_by_size'])) {
  361. return array_sum($product['stock_by_size']);
  362. }
  363. return isset($product['stock']) ? (int)$product['stock'] : 0;
  364. }
  365. /**
  366. * Allocate stock for reservation
  367. */
  368. function allocateStock($productId, $quantity, $size = null) {
  369. $products = getProducts();
  370. foreach ($products as &$product) {
  371. if ($product['id'] == $productId) {
  372. // For apparel with sizes, allocate per size
  373. if ($product['category'] === 'apparel' && !empty($product['sizes']) && $size !== null) {
  374. if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
  375. $product['stock_by_size'] = [];
  376. }
  377. if (!isset($product['stock_by_size'][$size])) {
  378. $product['stock_by_size'][$size] = 0;
  379. }
  380. $product['stock_by_size'][$size] -= $quantity;
  381. if ($product['stock_by_size'][$size] < 0) {
  382. $product['stock_by_size'][$size] = 0;
  383. }
  384. } else {
  385. // For merch or general stock
  386. if (!isset($product['stock'])) {
  387. $product['stock'] = 0;
  388. }
  389. $product['stock'] -= $quantity;
  390. if ($product['stock'] < 0) {
  391. $product['stock'] = 0;
  392. }
  393. }
  394. break;
  395. }
  396. }
  397. saveProducts($products);
  398. }
  399. /**
  400. * Release stock from reservation
  401. */
  402. function releaseStock($productId, $quantity, $size = null) {
  403. $products = getProducts();
  404. foreach ($products as &$product) {
  405. if ($product['id'] == $productId) {
  406. // For apparel with sizes, release per size
  407. if ($product['category'] === 'apparel' && !empty($product['sizes']) && $size !== null) {
  408. if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
  409. $product['stock_by_size'] = [];
  410. }
  411. if (!isset($product['stock_by_size'][$size])) {
  412. $product['stock_by_size'][$size] = 0;
  413. }
  414. $product['stock_by_size'][$size] += $quantity;
  415. } else {
  416. // For merch or general stock
  417. if (!isset($product['stock'])) {
  418. $product['stock'] = 0;
  419. }
  420. $product['stock'] += $quantity;
  421. }
  422. break;
  423. }
  424. }
  425. saveProducts($products);
  426. }
  427. /**
  428. * Create new reservation
  429. */
  430. function createReservation($customerName, $customerEmail, $items) {
  431. $reservations = getReservations();
  432. // Validate stock for all items
  433. foreach ($items as $item) {
  434. $size = isset($item['size']) ? $item['size'] : null;
  435. if (!checkStock($item['product_id'], $item['quantity'], $size)) {
  436. $product = getProductById($item['product_id']);
  437. $productName = $product ? $product['name'] : 'Produkt';
  438. $sizeInfo = $size ? " (Größe: $size)" : '';
  439. return ['success' => false, 'message' => "Nicht genügend Lagerbestand für: $productName$sizeInfo"];
  440. }
  441. }
  442. // Allocate stock
  443. foreach ($items as $item) {
  444. $size = isset($item['size']) ? $item['size'] : null;
  445. allocateStock($item['product_id'], $item['quantity'], $size);
  446. }
  447. // Create reservation
  448. $now = new DateTime();
  449. $expires = clone $now;
  450. $expires->modify('+' . RESERVATION_EXPIRY_DAYS . ' days');
  451. $reservation = [
  452. 'id' => generateReservationId(),
  453. 'customer_name' => $customerName,
  454. 'customer_email' => $customerEmail,
  455. 'items' => $items,
  456. 'created' => $now->format('Y-m-d H:i:s'),
  457. 'expires' => $expires->format('Y-m-d H:i:s'),
  458. 'status' => 'open',
  459. 'picked_up' => false,
  460. 'type' => 'regular',
  461. 'is_hidden' => false
  462. ];
  463. $reservations[] = $reservation;
  464. saveReservations($reservations);
  465. // Send confirmation emails
  466. sendReservationEmails($reservation);
  467. return ['success' => true, 'reservation' => $reservation];
  468. }
  469. /**
  470. * Create new backorder reservation
  471. */
  472. function createBackorderReservation($customerName, $customerEmail, $items) {
  473. $reservations = getReservations();
  474. $now = new DateTime();
  475. $reservation = [
  476. 'id' => generateReservationId(),
  477. 'customer_name' => $customerName,
  478. 'customer_email' => $customerEmail,
  479. 'items' => $items,
  480. 'created' => $now->format('Y-m-d H:i:s'),
  481. 'expires' => '',
  482. 'status' => 'open',
  483. 'picked_up' => false,
  484. 'type' => 'backorder',
  485. 'backorder_status' => 'pending',
  486. 'is_hidden' => false
  487. ];
  488. $reservations[] = $reservation;
  489. saveReservations($reservations);
  490. // Send confirmation emails
  491. sendBackorderEmails($reservation);
  492. return ['success' => true, 'reservation' => $reservation];
  493. }
  494. /**
  495. * Mark reservation as picked up
  496. */
  497. function markReservationPickedUp($reservationId) {
  498. $reservations = getReservations();
  499. foreach ($reservations as &$reservation) {
  500. if ($reservation['id'] === $reservationId) {
  501. if (isReservationHidden($reservation)) {
  502. break;
  503. }
  504. $reservation['picked_up'] = true;
  505. $reservation['status'] = 'picked_up';
  506. break;
  507. }
  508. }
  509. saveReservations($reservations);
  510. }
  511. /**
  512. * Mark reservation/backorder as spam/deleted and hide it from non-admin views.
  513. * For open regular reservations we release stock, because the order is discarded.
  514. */
  515. function markReservationHidden($reservationId) {
  516. $reservations = getReservations();
  517. foreach ($reservations as &$reservation) {
  518. if ($reservation['id'] !== $reservationId) {
  519. continue;
  520. }
  521. if (isReservationHidden($reservation)) {
  522. return ['success' => false, 'message' => 'Bestellung ist bereits als Spam/Gelöscht markiert.'];
  523. }
  524. $isBackorder = isset($reservation['type']) && $reservation['type'] === 'backorder';
  525. if (!$isBackorder && isset($reservation['status']) && $reservation['status'] === 'open' && empty($reservation['picked_up'])) {
  526. foreach ($reservation['items'] as $item) {
  527. $size = isset($item['size']) ? $item['size'] : null;
  528. releaseStock($item['product_id'], $item['quantity'], $size);
  529. }
  530. $reservation['status'] = 'deleted';
  531. }
  532. $reservation['is_hidden'] = true;
  533. $reservation['hidden_at'] = date('Y-m-d H:i:s');
  534. $reservation['hidden_reason'] = 'spam_deleted';
  535. saveReservations($reservations);
  536. return ['success' => true];
  537. }
  538. return ['success' => false, 'message' => 'Bestellung nicht gefunden.'];
  539. }
  540. /**
  541. * Check and expire old reservations
  542. */
  543. function expireOldReservations() {
  544. $reservations = getReservations();
  545. $now = new DateTime();
  546. $changed = false;
  547. foreach ($reservations as &$reservation) {
  548. if (isReservationHidden($reservation)) {
  549. continue;
  550. }
  551. if ($reservation['status'] === 'open' && !$reservation['picked_up']) {
  552. if (isset($reservation['type']) && $reservation['type'] === 'backorder') {
  553. continue;
  554. }
  555. if (empty($reservation['expires'])) {
  556. continue;
  557. }
  558. $expires = new DateTime($reservation['expires']);
  559. if ($now > $expires) {
  560. $reservation['status'] = 'expired';
  561. // Release stock
  562. foreach ($reservation['items'] as $item) {
  563. $size = isset($item['size']) ? $item['size'] : null;
  564. releaseStock($item['product_id'], $item['quantity'], $size);
  565. }
  566. $changed = true;
  567. }
  568. }
  569. }
  570. if ($changed) {
  571. saveReservations($reservations);
  572. }
  573. }
  574. /**
  575. * Check if all items are in stock
  576. */
  577. function canFulfillReservationItems($items) {
  578. foreach ($items as $item) {
  579. $size = isset($item['size']) ? $item['size'] : null;
  580. if (!checkStock($item['product_id'], $item['quantity'], $size)) {
  581. return false;
  582. }
  583. }
  584. return true;
  585. }
  586. /**
  587. * Mark backorder as available
  588. */
  589. function markBackorderAvailable($reservationId) {
  590. $reservations = getReservations();
  591. foreach ($reservations as &$reservation) {
  592. if ($reservation['id'] === $reservationId) {
  593. if (isReservationHidden($reservation)) {
  594. return ['success' => false, 'message' => 'Diese Vorbestellung ist als Spam/Gelöscht markiert.'];
  595. }
  596. if (!isset($reservation['type']) || $reservation['type'] !== 'backorder') {
  597. return ['success' => false, 'message' => 'Diese Vorbestellung wurde bereits in eine Bestellung umgewandelt.'];
  598. }
  599. if (isset($reservation['backorder_status']) && $reservation['backorder_status'] === 'notified') {
  600. return ['success' => false, 'message' => 'Diese Vorbestellung wurde bereits informiert.'];
  601. }
  602. if (!canFulfillReservationItems($reservation['items'])) {
  603. return ['success' => false, 'message' => 'Nicht alle Artikel sind verfügbar.'];
  604. }
  605. foreach ($reservation['items'] as $item) {
  606. $size = isset($item['size']) ? $item['size'] : null;
  607. allocateStock($item['product_id'], $item['quantity'], $size);
  608. }
  609. $now = new DateTime();
  610. $expires = clone $now;
  611. $expires->modify('+' . RESERVATION_EXPIRY_DAYS . ' days');
  612. $reservation['type'] = 'regular';
  613. $reservation['status'] = 'open';
  614. $reservation['picked_up'] = false;
  615. $reservation['expires'] = $expires->format('Y-m-d H:i:s');
  616. if (isset($reservation['backorder_status'])) {
  617. unset($reservation['backorder_status']);
  618. }
  619. saveReservations($reservations);
  620. sendBackorderAvailableEmail($reservation);
  621. return ['success' => true, 'reservation' => $reservation];
  622. }
  623. }
  624. return ['success' => false, 'message' => 'Vorbestellung nicht gefunden.'];
  625. }
  626. /**
  627. * Sanitize input
  628. */
  629. function sanitize($input) {
  630. return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
  631. }
  632. /**
  633. * Format price
  634. */
  635. function formatPrice($price) {
  636. return number_format($price, 2, ',', '.') . ' €';
  637. }
  638. /**
  639. * Format date
  640. */
  641. function formatDate($dateString) {
  642. $date = new DateTime($dateString);
  643. return $date->format('d.m.Y H:i');
  644. }
  645. /**
  646. * Send email
  647. */
  648. function sendEmail($to, $subject, $message, $isHtml = true) {
  649. $headers = [];
  650. $headers[] = 'From: ' . FROM_NAME . ' <' . FROM_EMAIL . '>';
  651. $headers[] = 'Reply-To: ' . FROM_EMAIL;
  652. $headers[] = 'X-Mailer: PHP/' . phpversion();
  653. if ($isHtml) {
  654. $headers[] = 'MIME-Version: 1.0';
  655. $headers[] = 'Content-type: text/html; charset=UTF-8';
  656. }
  657. return mail($to, $subject, $message, implode("\r\n", $headers));
  658. }
  659. /**
  660. * Send reservation confirmation emails
  661. */
  662. function sendReservationEmails($reservation) {
  663. $products = getProducts();
  664. // Build items list
  665. $itemsHtml = '<ul style="list-style: none; margin: 0; padding: 0;">';
  666. foreach ($reservation['items'] as $item) {
  667. $product = getProductById($item['product_id']);
  668. if ($product) {
  669. $sizeInfo = '';
  670. if (isset($item['size']) && !empty($item['size'])) {
  671. $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
  672. }
  673. $itemsHtml .= '<li style="margin: 0 0 0.6rem 0; padding: 0.75rem 0.9rem; background: #28292a; border: 1px solid #4a5263; border-radius: 6px;"><strong style="color: #cac300;">' . htmlspecialchars($product['name']) . '</strong>' . $sizeInfo . ' - Menge: <strong>' . (int) $item['quantity'] . '</strong></li>';
  674. }
  675. }
  676. $itemsHtml .= '</ul>';
  677. // Customer email
  678. $customerSubject = 'Ihre Reservierung bei ' . SITE_NAME;
  679. $customerMessage = '
  680. <html>
  681. <head>
  682. <meta charset="UTF-8">
  683. </head>
  684. <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
  685. <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
  686. <h2 style="color: #cac300; margin-top: 0;">Reservierung bestätigt</h2>
  687. <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
  688. <p>vielen Dank für Ihre Reservierung bei ' . SITE_NAME . '.</p>
  689. <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
  690. <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
  691. <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
  692. </div>
  693. <h3>Reservierungsdetails:</h3>
  694. <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
  695. <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
  696. <p><strong>Gültig bis:</strong> ' . formatDate($reservation['expires']) . '</p>
  697. <h3>Reservierte Artikel:</h3>
  698. <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
  699. <p><strong>Wichtig:</strong> Bitte nennen Sie diese Bestellnummer bei der Abholung. Die Reservierung ist bis zum ' . formatDate($reservation['expires']) . ' gültig.</p>
  700. <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
  701. </div>
  702. </body>
  703. </html>';
  704. sendEmail($reservation['customer_email'], $customerSubject, $customerMessage);
  705. // Admin email
  706. $adminSubject = 'Neue Reservierung: ' . $reservation['id'];
  707. $adminMessage = '
  708. <html>
  709. <head>
  710. <meta charset="UTF-8">
  711. </head>
  712. <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
  713. <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
  714. <h2 style="color: #cac300; margin-top: 0;">Neue Reservierung</h2>
  715. <p>Eine neue Reservierung wurde erstellt:</p>
  716. <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
  717. <h3 style="margin-top: 0;">Bestellnummer:</h3>
  718. <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
  719. </div>
  720. <h3>Kundendaten:</h3>
  721. <p><strong>Name:</strong> ' . htmlspecialchars($reservation['customer_name']) . '</p>
  722. <p><strong>E-Mail:</strong> ' . htmlspecialchars($reservation['customer_email']) . '</p>
  723. <h3>Reservierungsdetails:</h3>
  724. <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
  725. <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
  726. <p><strong>Gültig bis:</strong> ' . formatDate($reservation['expires']) . '</p>
  727. <h3>Reservierte Artikel:</h3>
  728. <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
  729. </div>
  730. </body>
  731. </html>';
  732. sendEmail(ADMIN_EMAIL, $adminSubject, $adminMessage);
  733. }
  734. /**
  735. * Send backorder confirmation emails
  736. */
  737. function sendBackorderEmails($reservation) {
  738. // Build items list
  739. $itemsHtml = '<ul style="list-style: none; margin: 0; padding: 0;">';
  740. foreach ($reservation['items'] as $item) {
  741. $product = getProductById($item['product_id']);
  742. if ($product) {
  743. $sizeInfo = '';
  744. if (isset($item['size']) && !empty($item['size'])) {
  745. $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
  746. }
  747. $itemsHtml .= '<li style="margin: 0 0 0.6rem 0; padding: 0.75rem 0.9rem; background: #28292a; border: 1px solid #4a5263; border-radius: 6px;"><strong style="color: #cac300;">' . htmlspecialchars($product['name']) . '</strong>' . $sizeInfo . ' - Menge: <strong>' . (int) $item['quantity'] . '</strong></li>';
  748. }
  749. }
  750. $itemsHtml .= '</ul>';
  751. // Customer email
  752. $customerSubject = 'Vorbestellung bei ' . SITE_NAME;
  753. $customerMessage = '
  754. <html>
  755. <head>
  756. <meta charset="UTF-8">
  757. </head>
  758. <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
  759. <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
  760. <h2 style="color: #cac300; margin-top: 0;">Vorbestellung bestätigt</h2>
  761. <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
  762. <p>vielen Dank für Ihre Vorbestellung bei ' . SITE_NAME . '.</p>
  763. <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
  764. <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
  765. <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
  766. </div>
  767. <h3>Vorbestellungsdetails:</h3>
  768. <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
  769. <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
  770. <h3>Vorbestellte Artikel:</h3>
  771. <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
  772. <div style="background: #28292a; border: 2px solid #cf2e2e; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
  773. <strong>Hinweis:</strong> Die Lieferzeiten sind nicht bekannt, da die Bestellung in Chargen erfolgt.
  774. </div>
  775. <p>Wir informieren Sie, sobald die komplette Vorbestellung zur Abholung bereit ist.</p>
  776. <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
  777. </div>
  778. </body>
  779. </html>';
  780. sendEmail($reservation['customer_email'], $customerSubject, $customerMessage);
  781. // Admin email
  782. $adminSubject = 'Neue Vorbestellung: ' . $reservation['id'];
  783. $adminMessage = '
  784. <html>
  785. <head>
  786. <meta charset="UTF-8">
  787. </head>
  788. <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
  789. <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
  790. <h2 style="color: #cac300; margin-top: 0;">Neue Vorbestellung</h2>
  791. <p>Eine neue Vorbestellung wurde erstellt:</p>
  792. <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
  793. <h3 style="margin-top: 0;">Bestellnummer:</h3>
  794. <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
  795. </div>
  796. <h3>Kundendaten:</h3>
  797. <p><strong>Name:</strong> ' . htmlspecialchars($reservation['customer_name']) . '</p>
  798. <p><strong>E-Mail:</strong> ' . htmlspecialchars($reservation['customer_email']) . '</p>
  799. <h3>Vorbestellungsdetails:</h3>
  800. <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
  801. <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
  802. <h3>Vorbestellte Artikel:</h3>
  803. <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
  804. </div>
  805. </body>
  806. </html>';
  807. sendEmail(ADMIN_EMAIL, $adminSubject, $adminMessage);
  808. }
  809. /**
  810. * Send backorder availability email
  811. */
  812. function sendBackorderAvailableEmail($reservation) {
  813. $itemsHtml = '<ul>';
  814. foreach ($reservation['items'] as $item) {
  815. $product = getProductById($item['product_id']);
  816. if ($product) {
  817. $sizeInfo = '';
  818. if (isset($item['size']) && !empty($item['size'])) {
  819. $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
  820. }
  821. $itemsHtml .= '<li>' . htmlspecialchars($product['name']) . $sizeInfo . ' - Menge: ' . $item['quantity'] . '</li>';
  822. }
  823. }
  824. $itemsHtml .= '</ul>';
  825. $subject = 'Ihre Vorbestellung ist zur Abholung bereit';
  826. $message = '
  827. <html>
  828. <head>
  829. <meta charset="UTF-8">
  830. </head>
  831. <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
  832. <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
  833. <h2 style="color: #cac300; margin-top: 0;">Vorbestellung zur Abholung bereit</h2>
  834. <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
  835. <p>Ihre komplette Vorbestellung ist jetzt zur Abholung bereit.</p>
  836. <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
  837. <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
  838. <h2 style="font-size: 2rem; letter-spacing: 0.2rem; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</h2>
  839. </div>
  840. <h3>Bereitliegende Artikel:</h3>
  841. ' . $itemsHtml . '
  842. <p>Bitte nennen Sie die Bestellnummer bei der Abholung.</p>
  843. <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
  844. </div>
  845. </body>
  846. </html>';
  847. sendEmail($reservation['customer_email'], $subject, $message);
  848. }