From 4ddf95dadf4247ff22db3256a86f9e75bf282eaa Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 28 May 2026 22:42:33 +0500 Subject: [PATCH] =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=8B=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notifications/channels/email-channel.js | 3 +- .../__tests__/email-templates.test.js | 54 +++++++++++++++++++ .../templates/email-templates.js | 51 ++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 server/src/lib/notifications/templates/__tests__/email-templates.test.js diff --git a/server/src/lib/notifications/channels/email-channel.js b/server/src/lib/notifications/channels/email-channel.js index bba6e5d..f37e75c 100644 --- a/server/src/lib/notifications/channels/email-channel.js +++ b/server/src/lib/notifications/channels/email-channel.js @@ -1,6 +1,7 @@ // server/src/lib/notifications/channels/email-channel.js import { sendNotificationEmail } from '../../email.js' import { + renderAdminOrderMessageEmail, renderOrderCreatedEmail, renderOrderStatusChangedEmail, renderOrderMessageEmail, @@ -17,7 +18,7 @@ const templateRenderers = { 'orderMessage:adminReply': renderOrderMessageEmail, 'payment:statusChanged': renderPaymentStatusChangedEmail, 'order:created:admin': renderAdminOrderCreatedEmail, - 'orderMessage:sent': renderOrderMessageEmail, + 'orderMessage:sent': renderAdminOrderMessageEmail, 'review:created': renderAdminNewReviewEmail, 'auth:codeRequested': renderAuthCodeEmail, 'order:deliveryFeeAdjusted': renderDeliveryFeeAdjustedEmail, diff --git a/server/src/lib/notifications/templates/__tests__/email-templates.test.js b/server/src/lib/notifications/templates/__tests__/email-templates.test.js new file mode 100644 index 0000000..2c09255 --- /dev/null +++ b/server/src/lib/notifications/templates/__tests__/email-templates.test.js @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { + renderAdminOrderMessageEmail, + renderDeliveryFeeAdjustedEmail, + renderOrderCreatedEmail, + renderOrderMessageEmail, + renderOrderStatusChangedEmail, + renderPaymentStatusChangedEmail, +} from '../email-templates.js' + +const originalClientPublicUrl = process.env.CLIENT_PUBLIC_URL +const orderId = 'order-123456789' + +afterEach(() => { + if (originalClientPublicUrl === undefined) { + delete process.env.CLIENT_PUBLIC_URL + return + } + process.env.CLIENT_PUBLIC_URL = originalClientPublicUrl +}) + +describe('email templates', () => { + it('adds personal account order links to order emails', () => { + process.env.CLIENT_PUBLIC_URL = 'https://shop.example.com/' + const expectedUrl = `https://shop.example.com/me/orders/${orderId}` + + const emails = [ + renderOrderCreatedEmail({ orderId, totalCents: 120000, itemsCount: 2, deliveryType: 'pickup' }), + renderOrderStatusChangedEmail({ orderId, oldStatus: 'PENDING_PAYMENT', newStatus: 'PAID' }), + renderPaymentStatusChangedEmail({ orderId, paymentStatus: 'confirmed' }), + renderDeliveryFeeAdjustedEmail({ orderId, totalCents: 135000 }), + ] + + for (const email of emails) { + expect(email.html).toContain(`href="${expectedUrl}"`) + } + }) + + it('adds personal account messages link to order message emails', () => { + process.env.CLIENT_PUBLIC_URL = 'https://shop.example.com' + + const email = renderOrderMessageEmail({ orderId, preview: 'Здравствуйте' }) + + expect(email.html).toContain('href="https://shop.example.com/me/messages"') + }) + + it('adds admin orders link to admin order message emails', () => { + process.env.CLIENT_PUBLIC_URL = 'https://shop.example.com' + + const email = renderAdminOrderMessageEmail({ orderId, preview: 'Нужна консультация' }) + + expect(email.html).toContain('href="https://shop.example.com/admin/orders"') + }) +}) diff --git a/server/src/lib/notifications/templates/email-templates.js b/server/src/lib/notifications/templates/email-templates.js index daf9900..dcfc24c 100644 --- a/server/src/lib/notifications/templates/email-templates.js +++ b/server/src/lib/notifications/templates/email-templates.js @@ -14,6 +14,36 @@ function baseLayout(title, body) { ` } +function getClientPublicUrl() { + return (process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173').replace(/\/$/, '') +} + +function buildClientUrl(path) { + return `${getClientPublicUrl()}${path}` +} + +function renderActionLink(url, label) { + return ` +

+ + ${label} + +

+ ` +} + +function buildOrderUrl(orderId) { + return buildClientUrl(`/me/orders/${encodeURIComponent(orderId)}`) +} + +function buildMessagesUrl() { + return buildClientUrl('/me/messages') +} + +function buildAdminOrdersUrl() { + return buildClientUrl('/admin/orders') +} + export function renderOrderCreatedEmail({ orderId, totalCents, itemsCount, deliveryType }) { const total = (totalCents / 100).toLocaleString('ru-RU') const nextAction = @@ -22,6 +52,7 @@ export function renderOrderCreatedEmail({ orderId, totalCents, itemsCount, deliv

Ваш заказ #${orderId.slice(0, 8)} успешно создан.

Товаров: ${itemsCount} | Сумма: ${total} ₽

${nextAction}

+ ${renderActionLink(buildOrderUrl(orderId), 'Открыть заказ в личном кабинете')} ` return { subject: 'Заказ создан', html: baseLayout('Заказ создан', body) } } @@ -42,6 +73,7 @@ export function renderOrderStatusChangedEmail({ orderId, oldStatus, newStatus }) const body = `

Статус заказа #${orderId.slice(0, 8)} изменён.

${oldLabel}${newLabel}

+ ${renderActionLink(buildOrderUrl(orderId), 'Открыть заказ в личном кабинете')} ` return { subject: `Статус заказа изменён — ${newLabel}`, @@ -57,6 +89,23 @@ export function renderOrderMessageEmail({ orderId, preview }) { ${truncated}

Ответьте в личном кабинете.

+ ${renderActionLink(buildMessagesUrl(), 'Открыть сообщения в личном кабинете')} + ` + return { + subject: 'Новое сообщение к заказу', + html: baseLayout('Новое сообщение', body), + } +} + +export function renderAdminOrderMessageEmail({ orderId, preview }) { + const truncated = preview.length > 200 ? preview.slice(0, 197) + '...' : preview + const body = ` +

Новое сообщение к заказу #${orderId.slice(0, 8)}:

+
+ ${truncated} +
+

Ответьте в админ-панели.

+ ${renderActionLink(buildAdminOrdersUrl(), 'Открыть заказы в админ-панели')} ` return { subject: 'Новое сообщение к заказу', @@ -73,6 +122,7 @@ export function renderPaymentStatusChangedEmail({ orderId, paymentStatus }) { const label = statusLabels[paymentStatus] || paymentStatus const body = `

Статус оплаты заказа #${orderId.slice(0, 8)}: ${label}.

+ ${renderActionLink(buildOrderUrl(orderId), 'Открыть заказ в личном кабинете')} ` return { subject: `Оплата заказа — ${label}`, @@ -115,6 +165,7 @@ export function renderDeliveryFeeAdjustedEmail({ orderId, totalCents }) {

Стоимость доставки заказа #${orderId.slice(0, 8)} скорректирована.

Новая сумма: ${total} ₽

Ожидает оплаты. Проверьте статус заказа в личном кабинете.

+ ${renderActionLink(buildOrderUrl(orderId), 'Открыть заказ в личном кабинете')} ` return { subject: 'Стоимость доставки скорректирована',