From a26f41af187bb6ac208c5a0ca1e1751636da9073 Mon Sep 17 00:00:00 2001 From: Anatolii Grynchuk Date: Sat, 2 May 2026 01:18:51 +0300 Subject: [PATCH] feat: add API controllers for template and channel management - ApiResponse, ApiError in Api/Infrastructure - ApiControllerBase with IMediator, FromServiceResult, MapServiceError - EmailTemplatesController: GET list, GET one, POST, PUT, DELETE - EmailChannelsController: GET list, GET one, POST, PUT, DELETE Ref: IT-628 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Controllers/ApiControllerBase.cs | 38 +++++++++ .../CreateEmailChannelRequest.cs | 14 ++++ .../EmailChannels/EmailChannelsController.cs | 75 +++++++++++++++++ .../UpdateEmailChannelRequest.cs | 12 +++ .../CreateEmailTemplateRequest.cs | 13 +++ .../EmailTemplatesController.cs | 83 +++++++++++++++++++ .../UpdateEmailTemplateRequest.cs | 10 +++ .../Infrastructure/ApiResponse.cs | 14 ++++ 8 files changed, 259 insertions(+) create mode 100644 HrynCo.NotificationService.Api/Controllers/ApiControllerBase.cs create mode 100644 HrynCo.NotificationService.Api/Controllers/EmailChannels/CreateEmailChannelRequest.cs create mode 100644 HrynCo.NotificationService.Api/Controllers/EmailChannels/EmailChannelsController.cs create mode 100644 HrynCo.NotificationService.Api/Controllers/EmailChannels/UpdateEmailChannelRequest.cs create mode 100644 HrynCo.NotificationService.Api/Controllers/EmailTemplates/CreateEmailTemplateRequest.cs create mode 100644 HrynCo.NotificationService.Api/Controllers/EmailTemplates/EmailTemplatesController.cs create mode 100644 HrynCo.NotificationService.Api/Controllers/EmailTemplates/UpdateEmailTemplateRequest.cs create mode 100644 HrynCo.NotificationService.Api/Infrastructure/ApiResponse.cs diff --git a/HrynCo.NotificationService.Api/Controllers/ApiControllerBase.cs b/HrynCo.NotificationService.Api/Controllers/ApiControllerBase.cs new file mode 100644 index 0000000..aecb4a9 --- /dev/null +++ b/HrynCo.NotificationService.Api/Controllers/ApiControllerBase.cs @@ -0,0 +1,38 @@ +using HrynCo.NotificationService.Api.Infrastructure; +using HrynCo.NotificationService.Services.Core; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace HrynCo.NotificationService.Api.Controllers; + +[Route("api/v1/[controller]")] +[ApiController] +public abstract class ApiControllerBase : ControllerBase +{ + protected ApiControllerBase(IMediator mediator) + { + Mediator = mediator; + } + + protected IMediator Mediator { get; } + + protected IActionResult FromServiceResult(ServiceResult result) => + result.IsSuccess ? Ok(result.Result) : MapServiceError(result.Error!); + + protected IActionResult MapServiceError(ServiceError error) + { + string code = error.Code?.ToString() ?? "Unknown"; + + return error.Code switch + { + ServiceErrorCode.NotFound => NotFound(ErrorResponse(code, error.Message)), + ServiceErrorCode.Conflict => Conflict(ErrorResponse(code, error.Message)), + ServiceErrorCode.InvalidRequest => BadRequest(ErrorResponse(code, error.Message)), + null => throw new InvalidOperationException("Error code was null for failed result."), + _ => throw new ArgumentOutOfRangeException(nameof(error), error, "Unexpected error code.") + }; + } + + private static ApiResponse ErrorResponse(string code, string message) => + new() { Success = false, Error = new ApiError { Code = code, Message = message } }; +} diff --git a/HrynCo.NotificationService.Api/Controllers/EmailChannels/CreateEmailChannelRequest.cs b/HrynCo.NotificationService.Api/Controllers/EmailChannels/CreateEmailChannelRequest.cs new file mode 100644 index 0000000..c665143 --- /dev/null +++ b/HrynCo.NotificationService.Api/Controllers/EmailChannels/CreateEmailChannelRequest.cs @@ -0,0 +1,14 @@ +using HrynCo.NotificationService.DAL.Abstract.Providers; + +namespace HrynCo.NotificationService.Api.Controllers.EmailChannels; + +public sealed record CreateEmailChannelRequest( + string ServiceName, + int Priority, + EmailChannelType ChannelType, + EmailChannelSettings Settings, + int? DailyLimit, + int? MonthlyLimit, + int WarnThresholdPercent, + bool IsActive +); diff --git a/HrynCo.NotificationService.Api/Controllers/EmailChannels/EmailChannelsController.cs b/HrynCo.NotificationService.Api/Controllers/EmailChannels/EmailChannelsController.cs new file mode 100644 index 0000000..3cab68f --- /dev/null +++ b/HrynCo.NotificationService.Api/Controllers/EmailChannels/EmailChannelsController.cs @@ -0,0 +1,75 @@ +using HrynCo.NotificationService.Services.EmailChannels.Create; +using HrynCo.NotificationService.Services.EmailChannels.Delete; +using HrynCo.NotificationService.Services.EmailChannels.Get; +using HrynCo.NotificationService.Services.EmailChannels.GetByService; +using HrynCo.NotificationService.Services.EmailChannels.Update; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace HrynCo.NotificationService.Api.Controllers.EmailChannels; + +[Route("api/v1/email-channels")] +public sealed class EmailChannelsController : ApiControllerBase +{ + public EmailChannelsController(IMediator mediator) : base(mediator) { } + + [HttpGet] + public async Task GetAll([FromQuery] string serviceName, CancellationToken cancellationToken) + { + var result = await Mediator.Send(new GetEmailChannelsQuery(serviceName), cancellationToken); + return FromServiceResult(result); + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid id, CancellationToken cancellationToken) + { + var result = await Mediator.Send(new GetEmailChannelQuery(id), cancellationToken); + return FromServiceResult(result); + } + + [HttpPost] + public async Task Create([FromBody] CreateEmailChannelRequest request, CancellationToken cancellationToken) + { + var command = new CreateEmailChannelCommand( + request.ServiceName, + request.Priority, + request.ChannelType, + request.Settings, + request.DailyLimit, + request.MonthlyLimit, + request.WarnThresholdPercent, + request.IsActive + ); + + var result = await Mediator.Send(command, cancellationToken); + + if (!result.IsSuccess) + return MapServiceError(result.Error!); + + return CreatedAtAction(nameof(Get), new { id = result.Result }, result.Result); + } + + [HttpPut("{id:guid}")] + public async Task Update(Guid id, [FromBody] UpdateEmailChannelRequest request, CancellationToken cancellationToken) + { + var command = new UpdateEmailChannelCommand( + id, + request.Priority, + request.Settings, + request.DailyLimit, + request.MonthlyLimit, + request.WarnThresholdPercent, + request.IsActive + ); + + var result = await Mediator.Send(command, cancellationToken); + return FromServiceResult(result); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id, CancellationToken cancellationToken) + { + var result = await Mediator.Send(new DeleteEmailChannelCommand(id), cancellationToken); + return FromServiceResult(result); + } +} diff --git a/HrynCo.NotificationService.Api/Controllers/EmailChannels/UpdateEmailChannelRequest.cs b/HrynCo.NotificationService.Api/Controllers/EmailChannels/UpdateEmailChannelRequest.cs new file mode 100644 index 0000000..caed07f --- /dev/null +++ b/HrynCo.NotificationService.Api/Controllers/EmailChannels/UpdateEmailChannelRequest.cs @@ -0,0 +1,12 @@ +using HrynCo.NotificationService.DAL.Abstract.Providers; + +namespace HrynCo.NotificationService.Api.Controllers.EmailChannels; + +public sealed record UpdateEmailChannelRequest( + int Priority, + EmailChannelSettings Settings, + int? DailyLimit, + int? MonthlyLimit, + int WarnThresholdPercent, + bool IsActive +); diff --git a/HrynCo.NotificationService.Api/Controllers/EmailTemplates/CreateEmailTemplateRequest.cs b/HrynCo.NotificationService.Api/Controllers/EmailTemplates/CreateEmailTemplateRequest.cs new file mode 100644 index 0000000..ff7c68a --- /dev/null +++ b/HrynCo.NotificationService.Api/Controllers/EmailTemplates/CreateEmailTemplateRequest.cs @@ -0,0 +1,13 @@ +using HrynCo.NotificationService.DAL.Abstract.Templates; + +namespace HrynCo.NotificationService.Api.Controllers.EmailTemplates; + +public sealed record CreateEmailTemplateRequest( + string ServiceName, + string Key, + string LanguageCode, + string Subject, + string HtmlBody, + string TextBody, + IReadOnlyList Variables +); diff --git a/HrynCo.NotificationService.Api/Controllers/EmailTemplates/EmailTemplatesController.cs b/HrynCo.NotificationService.Api/Controllers/EmailTemplates/EmailTemplatesController.cs new file mode 100644 index 0000000..984752b --- /dev/null +++ b/HrynCo.NotificationService.Api/Controllers/EmailTemplates/EmailTemplatesController.cs @@ -0,0 +1,83 @@ +using HrynCo.NotificationService.Services.EmailTemplates.Create; +using HrynCo.NotificationService.Services.EmailTemplates.Delete; +using HrynCo.NotificationService.Services.EmailTemplates.Get; +using HrynCo.NotificationService.Services.EmailTemplates.GetByService; +using HrynCo.NotificationService.Services.EmailTemplates.Update; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace HrynCo.NotificationService.Api.Controllers.EmailTemplates; + +[Route("api/v1/email-templates")] +public sealed class EmailTemplatesController : ApiControllerBase +{ + public EmailTemplatesController(IMediator mediator) : base(mediator) { } + + [HttpGet] + public async Task GetAll([FromQuery] string serviceName, CancellationToken cancellationToken) + { + var result = await Mediator.Send(new GetEmailTemplatesQuery(serviceName), cancellationToken); + return FromServiceResult(result); + } + + [HttpGet("{serviceName}/{key}/{languageCode}")] + public async Task Get(string serviceName, string key, string languageCode, CancellationToken cancellationToken) + { + var result = await Mediator.Send(new GetEmailTemplateQuery(serviceName, key, languageCode), cancellationToken); + return FromServiceResult(result); + } + + [HttpPost] + public async Task Create([FromBody] CreateEmailTemplateRequest request, CancellationToken cancellationToken) + { + var command = new CreateEmailTemplateCommand( + request.ServiceName, + request.Key, + request.LanguageCode, + request.Subject, + request.HtmlBody, + request.TextBody, + request.Variables + ); + + var result = await Mediator.Send(command, cancellationToken); + + if (!result.IsSuccess) + return MapServiceError(result.Error!); + + return CreatedAtAction( + nameof(Get), + new { serviceName = request.ServiceName, key = request.Key, languageCode = request.LanguageCode }, + result.Result + ); + } + + [HttpPut("{serviceName}/{key}/{languageCode}")] + public async Task Update( + string serviceName, + string key, + string languageCode, + [FromBody] UpdateEmailTemplateRequest request, + CancellationToken cancellationToken) + { + var command = new UpdateEmailTemplateCommand( + serviceName, + key, + languageCode, + request.Subject, + request.HtmlBody, + request.TextBody, + request.Variables + ); + + var result = await Mediator.Send(command, cancellationToken); + return FromServiceResult(result); + } + + [HttpDelete("{serviceName}/{key}/{languageCode}")] + public async Task Delete(string serviceName, string key, string languageCode, CancellationToken cancellationToken) + { + var result = await Mediator.Send(new DeleteEmailTemplateCommand(serviceName, key, languageCode), cancellationToken); + return FromServiceResult(result); + } +} diff --git a/HrynCo.NotificationService.Api/Controllers/EmailTemplates/UpdateEmailTemplateRequest.cs b/HrynCo.NotificationService.Api/Controllers/EmailTemplates/UpdateEmailTemplateRequest.cs new file mode 100644 index 0000000..4b14033 --- /dev/null +++ b/HrynCo.NotificationService.Api/Controllers/EmailTemplates/UpdateEmailTemplateRequest.cs @@ -0,0 +1,10 @@ +using HrynCo.NotificationService.DAL.Abstract.Templates; + +namespace HrynCo.NotificationService.Api.Controllers.EmailTemplates; + +public sealed record UpdateEmailTemplateRequest( + string Subject, + string HtmlBody, + string TextBody, + IReadOnlyList Variables +); diff --git a/HrynCo.NotificationService.Api/Infrastructure/ApiResponse.cs b/HrynCo.NotificationService.Api/Infrastructure/ApiResponse.cs new file mode 100644 index 0000000..57adf75 --- /dev/null +++ b/HrynCo.NotificationService.Api/Infrastructure/ApiResponse.cs @@ -0,0 +1,14 @@ +namespace HrynCo.NotificationService.Api.Infrastructure; + +public sealed class ApiResponse +{ + public T? Data { get; init; } = default; + public ApiError? Error { get; init; } + public bool Success { get; init; } +} + +public sealed class ApiError +{ + public required string Code { get; init; } + public required string Message { get; init; } +}