Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ appsettings.Production.json
# Logs
logs/

# Documentation
docs/


# Copilot instructions
.github/copilot-instructions.md
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Voltiq.Application.Common.Interfaces.Queue;

public interface IQueueService
{
Task SendMessageAsync<T>(string queueName, T message, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Voltiq.Application.Common.Interfaces.Reports;

public class BudgetReportData
{
public Guid BudgetId { get; set; }
public string ClientName { get; set; } = string.Empty;
public string ProjectName { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Voltiq.Application.Common.Interfaces.Reports;

public interface IReportGenerator
{
Task<byte[]> GenerateAsync<TData>(TData data, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Voltiq.Application.Common.Interfaces.Reports;

public interface IReportStrategy<in TData>
{
Task<byte[]> GenerateAsync(TData data, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Voltiq.Application.Common.Interfaces.Storage;

public interface IStorageService
{
Task<string> UploadAsync(string fileName, byte[] data, string contentType, CancellationToken cancellationToken = default);
Task<string> GetSasUrlAsync(string fileName, int expirationInHours = 1, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Common.Interfaces.Queue;
using Voltiq.Domain.Interfaces.Repositories.Budget;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Budgets.Commands.GenerateBudgetPdf;

public record GenerateBudgetPdfCommand(Guid BudgetId) : IRequest<ErrorOr<Success>>;

public class GenerateBudgetPdfCommandHandler : IRequestHandler<GenerateBudgetPdfCommand, ErrorOr<Success>>
{
private readonly IQueueService _queueService;
private readonly IBudgetReadOnlyRepository _budgetRepository;

public GenerateBudgetPdfCommandHandler(IQueueService queueService, IBudgetReadOnlyRepository budgetRepository)
{
_queueService = queueService;
_budgetRepository = budgetRepository;
}

public async Task<ErrorOr<Success>> Handle(GenerateBudgetPdfCommand request, CancellationToken cancellationToken)
{
var budget = await _budgetRepository.GetByIdAsync(request.BudgetId, cancellationToken);
if (budget == null)
return Error.NotFound(description: ResourceErrorMessages.TITULO_NAO_ENCONTRADO);

var message = new { request.BudgetId };

await _queueService.SendMessageAsync("budget-reports", message, cancellationToken);

return Result.Success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Common.Interfaces.Storage;
using Voltiq.Domain.Interfaces.Repositories.Budget;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Budgets.Queries.GetBudgetPdfUrl;

public record GetBudgetPdfUrlQuery(Guid BudgetId) : IRequest<ErrorOr<string>>;

public class GetBudgetPdfUrlQueryHandler : IRequestHandler<GetBudgetPdfUrlQuery, ErrorOr<string>>
{
private readonly IStorageService _storageService;
private readonly IBudgetReadOnlyRepository _budgetRepository;

public GetBudgetPdfUrlQueryHandler(IStorageService storageService, IBudgetReadOnlyRepository budgetRepository)
{
_storageService = storageService;
_budgetRepository = budgetRepository;
}

public async Task<ErrorOr<string>> Handle(GetBudgetPdfUrlQuery request, CancellationToken cancellationToken)
{
var budget = await _budgetRepository.GetByIdAsync(request.BudgetId, cancellationToken);
if (budget == null)
return Error.NotFound(description: ResourceErrorMessages.TITULO_NAO_ENCONTRADO);

var fileName = $"budget-{request.BudgetId}.pdf";
var url = await _storageService.GetSasUrlAsync(fileName, 1, cancellationToken);

if (string.IsNullOrEmpty(url))
return Error.NotFound(code: "Budget.PdfNotGenerated", description: "O PDF ainda não foi gerado ou não está disponível.");

return url;
}
}
54 changes: 54 additions & 0 deletions src/Voltiq.Domain/Entities/Service.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Voltiq.Domain.Events;
using Voltiq.Exceptions.Exceptions;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Domain.Entities;

public sealed class Service : AuditableEntity
{
public Guid UserId { get; private set; }
public string Name { get; private set; } = null!;
public decimal BasePrice { get; private set; }
public bool IsActive { get; private set; }

private Service() { }

private Service(Guid userId, string name, decimal basePrice)
{
UserId = userId;
Name = name;
BasePrice = basePrice;
IsActive = true;
AddDomainEvent(new ServiceRegisteredEvent(Id));
}

public static Service Register(Guid userId, string name, decimal basePrice)
{
if (userId == Guid.Empty)
throw new DomainException(ResourceErrorMessages.SERVICE_USUARIO_OBRIGATORIO);

if (string.IsNullOrWhiteSpace(name))
throw new DomainException(ResourceErrorMessages.SERVICE_NOME_OBRIGATORIO);

if (basePrice <= 0)
throw new DomainException(ResourceErrorMessages.SERVICE_PRECO_INVALIDO);

return new Service(userId, name.Trim(), basePrice);
}

public void Deactivate() => IsActive = false;

public void Activate() => IsActive = true;

public void Update(string name, decimal basePrice)
{
if (string.IsNullOrWhiteSpace(name))
throw new DomainException(ResourceErrorMessages.SERVICE_NOME_OBRIGATORIO);

if (basePrice <= 0)
throw new DomainException(ResourceErrorMessages.SERVICE_PRECO_INVALIDO);

Name = name.Trim();
BasePrice = basePrice;
}
}
3 changes: 3 additions & 0 deletions src/Voltiq.Domain/Events/ServiceRegisteredEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Voltiq.Domain.Events;

public sealed record ServiceRegisteredEvent(Guid ServiceId) : BaseDomainEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Voltiq.Domain.Interfaces.Repositories.Service;

public interface IServiceReadOnlyRepository
{
Task<Entities.Service?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Entities.Service>> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default);
Task<Entities.Service?> GetByIdAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Entities.Service>> GetActiveByUserIdAsync(Guid userId, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Voltiq.Domain.Interfaces.Repositories.Service;

public interface IServiceUpdateOnlyRepository
{
Task<Entities.Service?> GetTrackedByIdAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default);
void Remove(Entities.Service entity);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Voltiq.Domain.Interfaces.Repositories.Service;

public interface IServiceWriteOnlyRepository
{
Task AddAsync(Entities.Service entity, CancellationToken cancellationToken = default);
}
36 changes: 36 additions & 0 deletions src/Voltiq.Exceptions/Resources/ResourceErrorMessages.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/Voltiq.Exceptions/Resources/ResourceErrorMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,21 @@
<value>Material não encontrado.</value>
</data>

<!-- ═══════════════════════════════ Domínio — Serviço ═══════════════════ -->

<data name="SERVICE_USUARIO_OBRIGATORIO" xml:space="preserve">
<value>O usuário do serviço é obrigatório.</value>
</data>
<data name="SERVICE_NOME_OBRIGATORIO" xml:space="preserve">
<value>O nome do serviço é obrigatório.</value>
</data>
<data name="SERVICE_PRECO_INVALIDO" xml:space="preserve">
<value>O preço base do serviço deve ser maior que zero.</value>
</data>
<data name="SERVICE_NAO_ENCONTRADO" xml:space="preserve">
<value>Serviço não encontrado.</value>
</data>

<!-- ═══════════════════════════════ Domínio — Orçamento ══════════════════ -->

<data name="ORCAMENTO_USUARIO_OBRIGATORIO" xml:space="preserve">
Expand Down
Loading
Loading