WebOrbiton
v1.0.0.0

StocketBase

193 lines · 10.4 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/site-front.php';
  6. require_once __DIR__ . '/currency.php';
  7. require_once __DIR__ . '/language.php';
  8. ​
  9. final class MailTemplates
  10. {
  11. public static function orderConfirmationEmail(array $order, array $items, array $downloadLinks = []): array
  12. {
  13. $siteName = (string) (SiteFront::settings()['site_name'] ?? 'Our store');
  14. $currency = (string) ($order['currency'] ?? Currency::DEFAULT_CODE);
  15. $orderNumber = (string) ($order['order_number'] ?? '');
  16. ​
  17. $rows = '';
  18. foreach ($items as $item) {
  19. $title = htmlspecialchars((string) ($item['product_title_snapshot'] ?? ''), ENT_QUOTES);
  20. $variant = (string) ($item['variant_name_snapshot'] ?? '');
  21. $qty = (int) ($item['quantity'] ?? 1);
  22. $lineTotal = Currency::format((int) ($item['total_cents'] ?? 0), $currency);
  23. ​
  24. $rows .= '<tr>'
  25. . '<td style="padding:10px 0;border-bottom:1px solid #e2e8f0;">' . $title
  26. . ($variant !== '' ? '<br><span style="color:#64748b;font-size:12.5px;">' . htmlspecialchars($variant, ENT_QUOTES) . '</span>' : '')
  27. . '</td>'
  28. . '<td style="padding:10px 0;border-bottom:1px solid #e2e8f0;text-align:center;">' . $qty . '</td>'
  29. . '<td style="padding:10px 0;border-bottom:1px solid #e2e8f0;text-align:right;">' . htmlspecialchars($lineTotal, ENT_QUOTES) . '</td>'
  30. . '</tr>';
  31. }
  32. ​
  33. $summaryRows = self::summaryRows($order, $currency);
  34. $shippingBlock = self::shippingAddressBlock($order);
  35. ​
  36. $downloadsBlock = '';
  37. if (!empty($downloadLinks)) {
  38. $downloadItems = '';
  39. foreach ($downloadLinks as $link) {
  40. $downloadItems .= '<li style="margin:0 0 6px;"><a href="' . htmlspecialchars((string) $link['url'], ENT_QUOTES) . '" style="color:#2563eb;font-weight:600;">'
  41. . htmlspecialchars((string) $link['name'], ENT_QUOTES) . '</a></li>';
  42. }
  43. $downloadsBlock = '<h3 style="margin:24px 0 8px;font-size:15px;color:#0f172a;">' . self::t('email_downloads_title', 'Your downloads') . '</h3>'
  44. . '<ul style="margin:0 0 8px;padding-left:18px;">' . $downloadItems . '</ul>'
  45. . '<p style="margin:0 0 16px;color:#64748b;font-size:12.5px;">' . self::t('email_downloads_note', 'These links are personal — please do not share them. They may expire or allow a limited number of downloads.') . '</p>';
  46. }
  47. ​
  48. $subject = self::fill(self::raw('email_order_subject', 'Order confirmation — {site} #{order}'), $siteName, $orderNumber);
  49. ​
  50. $html = self::layout(
  51. $siteName,
  52. self::raw('email_order_heading', 'Thanks for your order!'),
  53. '<p style="margin:0 0 16px;color:#334155;">' . self::fill(self::t('email_order_intro', 'Your order {order} has been received and is being processed.'), '', '<strong>#' . htmlspecialchars($orderNumber, ENT_QUOTES) . '</strong>') . '</p>'
  54. . '<table style="width:100%;border-collapse:collapse;margin:16px 0;">'
  55. . '<thead><tr>'
  56. . '<th style="text-align:left;padding:8px 0;border-bottom:2px solid #0f172a;color:#0f172a;">' . self::t('email_column_item', 'Item') . '</th>'
  57. . '<th style="text-align:center;padding:8px 0;border-bottom:2px solid #0f172a;color:#0f172a;">' . self::t('email_column_qty', 'Qty') . '</th>'
  58. . '<th style="text-align:right;padding:8px 0;border-bottom:2px solid #0f172a;color:#0f172a;">' . self::t('cart_total', 'Total') . '</th>'
  59. . '</tr></thead>'
  60. . '<tbody>' . $rows . '</tbody>'
  61. . '</table>'
  62. . '<table style="width:100%;border-collapse:collapse;margin:0 0 16px;">' . $summaryRows . '</table>'
  63. . $downloadsBlock
  64. . $shippingBlock
  65. );
  66. ​
  67. return ['subject' => $subject, 'html' => $html];
  68. }
  69. ​
  70. public static function orderStatusEmail(array $order, string $newStatus): array
  71. {
  72. $siteName = (string) (SiteFront::settings()['site_name'] ?? 'Our store');
  73. $orderNumber = (string) ($order['order_number'] ?? '');
  74. $statusLabel = self::statusLabel($newStatus);
  75. ​
  76. $subject = str_replace('{status}', $statusLabel, self::fill(self::raw('email_status_subject', 'Your order #{order} is now: {status}'), $siteName, $orderNumber));
  77. ​
  78. $trackingBlock = '';
  79. if ($newStatus === 'shipped' && !empty($order['tracking_number'])) {
  80. $trackingNumber = htmlspecialchars((string) $order['tracking_number'], ENT_QUOTES);
  81. $trackingUrl = (string) ($order['tracking_url'] ?? '');
  82. $trackingBlock = '<p style="margin:16px 0 0;color:#334155;">' . self::t('email_tracking_number', 'Tracking number:') . ' <strong>' . $trackingNumber . '</strong>'
  83. . ($trackingUrl !== '' ? '<br><a href="' . htmlspecialchars($trackingUrl, ENT_QUOTES) . '" style="color:#2563eb;">' . self::t('email_track_package', 'Track your package') . '</a>' : '')
  84. . '</p>';
  85. }
  86. ​
  87. $html = self::layout(
  88. $siteName,
  89. self::raw('email_status_heading', 'Order update'),
  90. '<p style="margin:0 0 16px;color:#334155;">' . self::fill(self::t('email_status_intro', 'The status of your order {order} changed to:'), '', '<strong>#' . htmlspecialchars($orderNumber, ENT_QUOTES) . '</strong>') . '</p>'
  91. . '<p style="margin:0 0 16px;"><span style="display:inline-block;padding:6px 14px;border-radius:999px;background:#eff6ff;color:#2563eb;font-weight:600;font-size:13.5px;">' . htmlspecialchars($statusLabel, ENT_QUOTES) . '</span></p>'
  92. . $trackingBlock
  93. );
  94. ​
  95. return ['subject' => $subject, 'html' => $html];
  96. }
  97. ​
  98. private static function statusLabel(string $status): string
  99. {
  100. return self::raw('order_status_' . $status, ucfirst($status));
  101. }
  102. ​
  103. private static function raw(string $key, string $default): string
  104. {
  105. return class_exists('Language') ? Language::get($key, $default) : $default;
  106. }
  107. ​
  108. private static function t(string $key, string $default): string
  109. {
  110. return htmlspecialchars(self::raw($key, $default), ENT_QUOTES);
  111. }
  112. ​
  113. private static function fill(string $template, string $siteName, string $orderNumber): string
  114. {
  115. return str_replace(['{site}', '{order}'], [$siteName, $orderNumber], $template);
  116. }
  117. ​
  118. private static function summaryRows(array $order, string $currency): string
  119. {
  120. $rows = '';
  121. $line = static function (string $label, int $cents, string $currency) use (&$rows, $order): void {
  122. $rows .= '<tr>'
  123. . '<td style="padding:4px 0;color:#64748b;">' . htmlspecialchars($label, ENT_QUOTES) . '</td>'
  124. . '<td style="padding:4px 0;text-align:right;color:#334155;">' . htmlspecialchars(Currency::format($cents, $currency), ENT_QUOTES) . '</td>'
  125. . '</tr>';
  126. };
  127. ​
  128. $line(self::raw('cart_subtotal', 'Subtotal'), (int) ($order['subtotal_cents'] ?? 0), $currency);
  129. if ((int) ($order['discount_cents'] ?? 0) > 0) {
  130. $line(self::raw('cart_discount', 'Discount'), -(int) $order['discount_cents'], $currency);
  131. }
  132. if ((int) ($order['shipping_cents'] ?? 0) > 0) {
  133. $line(self::raw('order_shipping_label', 'Shipping'), (int) $order['shipping_cents'], $currency);
  134. }
  135. if ((int) ($order['tax_cents'] ?? 0) > 0) {
  136. $line(self::raw('email_tax', 'Tax'), (int) $order['tax_cents'], $currency);
  137. }
  138. ​
  139. $rows .= '<tr>'
  140. . '<td style="padding:10px 0 0;border-top:1px solid #e2e8f0;font-weight:700;color:#0f172a;">' . self::t('cart_total', 'Total') . '</td>'
  141. . '<td style="padding:10px 0 0;border-top:1px solid #e2e8f0;text-align:right;font-weight:700;color:#0f172a;">' . htmlspecialchars(Currency::format((int) ($order['total_cents'] ?? 0), $currency), ENT_QUOTES) . '</td>'
  142. . '</tr>';
  143. ​
  144. return $rows;
  145. }
  146. ​
  147. private static function shippingAddressBlock(array $order): string
  148. {
  149. $name = trim((string) ($order['shipping_name'] ?? ''));
  150. if ($name === '') {
  151. return '';
  152. }
  153. ​
  154. $lines = array_filter([
  155. $name,
  156. (string) ($order['shipping_address_line1'] ?? ''),
  157. (string) ($order['shipping_address_line2'] ?? ''),
  158. trim(((string) ($order['shipping_city'] ?? '')) . ' ' . ((string) ($order['shipping_postal_code'] ?? ''))),
  159. (string) ($order['shipping_region'] ?? ''),
  160. (string) ($order['shipping_country'] ?? ''),
  161. ], static fn (string $line): bool => trim($line) !== '');
  162. ​
  163. return '<h3 style="margin:20px 0 8px;color:#0f172a;font-size:14px;">' . self::t('email_shipping_to', 'Shipping to') . '</h3>'
  164. . '<p style="margin:0;color:#334155;line-height:1.6;">' . implode('<br>', array_map(static fn (string $line): string => htmlspecialchars($line, ENT_QUOTES), $lines)) . '</p>';
  165. }
  166. ​
  167. private static function layout(string $siteName, string $heading, string $bodyHtml): string
  168. {
  169. $safeSiteName = htmlspecialchars($siteName, ENT_QUOTES);
  170. $safeHeading = htmlspecialchars($heading, ENT_QUOTES);
  171. ​
  172. return '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head>'
  173. . '<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;">'
  174. . '<table role="presentation" style="width:100%;border-collapse:collapse;background:#f1f5f9;padding:24px 0;">'
  175. . '<tr><td align="center">'
  176. . '<table role="presentation" style="width:100%;max-width:560px;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e2e8f0;">'
  177. . '<tr><td style="padding:24px 32px;background:#0f172a;">'
  178. . '<span style="color:#f8fafc;font-size:18px;font-weight:700;">' . $safeSiteName . '</span>'
  179. . '</td></tr>'
  180. . '<tr><td style="padding:32px;">'
  181. . '<h1 style="margin:0 0 16px;font-size:20px;color:#0f172a;">' . $safeHeading . '</h1>'
  182. . $bodyHtml
  183. . '</td></tr>'
  184. . '<tr><td style="padding:20px 32px;background:#f8fafc;border-top:1px solid #e2e8f0;">'
  185. . '<span style="color:#94a3b8;font-size:12px;">' . $safeSiteName . '</span>'
  186. . '</td></tr>'
  187. . '</table>'
  188. . '</td></tr>'
  189. . '</table>'
  190. . '</body></html>';
  191. }
  192. }
  193. ​