Repository: elizavetapavliv/report_hub
Pull request #195: EmailSender Implementation
Author: login: mkknyaz
Created: 2025-05-06T22:49:47Z | Merged: 2025-05-09T08:22:16Z



Diff:
diff --git a/.github/workflows/deploy-service-and-run-mongo.yml b/.github/workflows/deploy-service-and-run-mongo.yml
index 0fc6549b..3ba1fd1d 100644
--- a/.github/workflows/deploy-service-and-run-mongo.yml
+++ b/.github/workflows/deploy-service-and-run-mongo.yml
@@ -20,9 +20,11 @@ jobs:
        
        Updates: |
          Authority: ${{ vars.AUTHORITY }}
-         ConnectionStrings__Mongo: ${{secrets.MONGO_CONNECTION_STRING}}
-         EcbConfig__Host: ${{vars.ECB_CONFIG_HOST}}
-         ReportHubConfig__Host: ${{vars.REPORTHUB_CONFIG_HOST}}
+         ConnectionStrings__Mongo: ${{ secrets.MONGO_CONNECTION_STRING }}
+         EcbConfig__Host: ${{ vars.ECB_CONFIG_HOST }}
+         ReportHubConfig__Host: ${{ vars.REPORTHUB_CONFIG_HOST }}
+         SmtpConfig__Email: ${{ vars.SMTP_Email }}
+         SmtpConfig__Password: ${{ secrets.SMTP_PASSWORD }}
          
     steps:
     - name: Checkout
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Abstract/IEmailSender.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Abstract/IEmailSender.cs
new file mode 100644
index 00000000..0e0bffaf
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Abstract/IEmailSender.cs
@@ -0,0 +1,9 @@
+﻿using System.Net.Mail;
+using Exadel.ReportHub.Email.Models;
+
+namespace Exadel.ReportHub.Email.Abstract;
+
+public interface IEmailSender
+{
+    Task SendAsync(string to, string subject, Attachment attachment,  string templateName, object data, CancellationToken cancellationToken);
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Abstract/ITemplateRender.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Abstract/ITemplateRender.cs
new file mode 100644
index 00000000..14ae0d53
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Abstract/ITemplateRender.cs
@@ -0,0 +1,6 @@
+﻿namespace Exadel.ReportHub.Email.Abstract;
+
+public interface ITemplateRender
+{
+    Task<string> RenderAsync(string templateName, object data, CancellationToken cancellationToken);
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Configs/SmtpConfig.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Configs/SmtpConfig.cs
new file mode 100644
index 00000000..cf7d3862
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Configs/SmtpConfig.cs
@@ -0,0 +1,14 @@
+﻿namespace Exadel.ReportHub.Email.Configs;
+
+public class SmtpConfig
+{
+    public string Host { get; set; }
+
+    public int Port { get; set; }
+
+    public string Email { get; set; }
+
+    public string Password { get; set; }
+
+    public string DisplayName { get; set; }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Constants.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Constants.cs
new file mode 100644
index 00000000..86c54b33
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Constants.cs
@@ -0,0 +1,15 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Exadel.ReportHub.Email;
+
+public static class Constants
+{
+    public static class ResourcePath
+    {
+        public static readonly string EmailTemplates = Path.Combine("Deployment", "Resources", "EmailTemplates");
+    }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/EmailSender.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/EmailSender.cs
new file mode 100644
index 00000000..dcdb2287
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/EmailSender.cs
@@ -0,0 +1,36 @@
+﻿using System.Net;
+using System.Net.Mail;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Configs;
+using Exadel.ReportHub.Email.Models;
+using Microsoft.Extensions.Options;
+
+namespace Exadel.ReportHub.Email;
+
+public class EmailSender(IOptionsMonitor<SmtpConfig> smtpConfig, ITemplateRender templateRender) : IEmailSender
+{
+    public async Task SendAsync(string to, string subject, Attachment attachment, string templateName, object data, CancellationToken cancellationToken)
+    {
+        var htmlBody = await templateRender.RenderAsync(templateName, data, cancellationToken);
+        using var message = new MailMessage
+        {
+            From = new MailAddress(smtpConfig.CurrentValue.Email, smtpConfig.CurrentValue.DisplayName),
+            Subject = subject,
+            Body = htmlBody,
+            IsBodyHtml = true
+        };
+        message.To.Add(to);
+        if (attachment is not null)
+        {
+            message.Attachments.Add(attachment);
+        }
+
+        using var smtpClient = new SmtpClient(smtpConfig.CurrentValue.Host, smtpConfig.CurrentValue.Port)
+        {
+            EnableSsl = true,
+            Credentials = new NetworkCredential(smtpConfig.CurrentValue.Email, smtpConfig.CurrentValue.Password)
+        };
+
+        await smtpClient.SendMailAsync(message, cancellationToken);
+    }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Exadel.ReportHub.Email.csproj b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Exadel.ReportHub.Email.csproj
new file mode 100644
index 00000000..7cb3085b
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Exadel.ReportHub.Email.csproj
@@ -0,0 +1,22 @@
+﻿<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>disable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.3" />
+    <PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="Stubble.Core" Version="1.10.8" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+  </ItemGroup>
+
+</Project>
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Models/ReportEmailModel.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Models/ReportEmailModel.cs
new file mode 100644
index 00000000..9cbd130a
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Models/ReportEmailModel.cs
@@ -0,0 +1,18 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Exadel.ReportHub.Email.Models;
+
+public class ReportEmailModel
+{
+    public string UserName { get; set; }
+
+    public string StartDate { get; set; }
+
+    public string EndDate { get; set; }
+
+    public bool IsSuccess { get; set; }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/TemplateRender.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/TemplateRender.cs
new file mode 100644
index 00000000..208a71b8
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Email/TemplateRender.cs
@@ -0,0 +1,16 @@
+﻿using Exadel.ReportHub.Email.Abstract;
+using Stubble.Core;
+using Stubble.Core.Builders;
+using Stubble.Core.Interfaces;
+
+namespace Exadel.ReportHub.Email;
+
+public class TemplateRender : ITemplateRender
+{
+    public async Task<string> RenderAsync(string templateName, object data, CancellationToken cancellationToken)
+    {
+        var path = Path.Combine(Constants.ResourcePath.EmailTemplates, templateName);
+        var text = await File.ReadAllTextAsync(path, cancellationToken);
+        return await new StubbleBuilder().Build().RenderAsync(text, data);
+    }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Exadel.ReportHub.Handlers.csproj b/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Exadel.ReportHub.Handlers.csproj
index 7bbdcd05..9fce19d0 100644
--- a/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Exadel.ReportHub.Handlers.csproj
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Exadel.ReportHub.Handlers.csproj
@@ -25,6 +25,7 @@
     <ProjectReference Include="..\Exadel.ReportHub.Audit\Exadel.ReportHub.Audit.csproj" />
     <ProjectReference Include="..\Exadel.ReportHub.Common\Exadel.ReportHub.Common.csproj" />
     <ProjectReference Include="..\Exadel.ReportHub.Csv\Exadel.ReportHub.Csv.csproj" />
+    <ProjectReference Include="..\Exadel.ReportHub.Email\Exadel.ReportHub.Email.csproj" />
     <ProjectReference Include="..\Exadel.ReportHub.ExchangeRate\Exadel.ReportHub.Ecb.csproj" />
     <ProjectReference Include="..\Exadel.ReportHub.Pdf\Exadel.ReportHub.Pdf.csproj" />
     <ProjectReference Include="..\Exadel.ReportHub.RA\Exadel.ReportHub.RA.csproj" />
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Report/Send/SendReportsHandler.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Report/Send/SendReportsHandler.cs
index 12b2aec4..86498d1e 100644
--- a/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Report/Send/SendReportsHandler.cs
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Report/Send/SendReportsHandler.cs
@@ -1,14 +1,21 @@
-﻿using AutoMapper;
-using ErrorOr;
+﻿using System.IO;
+using System.Net.Mail;
+using System.Net.Mime;
+using AutoMapper;
+using Exadel.ReportHub.Email;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Models;
+using Exadel.ReportHub.Pdf;
 using Exadel.ReportHub.RA.Abstract;
 using Exadel.ReportHub.SDK.DTOs.User;
 using MediatR;
+using Microsoft.Extensions.Logging;
 
 namespace Exadel.ReportHub.Handlers.Report.Send;
 
 public record SendReportsRequest : IRequest<Unit>;
 
-public class SendReportsHandler(IUserRepository userRepository) : IRequestHandler<SendReportsRequest, Unit>
+public class SendReportsHandler(IUserRepository userRepository, IEmailSender emailSender, ILogger<SendReportsHandler> logger) : IRequestHandler<SendReportsRequest, Unit>
 {
     public async Task<Unit> Handle(SendReportsRequest request, CancellationToken cancellationToken)
     {
@@ -18,6 +25,31 @@ public async Task<Unit> Handle(SendReportsRequest request, CancellationToken can
         var currentDayOfWeek = now.DayOfWeek;
         var users = await userRepository.GetUsersByNotificationSettingsAsync(currentDay, currentDayOfWeek, currentHour, cancellationToken);
 
+        await Task.WhenAll(users.Select(async user =>
+        {
+            try
+            {
+                // var stream = generator
+                // var attachment = new Attachment(stream, $"{reportName}.{user.NotificationSettings.ExportFormat}",);
+
+                var reportEmail = new ReportEmailModel
+                {
+                    UserName = user.FullName,
+                    StartDate = FormatDate(DateTime.Now), // Here'll be a report date
+                    EndDate = FormatDate(DateTime.Now.AddDays(2)), // Here'll be a report date
+                    IsSuccess = true // It'll be depend on generation status
+                };
+
+                await emailSender.SendAsync(user.Email, "Report", null, "Report.html", reportEmail, cancellationToken);
+            }
+            catch (Exception ex)
+            {
+                logger.LogError(ex, "Failed to send report email to user {UserId} ({Email})", user.Id, user.Email);
+            }
+        }));
+
         return Unit.Value;
     }
+
+    private static string FormatDate(DateTime date) => date.ToString("dd.MM.yyyy");
 }
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
new file mode 100644
index 00000000..b3c42bf9
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
@@ -0,0 +1,70 @@
+﻿using System.Net.Mail;
+using System.Net.Mime;
+using AutoMapper;
+using ErrorOr;
+using Exadel.ReportHub.Common.Providers;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Models;
+using Exadel.ReportHub.Pdf;
+using Exadel.ReportHub.Pdf.Abstract;
+using Exadel.ReportHub.Pdf.Models;
+using Exadel.ReportHub.RA;
+using Exadel.ReportHub.RA.Abstract;
+using Exadel.ReportHub.SDK.DTOs.Item;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(Guid InvoiceId, string Email, string Subject) : IRequest<ErrorOr<TestEmailResult>>;
+public class TestEmailHandler(
+    IEmailSender emailSender,
+    IInvoiceRepository invoiceRepository,
+    IUserRepository userRepository,
+    IItemRepository itemRepository,
+    IClientRepository clientRepository,
+    ICustomerRepository customerRepository,
+    IMapper mapper,
+    IPdfInvoiceGenerator pdfInvoiceGenerator,
+    IUserProvider userProvider) : IRequestHandler<TestEmailRequest, ErrorOr<TestEmailResult>>
+{
+    public async Task<ErrorOr<TestEmailResult>> Handle(TestEmailRequest request, CancellationToken cancellationToken)
+    {
+        try
+        {
+            var invoice = await invoiceRepository.GetByIdAsync(request.InvoiceId, cancellationToken);
+            if (invoice is null)
+            {
+                return Error.NotFound();
+            }
+
+            var itemsTask = itemRepository.GetByIdsAsync(invoice.ItemIds, cancellationToken);
+            var clientTask = clientRepository.GetByIdAsync(invoice.ClientId, cancellationToken);
+            var customerTask = customerRepository.GetByIdAsync(invoice.CustomerId, cancellationToken);
+            await Task.WhenAll(itemsTask, clientTask, customerTask);
+
+            var invoiceModel = mapper.Map<InvoiceModel>(invoice);
+            invoiceModel.ClientName = clientTask.Result.Name;
+            invoiceModel.CustomerName = customerTask.Result.Name;
+            invoiceModel.Items = mapper.Map<IList<ItemDTO>>(itemsTask.Result);
+
+            var stream = await pdfInvoiceGenerator.GenerateAsync(invoiceModel, cancellationToken);
+            var attachment = new Attachment(stream, $"{invoice.InvoiceNumber}{Constants.File.Extension.Pdf}", MediaTypeNames.Application.Pdf);
+
+            var user = await userRepository.GetByIdAsync(userProvider.GetUserId(), cancellationToken);
+            var reportEmail = new ReportEmailModel
+            {
+                UserName = user.FullName,
+                StartDate = DateTime.Now.Date.ToString("dd.MM.yyyy"),
+                EndDate = DateTime.Now.Date.AddDays(2).ToString("dd.MM.yyyy"),
+                IsSuccess = true
+            };
+
+            await emailSender.SendAsync(request.Email, request.Subject, attachment, "Report.html", reportEmail, cancellationToken);
+            return new TestEmailResult { IsSent = true };
+        }
+        catch
+        {
+            return new TestEmailResult { IsSent = false };
+        }
+    }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailResult.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailResult.cs
new file mode 100644
index 00000000..231a1f01
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailResult.cs
@@ -0,0 +1,12 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public class TestEmailResult
+{
+    public bool IsSent { get; set; }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Deployment/Resources/EmailTemplates/Report.html b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Deployment/Resources/EmailTemplates/Report.html
new file mode 100644
index 00000000..274a3be1
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Deployment/Resources/EmailTemplates/Report.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8" />
+    <title>Your report is ready</title>
+    <style>
+        body {
+            margin: 0;
+            padding: 0;
+            font-family: Arial, sans-serif;
+            background: #f4f4f4;
+        }
+
+        .container {
+            max-width: 600px;
+            margin: 20px auto;
+            background: #fff;
+        }
+
+        .header {
+            background: #1E90FF;
+            color: #fff;
+            padding: 20px;
+            text-align: center;
+            font-size: 24px;
+        }
+
+        .content {
+            padding: 30px;
+            color: #333;
+        }
+
+        .button {
+            display: inline-block;
+            margin-top: 20px;
+            padding: 12px 24px;
+            background: #e50914;
+            color: #fff;
+            text-decoration: none;
+            border-radius: 4px;
+        }
+
+        .footer {
+            padding: 20px;
+            text-align: center;
+            font-size: 12px;
+            color: #777;
+            border-top: 1px solid #ddd;
+        }
+    </style>
+</head>
+<body>
+    <table class="container" cellspacing="0" cellpadding="0">
+        <tr>
+            <td class="header">Exadel Report Hub</td>
+        </tr>
+        <tr>
+            <td class="content">
+                <h2>Hello, {{UserName}}!</h2>
+
+                {{#IsSuccess}}
+                <p>Your report for <strong>{{StartDate}}</strong> to <strong>{{EndDate}}</strong> is now ready.</p>
+                {{/IsSuccess}}
+
+                {{^IsSuccess}}
+                <p>Unfortunately, we couldn't generate your report for <strong>{{StartDate}}</strong> to <strong>{{EndDate}}</strong>.</p>
+                <p>Please try again later or contact support.</p>
+                {{/IsSuccess}}
+            </td>
+        </tr>
+        <tr>
+            <td class="footer">
+                &copy; Exadel Report Hub<br />
+                You received this email because you requested a report from our service.
+            </td>
+        </tr>
+    </table>
+</body>
+</html>
\ No newline at end of file
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Exadel.ReportHub.Host.csproj b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Exadel.ReportHub.Host.csproj
index b1ef3f2e..340fe874 100644
--- a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Exadel.ReportHub.Host.csproj
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Exadel.ReportHub.Host.csproj
@@ -33,4 +33,10 @@
     <ProjectReference Include="..\Exadel.ReportHub.RA\Exadel.ReportHub.RA.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Content Include="Deployment\Resources\**">
+    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+
 </Project>
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Registrations/EmailRegistrations.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Registrations/EmailRegistrations.cs
new file mode 100644
index 00000000..8a5caf40
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Registrations/EmailRegistrations.cs
@@ -0,0 +1,24 @@
+﻿using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Mail;
+using Aspose.Pdf.Forms;
+using Exadel.ReportHub.Email;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Configs;
+using Microsoft.Extensions.Options;
+
+namespace Exadel.ReportHub.Host.Registrations;
+
+[ExcludeFromCodeCoverage]
+public static class EmailRegistrations
+{
+    public static IServiceCollection AddEmailSender(this IServiceCollection services, IConfiguration configuration)
+    {
+        services.Configure<SmtpConfig>(configuration.GetSection(nameof(SmtpConfig)));
+
+        services.AddSingleton<ITemplateRender, TemplateRender>();
+        services.AddSingleton<IEmailSender, EmailSender>();
+
+        return services;
+    }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Services/TestService.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Services/TestService.cs
new file mode 100644
index 00000000..e9cb89b3
--- /dev/null
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Services/TestService.cs
@@ -0,0 +1,20 @@
+﻿using System.Diagnostics.CodeAnalysis;
+using Exadel.ReportHub.Handlers.TestEmail;
+using Exadel.ReportHub.Host.Services.Abstract;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Exadel.ReportHub.Host.Services;
+
+[ExcludeFromCodeCoverage]
+[Route("api/test")]
+public class TestService(ISender sender) : BaseService
+{
+    [HttpGet("send-email")]
+    public async Task<ActionResult<TestEmailResult>> SendEmail([FromQuery] Guid invoiceId, [FromQuery] string email, [FromQuery] string subject)
+    {
+        var result = await sender.Send(new TestEmailRequest(invoiceId, email, subject));
+
+        return FromResult(result);
+    }
+}
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Startup.cs b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Startup.cs
index c0d130e0..9168e5e1 100644
--- a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Startup.cs
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Startup.cs
@@ -94,6 +94,7 @@ public void ConfigureServices(IServiceCollection services)
             .AddHttpContextAccessor()
             .AddCsv()
             .AddPdf()
+            .AddEmailSender(configuration)
             .AddExchangeRate(configuration)
             .AddPing(configuration)
             .AddScheduler()
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.Development.json b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.Development.json
index 8380a37c..6abbffd7 100644
--- a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.Development.json
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.Development.json
@@ -8,5 +8,12 @@
     },
     "ReportHubConfig": {
         "Host": "https://localhost:5001"
+    },
+    "SmtpConfig": {
+        "Host": "smtp.gmail.com",
+        "Port": 587,
+        "Email": "groupbootcamp1@gmail.com",
+        "Password": "zmhivblehbpotfmu",
+        "DisplayName": "Exadel Report Hub"
     }
 }
\ No newline at end of file
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.json b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.json
index f38bd01c..56b3930f 100644
--- a/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.json
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.json
@@ -11,5 +11,9 @@
     "ReportHubConfig": {
         "ConnectionTimeout": 10
     },
-    "AllowedHosts": "*"
+    "AllowedHosts": "*",
+    "SmtpConfig": {
+        "Host": "smtp.gmail.com",
+        "Port": 587
+    }
 }
\ No newline at end of file
diff --git a/Projects/Exadel.ReportHub/Exadel.ReportHub.sln b/Projects/Exadel.ReportHub/Exadel.ReportHub.sln
index d86cf445..893309fa 100644
--- a/Projects/Exadel.ReportHub/Exadel.ReportHub.sln
+++ b/Projects/Exadel.ReportHub/Exadel.ReportHub.sln
@@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exadel.ReportHub.Pdf", "Exa
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exadel.ReportHub.Audit", "Exadel.ReportHub.Audit\Exadel.ReportHub.Audit.csproj", "{BF0C56A7-E0D0-4E1A-98DB-8AAB7F9D4990}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exadel.ReportHub.Email", "Exadel.ReportHub.Email\Exadel.ReportHub.Email.csproj", "{8747CD31-593B-4DA6-95F6-C04F41DF12AD}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -86,6 +88,10 @@ Global
 		{BF0C56A7-E0D0-4E1A-98DB-8AAB7F9D4990}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{BF0C56A7-E0D0-4E1A-98DB-8AAB7F9D4990}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{BF0C56A7-E0D0-4E1A-98DB-8AAB7F9D4990}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8747CD31-593B-4DA6-95F6-C04F41DF12AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8747CD31-593B-4DA6-95F6-C04F41DF12AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8747CD31-593B-4DA6-95F6-C04F41DF12AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8747CD31-593B-4DA6-95F6-C04F41DF12AD}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE


Reviews:
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:34:12Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:42:27Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:44:12Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:44:40Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:45:37Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:45:57Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:46:59Z
user: login: mkknyaz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T07:53:47Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T06:09:48Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T07:31:27Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T08:54:57Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T08:55:38Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T08:58:22Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T08:59:37Z
user: login: mkknyaz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T09:04:12Z
user: login: mkknyaz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T09:06:07Z
user: login: mkknyaz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T09:11:23Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T09:16:35Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T09:27:53Z
user: login: elizavetapavliv
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T13:19:08Z
user: login: elizavetapavliv
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-09T08:17:02Z

Review comments:
diff_hunk: @@ -20,9 +20,11 @@ jobs:
        
        Updates: |
          Authority: ${{ vars.AUTHORITY }}
-         ConnectionStrings__Mongo: ${{secrets.MONGO_CONNECTION_STRING}}
-         EcbConfig__Host: ${{vars.ECB_CONFIG_HOST}}
-         ReportHubConfig__Host: ${{vars.REPORTHUB_CONFIG_HOST}}
+         ConnectionStrings__Mongo: ${{ secrets.MONGO_CONNECTION_STRING }}
+         EcbConfig__Host: ${{ vars.ECB_CONFIG_HOST }}
+         ReportHubConfig__Host: ${{ vars.REPORTHUB_CONFIG_HOST }}
+         SmtpConfig__UserName: ${{ secrets.SMTP_USERNAME }}
path: .github/workflows/deploy-service-and-run-mongo.yml
user: login: elizavetapavliv
body: user name can in vars, password in secret
created_at: 2025-05-07T07:34:11Z
author_association: OWNER
diff_hunk: @@ -11,5 +11,10 @@
     "ReportHubConfig": {
         "ConnectionTimeout": 10
     },
-    "AllowedHosts": "*"
+    "AllowedHosts": "*",
+    "SmtpConfig": {
+        "Host": "smtp.gmail.com",
+        "Port": 587,
+        "EnableSsl": true
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.json
user: login: elizavetapavliv
body: EnableSsl this is not config  staff, just set always true
created_at: 2025-05-07T07:42:27Z
author_association: OWNER
diff_hunk: @@ -8,5 +8,12 @@
     },
     "ReportHubConfig": {
         "Host": "https://localhost:5001"
+    },
+    "SmtpConfig": {
+        "Host": "smtp.gmail.com",
+        "Port": 587,
+        "EnableSsl": true,
+        "UserName": "groupbootcamp1@gmail.com",
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.Development.json
user: login: elizavetapavliv
body: rename to Email.

is there ability to specify user friendly Name? E.g. Exadel Report Hub, like usually when recieving Email you recieve from Petr Ivanov not petr_ivanov@gmail.com
created_at: 2025-05-07T07:44:12Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,8 @@
+﻿using System.Net.Mail;
+
+namespace Exadel.ReportHub.Email.Abstract;
+
+public interface IEmailSender
+{
+    Task SendAsync(MailMessage message);
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Abstract/IEmailSender.cs
user: login: elizavetapavliv
body: Cancellation token
created_at: 2025-05-07T07:44:40Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,30 @@
+﻿using System.Net.Mail;
+using ErrorOr;
+using Exadel.ReportHub.Email.Abstract;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(string Email, string Subject, string Body) : IRequest<ErrorOr<TestEmailResult>>;
+public class TestEmailHandler(IEmailSender emailSender) : IRequestHandler<TestEmailRequest, ErrorOr<TestEmailResult>>
+{
+    public async Task<ErrorOr<TestEmailResult>> Handle(TestEmailRequest request, CancellationToken cancellationToken)
+    {
+        var msg = new MailMessage
+        {
+            Subject = request.Subject,
+            Body = request.Body,
+            IsBodyHtml = false
+        };
+        msg.To.Add(request.Email);
+        try
+        {
+            await emailSender.SendAsync(msg);
+            return new TestEmailResult { IsSent = true };
+        }
+        catch (Exception ex)
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: elizavetapavliv
body: fix warn, ex can  be not specified
created_at: 2025-05-07T07:45:37Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,30 @@
+﻿using System.Net.Mail;
+using ErrorOr;
+using Exadel.ReportHub.Email.Abstract;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(string Email, string Subject, string Body) : IRequest<ErrorOr<TestEmailResult>>;
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: elizavetapavliv
body: are you going to remove it in future?
created_at: 2025-05-07T07:45:57Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,30 @@
+﻿using System.Net.Mail;
+using ErrorOr;
+using Exadel.ReportHub.Email.Abstract;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(string Email, string Subject, string Body) : IRequest<ErrorOr<TestEmailResult>>;
+public class TestEmailHandler(IEmailSender emailSender) : IRequestHandler<TestEmailRequest, ErrorOr<TestEmailResult>>
+{
+    public async Task<ErrorOr<TestEmailResult>> Handle(TestEmailRequest request, CancellationToken cancellationToken)
+    {
+        var msg = new MailMessage
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: elizavetapavliv
body: this should be inside email sender
created_at: 2025-05-07T07:46:59Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,30 @@
+﻿using System.Net.Mail;
+using ErrorOr;
+using Exadel.ReportHub.Email.Abstract;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(string Email, string Subject, string Body) : IRequest<ErrorOr<TestEmailResult>>;
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: mkknyaz
body: Yes, it’s essentially a way to test and understand how it works, as well as to practice sending emails.
It'll deleted after report-sending generation

created_at: 2025-05-07T07:53:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+<!DOCTYPE html>
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/EmailTemplates/Report.html
user: login: elizavetapavliv
body: Put templates to deployment folder, sub folder Resources/EmailTemplates 
created_at: 2025-05-08T05:59:45Z
author_association: OWNER
diff_hunk: @@ -8,5 +8,16 @@
     },
     "ReportHubConfig": {
         "Host": "https://localhost:5001"
+    },
+    "SmtpConfig": {
+        "Host": "smtp.gmail.com",
+        "Port": 587,
+        "EnableSsl": true,
+        "Email": "groupbootcamp1@gmail.com",
+        "Password": "zmhivblehbpotfmu",
+        "DisplayName": "Exadel Report Hub"
+    },
+    "TemplateConfig": {
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.Development.json
user: login: elizavetapavliv
body: This config not needed
created_at: 2025-05-08T06:00:20Z
author_association: OWNER
diff_hunk: @@ -8,5 +8,16 @@
     },
     "ReportHubConfig": {
         "Host": "https://localhost:5001"
+    },
+    "SmtpConfig": {
+        "Host": "smtp.gmail.com",
+        "Port": 587,
+        "EnableSsl": true,
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/appsettings.Development.json
user: login: elizavetapavliv
body: Not needed
created_at: 2025-05-08T06:00:39Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,16 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Exadel.ReportHub.Email.Models;
+
+public class ReportEmailModel
+{
+    public string UserName { get; set; }
+
+    public string ReportPeriod { get; set; }
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Models/ReportEmailModel.cs
user: login: elizavetapavliv
body: Pass two dates, it's not task of handler to build correct dates string 
created_at: 2025-05-08T06:02:12Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,26 @@
+﻿using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Mail;
+using Aspose.Pdf.Forms;
+using Exadel.ReportHub.Email;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Configs;
+using Exadel.ReportHub.ReportHub.Configs;
+using Microsoft.Extensions.Options;
+
+namespace Exadel.ReportHub.Host.Registrations;
+
+[ExcludeFromCodeCoverage]
+public static class EmailRegistrations
+{
+    public static IServiceCollection AddEmailSender(this IServiceCollection services, IConfiguration configuration)
+    {
+        services.Configure<SmtpConfig>(configuration.GetSection(nameof(SmtpConfig)));
+        services.Configure<TemplateConfig>(configuration.GetSection(nameof(TemplateConfig)));
+
+        services.AddSingleton<ITemplateRender, TemplateRender>();
+        services.AddScoped<IEmailSender, EmailSender>();
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Registrations/EmailRegistrations.cs
user: login: elizavetapavliv
body: Why it scoped
created_at: 2025-05-08T06:02:49Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,26 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.ReportHub.Configs;
+using Microsoft.Extensions.Options;
+using Stubble.Core;
+using Stubble.Core.Builders;
+using Stubble.Core.Interfaces;
+
+namespace Exadel.ReportHub.Email;
+
+public class TemplateRender(IOptionsMonitor<TemplateConfig> templateConfig) : ITemplateRender
+{
+    private readonly string _folder = templateConfig.CurrentValue.TemplatesFolder;
+    private readonly StubbleVisitorRenderer _renderer = new StubbleBuilder().Build();
+
+    public async Task<string> RenderAsync(string templateName, object viewModel, CancellationToken cancellationToken)
+    {
+        var path = Path.Combine(_folder, templateName);
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Email/TemplateRender.cs
user: login: elizavetapavliv
body: Move path of templates to constants
created_at: 2025-05-08T06:03:31Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,26 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.ReportHub.Configs;
+using Microsoft.Extensions.Options;
+using Stubble.Core;
+using Stubble.Core.Builders;
+using Stubble.Core.Interfaces;
+
+namespace Exadel.ReportHub.Email;
+
+public class TemplateRender(IOptionsMonitor<TemplateConfig> templateConfig) : ITemplateRender
+{
+    private readonly string _folder = templateConfig.CurrentValue.TemplatesFolder;
+    private readonly StubbleVisitorRenderer _renderer = new StubbleBuilder().Build();
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Email/TemplateRender.cs
user: login: elizavetapavliv
body: Why it's a field? Singleton classes can't store state. 
created_at: 2025-05-08T06:04:10Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,26 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.ReportHub.Configs;
+using Microsoft.Extensions.Options;
+using Stubble.Core;
+using Stubble.Core.Builders;
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Email/TemplateRender.cs
user: login: elizavetapavliv
body: The description of task included to use pure Mustache package :) but ok, let's leave as it is
created_at: 2025-05-08T06:04:40Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,9 @@
+﻿using System.Net.Mail;
+using Exadel.ReportHub.Email.Models;
+
+namespace Exadel.ReportHub.Email.Abstract;
+
+public interface IEmailSender
+{
+    Task SendAsync(string to, string subject, string templateName, object viewModel, CancellationToken cancellationToken);
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Abstract/IEmailSender.cs
user: login: elizavetapavliv
body: viewModel -> data
created_at: 2025-05-08T07:31:27Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,70 @@
+﻿using System.Net.Mail;
+using System.Net.Mime;
+using AutoMapper;
+using ErrorOr;
+using Exadel.ReportHub.Common.Providers;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Models;
+using Exadel.ReportHub.Pdf;
+using Exadel.ReportHub.Pdf.Abstract;
+using Exadel.ReportHub.Pdf.Models;
+using Exadel.ReportHub.RA;
+using Exadel.ReportHub.RA.Abstract;
+using Exadel.ReportHub.SDK.DTOs.Item;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(Guid InvoiceId, string Email, string Subject) : IRequest<ErrorOr<TestEmailResult>>;
+public class TestEmailHandler(
+    IEmailSender emailSender,
+    IInvoiceRepository invoiceRepository,
+    IUserRepository userRepository,
+    IItemRepository itemRepository,
+    IClientRepository clientRepository,
+    ICustomerRepository customerRepository,
+    IMapper mapper,
+    IPdfInvoiceGenerator pdfInvoiceGenerator,
+    IUserProvider userProvider) : IRequestHandler<TestEmailRequest, ErrorOr<TestEmailResult>>
+{
+    public async Task<ErrorOr<TestEmailResult>> Handle(TestEmailRequest request, CancellationToken cancellationToken)
+    {
+        try
+        {
+            var invoice = await invoiceRepository.GetByIdAsync(request.InvoiceId, cancellationToken);
+            if (invoice is null)
+            {
+                return Error.NotFound();
+            }
+
+            var itemsTask = itemRepository.GetByIdsAsync(invoice.ItemIds, cancellationToken);
+            var clientTask = clientRepository.GetByIdAsync(invoice.ClientId, cancellationToken);
+            var customerTask = customerRepository.GetByIdAsync(invoice.CustomerId, cancellationToken);
+            await Task.WhenAll(itemsTask, clientTask, customerTask);
+
+            var invoiceModel = mapper.Map<InvoiceModel>(invoice);
+            invoiceModel.ClientName = clientTask.Result.Name;
+            invoiceModel.CustomerName = customerTask.Result.Name;
+            invoiceModel.Items = mapper.Map<IList<ItemDTO>>(itemsTask.Result);
+
+            var stream = await pdfInvoiceGenerator.GenerateAsync(invoiceModel, cancellationToken);
+            var attachment = new Attachment(stream, $"{invoice.InvoiceNumber}{Constants.File.Extension.Pdf}", MediaTypeNames.Application.Pdf);
+
+            var user = await userRepository.GetByIdAsync(userProvider.GetUserId(), cancellationToken);
+            var reportEmail = new ReportEmailModel
+            {
+                UserName = user.FullName,
+                StartDate = DateTime.Now.Date.ToString("dd.MM.yyyy"),
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: elizavetapavliv
body: cannot this formatting be applied in html template ?
created_at: 2025-05-08T08:54:57Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,70 @@
+﻿using System.Net.Mail;
+using System.Net.Mime;
+using AutoMapper;
+using ErrorOr;
+using Exadel.ReportHub.Common.Providers;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Models;
+using Exadel.ReportHub.Pdf;
+using Exadel.ReportHub.Pdf.Abstract;
+using Exadel.ReportHub.Pdf.Models;
+using Exadel.ReportHub.RA;
+using Exadel.ReportHub.RA.Abstract;
+using Exadel.ReportHub.SDK.DTOs.Item;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(Guid InvoiceId, string Email, string Subject) : IRequest<ErrorOr<TestEmailResult>>;
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: elizavetapavliv
body: you can now integrate your sender with Job which Jiorgi has implemented
created_at: 2025-05-08T08:55:37Z
author_association: OWNER
diff_hunk: @@ -33,4 +33,10 @@
     <ProjectReference Include="..\Exadel.ReportHub.RA\Exadel.ReportHub.RA.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Content Include="Deployment\Resources\EmailTemplates\*.*">
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Exadel.ReportHub.Host.csproj
user: login: elizavetapavliv
body: copy Deployment\Resources
created_at: 2025-05-08T08:58:22Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,15 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Exadel.ReportHub.Email;
+
+public static class Constants
+{
+    public static class ResourcePath
+    {
+        public static readonly string EmailTemplates = Path.Combine(AppContext.BaseDirectory, "Deployment", "Resources", "EmailTemplates");
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Email/Constants.cs
user: login: elizavetapavliv
body:  this AppContext.BaseDirectory should not be needed, relative path is enough
created_at: 2025-05-08T08:59:37Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,70 @@
+﻿using System.Net.Mail;
+using System.Net.Mime;
+using AutoMapper;
+using ErrorOr;
+using Exadel.ReportHub.Common.Providers;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Models;
+using Exadel.ReportHub.Pdf;
+using Exadel.ReportHub.Pdf.Abstract;
+using Exadel.ReportHub.Pdf.Models;
+using Exadel.ReportHub.RA;
+using Exadel.ReportHub.RA.Abstract;
+using Exadel.ReportHub.SDK.DTOs.Item;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(Guid InvoiceId, string Email, string Subject) : IRequest<ErrorOr<TestEmailResult>>;
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: mkknyaz
body: I'm working on it
created_at: 2025-05-08T09:04:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,70 @@
+﻿using System.Net.Mail;
+using System.Net.Mime;
+using AutoMapper;
+using ErrorOr;
+using Exadel.ReportHub.Common.Providers;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Models;
+using Exadel.ReportHub.Pdf;
+using Exadel.ReportHub.Pdf.Abstract;
+using Exadel.ReportHub.Pdf.Models;
+using Exadel.ReportHub.RA;
+using Exadel.ReportHub.RA.Abstract;
+using Exadel.ReportHub.SDK.DTOs.Item;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(Guid InvoiceId, string Email, string Subject) : IRequest<ErrorOr<TestEmailResult>>;
+public class TestEmailHandler(
+    IEmailSender emailSender,
+    IInvoiceRepository invoiceRepository,
+    IUserRepository userRepository,
+    IItemRepository itemRepository,
+    IClientRepository clientRepository,
+    ICustomerRepository customerRepository,
+    IMapper mapper,
+    IPdfInvoiceGenerator pdfInvoiceGenerator,
+    IUserProvider userProvider) : IRequestHandler<TestEmailRequest, ErrorOr<TestEmailResult>>
+{
+    public async Task<ErrorOr<TestEmailResult>> Handle(TestEmailRequest request, CancellationToken cancellationToken)
+    {
+        try
+        {
+            var invoice = await invoiceRepository.GetByIdAsync(request.InvoiceId, cancellationToken);
+            if (invoice is null)
+            {
+                return Error.NotFound();
+            }
+
+            var itemsTask = itemRepository.GetByIdsAsync(invoice.ItemIds, cancellationToken);
+            var clientTask = clientRepository.GetByIdAsync(invoice.ClientId, cancellationToken);
+            var customerTask = customerRepository.GetByIdAsync(invoice.CustomerId, cancellationToken);
+            await Task.WhenAll(itemsTask, clientTask, customerTask);
+
+            var invoiceModel = mapper.Map<InvoiceModel>(invoice);
+            invoiceModel.ClientName = clientTask.Result.Name;
+            invoiceModel.CustomerName = customerTask.Result.Name;
+            invoiceModel.Items = mapper.Map<IList<ItemDTO>>(itemsTask.Result);
+
+            var stream = await pdfInvoiceGenerator.GenerateAsync(invoiceModel, cancellationToken);
+            var attachment = new Attachment(stream, $"{invoice.InvoiceNumber}{Constants.File.Extension.Pdf}", MediaTypeNames.Application.Pdf);
+
+            var user = await userRepository.GetByIdAsync(userProvider.GetUserId(), cancellationToken);
+            var reportEmail = new ReportEmailModel
+            {
+                UserName = user.FullName,
+                StartDate = DateTime.Now.Date.ToString("dd.MM.yyyy"),
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: mkknyaz
body: Not sure, i'm found nothing about it
The data can be only passed, but not formatted
created_at: 2025-05-08T09:06:07Z
author_association: COLLABORATOR
diff_hunk: @@ -33,4 +33,10 @@
     <ProjectReference Include="..\Exadel.ReportHub.RA\Exadel.ReportHub.RA.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Content Include="Deployment\Resources\EmailTemplates\*.*">
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Exadel.ReportHub.Host.csproj
user: login: mkknyaz
body: What does you mean?
To copy the whole Resorces folder, instead of copying the EmailTemplates folder?
created_at: 2025-05-08T09:11:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,70 @@
+﻿using System.Net.Mail;
+using System.Net.Mime;
+using AutoMapper;
+using ErrorOr;
+using Exadel.ReportHub.Common.Providers;
+using Exadel.ReportHub.Email.Abstract;
+using Exadel.ReportHub.Email.Models;
+using Exadel.ReportHub.Pdf;
+using Exadel.ReportHub.Pdf.Abstract;
+using Exadel.ReportHub.Pdf.Models;
+using Exadel.ReportHub.RA;
+using Exadel.ReportHub.RA.Abstract;
+using Exadel.ReportHub.SDK.DTOs.Item;
+using MediatR;
+
+namespace Exadel.ReportHub.Handlers.TestEmail;
+
+public record TestEmailRequest(Guid InvoiceId, string Email, string Subject) : IRequest<ErrorOr<TestEmailResult>>;
+public class TestEmailHandler(
+    IEmailSender emailSender,
+    IInvoiceRepository invoiceRepository,
+    IUserRepository userRepository,
+    IItemRepository itemRepository,
+    IClientRepository clientRepository,
+    ICustomerRepository customerRepository,
+    IMapper mapper,
+    IPdfInvoiceGenerator pdfInvoiceGenerator,
+    IUserProvider userProvider) : IRequestHandler<TestEmailRequest, ErrorOr<TestEmailResult>>
+{
+    public async Task<ErrorOr<TestEmailResult>> Handle(TestEmailRequest request, CancellationToken cancellationToken)
+    {
+        try
+        {
+            var invoice = await invoiceRepository.GetByIdAsync(request.InvoiceId, cancellationToken);
+            if (invoice is null)
+            {
+                return Error.NotFound();
+            }
+
+            var itemsTask = itemRepository.GetByIdsAsync(invoice.ItemIds, cancellationToken);
+            var clientTask = clientRepository.GetByIdAsync(invoice.ClientId, cancellationToken);
+            var customerTask = customerRepository.GetByIdAsync(invoice.CustomerId, cancellationToken);
+            await Task.WhenAll(itemsTask, clientTask, customerTask);
+
+            var invoiceModel = mapper.Map<InvoiceModel>(invoice);
+            invoiceModel.ClientName = clientTask.Result.Name;
+            invoiceModel.CustomerName = customerTask.Result.Name;
+            invoiceModel.Items = mapper.Map<IList<ItemDTO>>(itemsTask.Result);
+
+            var stream = await pdfInvoiceGenerator.GenerateAsync(invoiceModel, cancellationToken);
+            var attachment = new Attachment(stream, $"{invoice.InvoiceNumber}{Constants.File.Extension.Pdf}", MediaTypeNames.Application.Pdf);
+
+            var user = await userRepository.GetByIdAsync(userProvider.GetUserId(), cancellationToken);
+            var reportEmail = new ReportEmailModel
+            {
+                UserName = user.FullName,
+                StartDate = DateTime.Now.Date.ToString("dd.MM.yyyy"),
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/TestEmail/TestEmailHandler.cs
user: login: elizavetapavliv
body: then add private method with formatting, so that it'll be reused for start/end dates
created_at: 2025-05-08T09:16:34Z
author_association: OWNER
diff_hunk: @@ -33,4 +33,10 @@
     <ProjectReference Include="..\Exadel.ReportHub.RA\Exadel.ReportHub.RA.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Content Include="Deployment\Resources\EmailTemplates\*.*">
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Host/Exadel.ReportHub.Host.csproj
user: login: elizavetapavliv
body: yes
created_at: 2025-05-08T09:27:53Z
author_association: OWNER
diff_hunk: @@ -18,6 +25,31 @@ public async Task<Unit> Handle(SendReportsRequest request, CancellationToken can
         var currentDayOfWeek = now.DayOfWeek;
         var users = await userRepository.GetUsersByNotificationSettingsAsync(currentDay, currentDayOfWeek, currentHour, cancellationToken);
 
+        foreach (var user in users)
path: Projects/Exadel.ReportHub/Exadel.ReportHub.Handlers/Report/Send/SendReportsHandler.cs
user: login: elizavetapavliv
body: make it in parallel
created_at: 2025-05-08T13:19:08Z
author_association: OWNER

PR comments:
user: login: mkknyaz
created_at: 2025-05-07T23:24:34Z
body: ![image](https://github.com/user-attachments/assets/4ab47281-5a19-413f-9490-af66d77e833d)
![image](https://github.com/user-attachments/assets/3cc00723-bfcb-481c-b372-33e431d23660)

author_association: COLLABORATOR
user: login: elizavetapavliv
created_at: 2025-05-08T05:57:02Z
body: > ![image](https://github.com/user-attachments/assets/4ab47281-5a19-413f-9490-af66d77e833d)
> 
> ![image](https://github.com/user-attachments/assets/3cc00723-bfcb-481c-b372-33e431d23660)
> 
> 

Let's maybe change red Color to blue one, something more calm :)

Is report attached in case of success?
author_association: OWNER
user: login: mkknyaz
created_at: 2025-05-08T08:13:05Z
body: ![image](https://github.com/user-attachments/assets/763e3214-47d1-48de-bedb-a945f6eaa83e)

author_association: COLLABORATOR