Application:
- IEmailSender.SendHtmlAsync(html, textFallback) - multipart/alternative
с plain-text для клиентов без HTML;
- EmailTemplateRenderer - минимальный mustache-light:
{{key}} (HTML-escape), {{{raw}}} (без escape), {{#key}}...{{/key}}
(условный блок, truthy если не-null/не-пусто/не-"0"/не-"false");
- EmailTemplates - загрузчик embedded HTML-шаблонов из
Resources/EmailTemplates/*.html, кеш after-first-read, parse
"Subject: <тема>" из первой строки, плюс plain-text strip для fallback.
Шаблоны (embedded в food-market.api.dll):
- invite.html - приглашение сотрудника с временным паролем и кнопкой
«Открыть Food Market».
- weekly-summary.html - выручка/чеки/средний чек за неделю + топ-товары.
- low-stock.html - таблица товаров с stock < MinStock.
EmployeesController.Create принимает SendInvite (требует CreateAccount):
формирует payload из orgName/loginUrl/role и шлёт через SendHtmlAsync.
SMTP-ошибки логируются warning'ом, не блокируют создание (showOnce
tempPassword фронту всё равно отдаётся).
Hangfire recurring jobs (EmailNotificationJobs):
- weekly-summary: cron "0 7 * * 1" (понедельник 07:00 UTC) - по каждой
активной орге считает revenue/tx/avgTicket/top-5, шлёт Admin'ам;
- low-stock-alert: cron "0 8 * * *" - товары с sum(stock)<MinStock,
шлёт Admin'ам. AsyncLocal tenant override на каждую орг чтобы
query-filter работал корректно.
Тесты: 8 unit на EmailTemplateRenderer + EmailTemplates (escape, raw,
условные блоки, invite/low-stock-шаблон-loaders). Все 35 unit
зелёные (27 + 8 новых).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
24 lines
1.5 KiB
C#
24 lines
1.5 KiB
C#
namespace foodmarket.Application.Common.Email;
|
||
|
||
/// <summary>Отправка одного письма через текущие платформенные SMTP-настройки.
|
||
/// Конфиг читается из БД (PlatformSettings) на каждой отправке — без рестарта
|
||
/// сервиса можно поменять SMTP-сервер. Реализация — MailKit.</summary>
|
||
public interface IEmailSender
|
||
{
|
||
Task SendAsync(string toEmail, string subject, string body, CancellationToken ct = default);
|
||
|
||
/// <summary>HTML-версия с опциональным plain-text fallback. Используется
|
||
/// шаблонизированными письмами (приглашения, weekly-summary, low-stock).
|
||
/// Если <paramref name="textBody"/> null — клиент должен сам уметь
|
||
/// рендерить HTML; для большинства современных clients это не проблема.</summary>
|
||
Task SendHtmlAsync(string toEmail, string subject, string htmlBody, string? textBody = null,
|
||
CancellationToken ct = default);
|
||
}
|
||
|
||
/// <summary>Бросается когда платформенный SMTP не настроен. Контроллеры должны
|
||
/// ловить и возвращать понятный 503/400 — не падать в 500.</summary>
|
||
public class EmailNotConfiguredException : Exception
|
||
{
|
||
public EmailNotConfiguredException(string message) : base(message) { }
|
||
}
|