v1.0.0.0
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/site-front.php';
- require_once __DIR__ . '/currency.php';
- require_once __DIR__ . '/language.php';
-
- final class MailTemplates
- {
- public static function orderConfirmationEmail(array $order, array $items, array $downloadLinks = []): array
- {
- $siteName = (string) (SiteFront::settings()['site_name'] ?? 'Our store');
- $currency = (string) ($order['currency'] ?? Currency::DEFAULT_CODE);
- $orderNumber = (string) ($order['order_number'] ?? '');
-
- $rows = '';
- foreach ($items as $item) {
- $title = htmlspecialchars((string) ($item['product_title_snapshot'] ?? ''), ENT_QUOTES);
- $variant = (string) ($item['variant_name_snapshot'] ?? '');
- $qty = (int) ($item['quantity'] ?? 1);
- $lineTotal = Currency::format((int) ($item['total_cents'] ?? 0), $currency);
-
- $rows .= '<tr>'
- . '<td style="padding:10px 0;border-bottom:1px solid #e2e8f0;">' . $title
- . ($variant !== '' ? '<br><span style="color:#64748b;font-size:12.5px;">' . htmlspecialchars($variant, ENT_QUOTES) . '</span>' : '')
- . '</td>'
- . '<td style="padding:10px 0;border-bottom:1px solid #e2e8f0;text-align:center;">' . $qty . '</td>'
- . '<td style="padding:10px 0;border-bottom:1px solid #e2e8f0;text-align:right;">' . htmlspecialchars($lineTotal, ENT_QUOTES) . '</td>'
- . '</tr>';
- }
-
- $summaryRows = self::summaryRows($order, $currency);
- $shippingBlock = self::shippingAddressBlock($order);
-
- $downloadsBlock = '';
- if (!empty($downloadLinks)) {
- $downloadItems = '';
- foreach ($downloadLinks as $link) {
- $downloadItems .= '<li style="margin:0 0 6px;"><a href="' . htmlspecialchars((string) $link['url'], ENT_QUOTES) . '" style="color:#2563eb;font-weight:600;">'
- . htmlspecialchars((string) $link['name'], ENT_QUOTES) . '</a></li>';
- }
- $downloadsBlock = '<h3 style="margin:24px 0 8px;font-size:15px;color:#0f172a;">' . self::t('email_downloads_title', 'Your downloads') . '</h3>'
- . '<ul style="margin:0 0 8px;padding-left:18px;">' . $downloadItems . '</ul>'
- . '<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>';
- }
-
- $subject = self::fill(self::raw('email_order_subject', 'Order confirmation — {site} #{order}'), $siteName, $orderNumber);
-
- $html = self::layout(
- $siteName,
- self::raw('email_order_heading', 'Thanks for your order!'),
- '<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>'
- . '<table style="width:100%;border-collapse:collapse;margin:16px 0;">'
- . '<thead><tr>'
- . '<th style="text-align:left;padding:8px 0;border-bottom:2px solid #0f172a;color:#0f172a;">' . self::t('email_column_item', 'Item') . '</th>'
- . '<th style="text-align:center;padding:8px 0;border-bottom:2px solid #0f172a;color:#0f172a;">' . self::t('email_column_qty', 'Qty') . '</th>'
- . '<th style="text-align:right;padding:8px 0;border-bottom:2px solid #0f172a;color:#0f172a;">' . self::t('cart_total', 'Total') . '</th>'
- . '</tr></thead>'
- . '<tbody>' . $rows . '</tbody>'
- . '</table>'
- . '<table style="width:100%;border-collapse:collapse;margin:0 0 16px;">' . $summaryRows . '</table>'
- . $downloadsBlock
- . $shippingBlock
- );
-
- return ['subject' => $subject, 'html' => $html];
- }
-
- public static function orderStatusEmail(array $order, string $newStatus): array
- {
- $siteName = (string) (SiteFront::settings()['site_name'] ?? 'Our store');
- $orderNumber = (string) ($order['order_number'] ?? '');
- $statusLabel = self::statusLabel($newStatus);
-
- $subject = str_replace('{status}', $statusLabel, self::fill(self::raw('email_status_subject', 'Your order #{order} is now: {status}'), $siteName, $orderNumber));
-
- $trackingBlock = '';
- if ($newStatus === 'shipped' && !empty($order['tracking_number'])) {
- $trackingNumber = htmlspecialchars((string) $order['tracking_number'], ENT_QUOTES);
- $trackingUrl = (string) ($order['tracking_url'] ?? '');
- $trackingBlock = '<p style="margin:16px 0 0;color:#334155;">' . self::t('email_tracking_number', 'Tracking number:') . ' <strong>' . $trackingNumber . '</strong>'
- . ($trackingUrl !== '' ? '<br><a href="' . htmlspecialchars($trackingUrl, ENT_QUOTES) . '" style="color:#2563eb;">' . self::t('email_track_package', 'Track your package') . '</a>' : '')
- . '</p>';
- }
-
- $html = self::layout(
- $siteName,
- self::raw('email_status_heading', 'Order update'),
- '<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>'
- . '<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>'
- . $trackingBlock
- );
-
- return ['subject' => $subject, 'html' => $html];
- }
-
- private static function statusLabel(string $status): string
- {
- return self::raw('order_status_' . $status, ucfirst($status));
- }
-
- private static function raw(string $key, string $default): string
- {
- return class_exists('Language') ? Language::get($key, $default) : $default;
- }
-
- private static function t(string $key, string $default): string
- {
- return htmlspecialchars(self::raw($key, $default), ENT_QUOTES);
- }
-
- private static function fill(string $template, string $siteName, string $orderNumber): string
- {
- return str_replace(['{site}', '{order}'], [$siteName, $orderNumber], $template);
- }
-
- private static function summaryRows(array $order, string $currency): string
- {
- $rows = '';
- $line = static function (string $label, int $cents, string $currency) use (&$rows, $order): void {
- $rows .= '<tr>'
- . '<td style="padding:4px 0;color:#64748b;">' . htmlspecialchars($label, ENT_QUOTES) . '</td>'
- . '<td style="padding:4px 0;text-align:right;color:#334155;">' . htmlspecialchars(Currency::format($cents, $currency), ENT_QUOTES) . '</td>'
- . '</tr>';
- };
-
- $line(self::raw('cart_subtotal', 'Subtotal'), (int) ($order['subtotal_cents'] ?? 0), $currency);
- if ((int) ($order['discount_cents'] ?? 0) > 0) {
- $line(self::raw('cart_discount', 'Discount'), -(int) $order['discount_cents'], $currency);
- }
- if ((int) ($order['shipping_cents'] ?? 0) > 0) {
- $line(self::raw('order_shipping_label', 'Shipping'), (int) $order['shipping_cents'], $currency);
- }
- if ((int) ($order['tax_cents'] ?? 0) > 0) {
- $line(self::raw('email_tax', 'Tax'), (int) $order['tax_cents'], $currency);
- }
-
- $rows .= '<tr>'
- . '<td style="padding:10px 0 0;border-top:1px solid #e2e8f0;font-weight:700;color:#0f172a;">' . self::t('cart_total', 'Total') . '</td>'
- . '<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>'
- . '</tr>';
-
- return $rows;
- }
-
- private static function shippingAddressBlock(array $order): string
- {
- $name = trim((string) ($order['shipping_name'] ?? ''));
- if ($name === '') {
- return '';
- }
-
- $lines = array_filter([
- $name,
- (string) ($order['shipping_address_line1'] ?? ''),
- (string) ($order['shipping_address_line2'] ?? ''),
- trim(((string) ($order['shipping_city'] ?? '')) . ' ' . ((string) ($order['shipping_postal_code'] ?? ''))),
- (string) ($order['shipping_region'] ?? ''),
- (string) ($order['shipping_country'] ?? ''),
- ], static fn (string $line): bool => trim($line) !== '');
-
- return '<h3 style="margin:20px 0 8px;color:#0f172a;font-size:14px;">' . self::t('email_shipping_to', 'Shipping to') . '</h3>'
- . '<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>';
- }
-
- private static function layout(string $siteName, string $heading, string $bodyHtml): string
- {
- $safeSiteName = htmlspecialchars($siteName, ENT_QUOTES);
- $safeHeading = htmlspecialchars($heading, ENT_QUOTES);
-
- return '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head>'
- . '<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;">'
- . '<table role="presentation" style="width:100%;border-collapse:collapse;background:#f1f5f9;padding:24px 0;">'
- . '<tr><td align="center">'
- . '<table role="presentation" style="width:100%;max-width:560px;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e2e8f0;">'
- . '<tr><td style="padding:24px 32px;background:#0f172a;">'
- . '<span style="color:#f8fafc;font-size:18px;font-weight:700;">' . $safeSiteName . '</span>'
- . '</td></tr>'
- . '<tr><td style="padding:32px;">'
- . '<h1 style="margin:0 0 16px;font-size:20px;color:#0f172a;">' . $safeHeading . '</h1>'
- . $bodyHtml
- . '</td></tr>'
- . '<tr><td style="padding:20px 32px;background:#f8fafc;border-top:1px solid #e2e8f0;">'
- . '<span style="color:#94a3b8;font-size:12px;">' . $safeSiteName . '</span>'
- . '</td></tr>'
- . '</table>'
- . '</td></tr>'
- . '</table>'
- . '</body></html>';
- }
- }
-