feat(api): wire IEmailTemplateService; replace Contact:ToEmail with OperatorCopy

- Add ProjectReference to email-api-data
- Register EmailApiDbContext (no migrate — email-api owns migrations)
- Register IEmailTemplateRepository (scoped) and IEmailTemplateService (singleton)
- EmailApiEmailSender: replace ITemplateService with IEmailTemplateService for
  all email.* template rendering (match body/subject/footer)
- SendMatchAsync: replace _contact.ToEmail operator copy with
  GetOperatorCopy("email.match.subject", "en") from DB template

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 08:43:07 +03:00
parent c415ab3957
commit e7ca6043b7
3 changed files with 30 additions and 10 deletions
+17
View File
@@ -1,6 +1,10 @@
using System.Reflection; using System.Reflection;
using Api.Services; using Api.Services;
using Api.Services.Contracts; using Api.Services.Contracts;
using EmailApi.Data;
using EmailApi.Data.Repositories;
using EmailApi.Data.Repositories.Contracts;
using EmailApi.Data.Services;
using EmailApi.Models.Clients; using EmailApi.Models.Clients;
using EmailApi.Models.Settings; using EmailApi.Models.Settings;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -47,6 +51,19 @@ try
}); });
builder.Services.AddSingleton<ITemplateService, DbTemplateService>(); builder.Services.AddSingleton<ITemplateService, DbTemplateService>();
builder.Services.AddDbContext<EmailApiDbContext>(options =>
{
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
options.UseSqlServer(connectionString, sql =>
{
sql.MigrationsHistoryTable(EmailApiDbContext.MigrationTableName, EmailApiDbContext.SchemaName);
sql.MigrationsAssembly("email-api-data");
});
});
builder.Services.AddScoped<IEmailTemplateRepository, EfEmailTemplateRepository>();
builder.Services.AddSingleton<IEmailTemplateService, EmailTemplateService>();
builder.Services.AddHttpClient<ICaptchaVerifier, RecaptchaVerifier>(); builder.Services.AddHttpClient<ICaptchaVerifier, RecaptchaVerifier>();
builder.Services.AddSingleton<IEmailSender, EmailApiEmailSender>(); builder.Services.AddSingleton<IEmailSender, EmailApiEmailSender>();
builder.Services.AddSingleton<Microsoft.AspNetCore.StaticFiles.IContentTypeProvider, Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider>(); builder.Services.AddSingleton<Microsoft.AspNetCore.StaticFiles.IContentTypeProvider, Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider>();
+12 -10
View File
@@ -1,11 +1,11 @@
using Api.Services.Contracts; using Api.Services.Contracts;
using CvMatcher.Models.Responses; using CvMatcher.Models.Responses;
using EmailApi.Data.Services;
using EmailApi.Models.Clients; using EmailApi.Models.Clients;
using EmailApi.Models.Requests; using EmailApi.Models.Requests;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Models.Requests; using Models.Requests;
using Models.Settings; using Models.Settings;
using MyAi.Data.Services;
namespace Api.Services; namespace Api.Services;
@@ -15,7 +15,7 @@ public sealed class EmailApiEmailSender : IEmailSender
private readonly ContactSettings _contact; private readonly ContactSettings _contact;
private readonly SubscribeSettings _subscribe; private readonly SubscribeSettings _subscribe;
private readonly FileStorageSettings _fileStorage; private readonly FileStorageSettings _fileStorage;
private readonly ITemplateService _templates; private readonly IEmailTemplateService _emailTemplates;
private readonly ILogger<EmailApiEmailSender> _log; private readonly ILogger<EmailApiEmailSender> _log;
public EmailApiEmailSender( public EmailApiEmailSender(
@@ -23,14 +23,14 @@ public sealed class EmailApiEmailSender : IEmailSender
IOptions<ContactSettings> contact, IOptions<ContactSettings> contact,
IOptions<SubscribeSettings> subscribe, IOptions<SubscribeSettings> subscribe,
IOptions<FileStorageSettings> fileStorage, IOptions<FileStorageSettings> fileStorage,
ITemplateService templates, IEmailTemplateService emailTemplates,
ILogger<EmailApiEmailSender> log) ILogger<EmailApiEmailSender> log)
{ {
_emailApi = emailApi; _emailApi = emailApi;
_contact = contact.Value; _contact = contact.Value;
_subscribe = subscribe.Value; _subscribe = subscribe.Value;
_fileStorage = fileStorage.Value; _fileStorage = fileStorage.Value;
_templates = templates; _emailTemplates = emailTemplates;
_log = log; _log = log;
} }
@@ -148,13 +148,15 @@ public sealed class EmailApiEmailSender : IEmailSender
public async Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct) public async Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct)
{ {
var operatorCopy = _emailTemplates.GetOperatorCopy("email.match.subject", "en");
var recipients = new List<string>(); var recipients = new List<string>();
if (!string.IsNullOrWhiteSpace(explicitTo)) if (!string.IsNullOrWhiteSpace(explicitTo))
recipients.Add(explicitTo); recipients.Add(explicitTo);
if (!string.IsNullOrWhiteSpace(_contact.ToEmail) && if (!string.IsNullOrWhiteSpace(operatorCopy) &&
!recipients.Any(x => string.Equals(x, _contact.ToEmail, StringComparison.OrdinalIgnoreCase))) !recipients.Any(x => string.Equals(x, operatorCopy, StringComparison.OrdinalIgnoreCase)))
recipients.Add(_contact.ToEmail); recipients.Add(operatorCopy);
if (recipients.Count == 0) if (recipients.Count == 0)
{ {
@@ -199,7 +201,7 @@ public sealed class EmailApiEmailSender : IEmailSender
string.Join("", result.Recommendations.Select(r => $"<li>{r}</li>")) + "</ul>" string.Join("", result.Recommendations.Select(r => $"<li>{r}</li>")) + "</ul>"
: "<p style=\"color:#6c757d\">—</p>"; : "<p style=\"color:#6c757d\">—</p>";
var body = _templates.Render("email.match.body", language, var body = _emailTemplates.Render("email.match.body", language,
("cvDocumentId", cvDocumentId), ("cvDocumentId", cvDocumentId),
("jobLabel", jobLabel ?? "N/A"), ("jobLabel", jobLabel ?? "N/A"),
("jobUrl", result.JobUrl ?? "N/A"), ("jobUrl", result.JobUrl ?? "N/A"),
@@ -211,7 +213,7 @@ public sealed class EmailApiEmailSender : IEmailSender
if (!string.IsNullOrWhiteSpace(jobSearchLink)) if (!string.IsNullOrWhiteSpace(jobSearchLink))
{ {
body += _templates.Render("email.match.job-search-footer", language, body += _emailTemplates.Render("email.match.job-search-footer", language,
("jobSearchLink", jobSearchLink), ("jobSearchLink", jobSearchLink),
("expiryDays", expiryDays.ToString())); ("expiryDays", expiryDays.ToString()));
} }
@@ -220,7 +222,7 @@ public sealed class EmailApiEmailSender : IEmailSender
} }
public string BuildMatchEmailSubject(int score, string? jobLabel, string language) => public string BuildMatchEmailSubject(int score, string? jobLabel, string language) =>
_templates.Render("email.match.subject", language, _emailTemplates.Render("email.match.subject", language,
("score", score.ToString()), ("score", score.ToString()),
("jobLabel", jobLabel ?? "Job")); ("jobLabel", jobLabel ?? "Job"));
} }
+1
View File
@@ -36,6 +36,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\api-models\api-models.csproj" /> <ProjectReference Include="..\api-models\api-models.csproj" />
<ProjectReference Include="..\email-api-data\email-api-data.csproj" />
<ProjectReference Include="..\email-api-models\email-api-models.csproj" /> <ProjectReference Include="..\email-api-models\email-api-models.csproj" />
<ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" /> <ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" />
<ProjectReference Include="..\common\common.csproj" /> <ProjectReference Include="..\common\common.csproj" />