food-market/src/food-market.application/Common/Email/IEmailSender.cs
nns 25d92c989a feat(email): HTML-шаблоны MailKit + invite/weekly/low-stock джобы (P1-22)
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>
2026-05-28 16:37:32 +05:00

24 lines
1.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) { }
}