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
87 changes: 87 additions & 0 deletions src/Voltiq.API/Controllers/Materials/MaterialsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Voltiq.Application.Features.Materials;
using Voltiq.Application.Features.Materials.Commands.DeleteMaterial;
using Voltiq.Application.Features.Materials.Commands.RegisterMaterial;
using Voltiq.Application.Features.Materials.Commands.UpdateMaterial;
using Voltiq.Application.Features.Materials.Queries.GetMaterialById;
using Voltiq.Application.Features.Materials.Queries.GetMaterials;
using Voltiq.Application.Mappings.Materials;

namespace Voltiq.API.Controllers.Materials;

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/materials")]
public sealed class MaterialsController : BaseApiController
{
/// <summary>Registers a new material for the authenticated user.</summary>
[HttpPost]
[ProducesResponseType(typeof(MaterialResponse), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Register(
[FromBody] RegisterMaterialRequest request,
CancellationToken cancellationToken)
{
var result = await Sender.Send(request.ToCommand(), cancellationToken);

return result.Match(
material => CreatedAtAction(nameof(GetById), new { id = material.Id }, material),
ToErrorResult);
}

/// <summary>Returns all materials belonging to the authenticated user.</summary>
[HttpGet]
[ProducesResponseType(typeof(IReadOnlyList<MaterialResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetAll(CancellationToken cancellationToken)
{
var result = await Sender.Send(new GetMaterialsQuery(), cancellationToken);

return result.Match(Ok, ToErrorResult);
}

/// <summary>Returns a specific material by ID (must belong to the authenticated user).</summary>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(MaterialResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(
Guid id,
CancellationToken cancellationToken)
{
var result = await Sender.Send(new GetMaterialByIdQuery(id), cancellationToken);

return result.Match(Ok, ToErrorResult);
}

/// <summary>Updates a material (must belong to the authenticated user).</summary>
[HttpPut("{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(
Guid id,
[FromBody] UpdateMaterialRequest request,
CancellationToken cancellationToken)
{
var result = await Sender.Send(request.ToCommand(id), cancellationToken);

return result.Match(_ => NoContent(), ToErrorResult);
}

/// <summary>Deletes a material (must belong to the authenticated user).</summary>
[HttpDelete("{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(
Guid id,
CancellationToken cancellationToken)
{
var result = await Sender.Send(new DeleteMaterialCommand(id), cancellationToken);

return result.Match(_ => NoContent(), ToErrorResult);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ErrorOr;
using Voltiq.Application.Common.Interfaces;

namespace Voltiq.Application.Features.Materials.Commands.DeleteMaterial;

public sealed record DeleteMaterialCommand(Guid Id) : IAuthenticatedRequest<ErrorOr<Deleted>>
{
public Guid UserId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using ErrorOr;
using MediatR;
using Voltiq.Domain.Interfaces;
using Voltiq.Domain.Interfaces.Repositories.Material;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Materials.Commands.DeleteMaterial;

public sealed class DeleteMaterialCommandHandler(
IMaterialUpdateOnlyRepository materialUpdateOnlyRepository,
IUnitOfWork unitOfWork)
: IRequestHandler<DeleteMaterialCommand, ErrorOr<Deleted>>
{
public async Task<ErrorOr<Deleted>> Handle(DeleteMaterialCommand request,
CancellationToken cancellationToken)
{
var material = await materialUpdateOnlyRepository.GetByIdAndUserIdAsync(
request.Id, request.UserId, cancellationToken);

if (material is null)
return Error.NotFound(description: ResourceErrorMessages.MATERIAL_NAO_ENCONTRADO);

materialUpdateOnlyRepository.Remove(material);
await unitOfWork.SaveChangesAsync(cancellationToken);

return Result.Deleted;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using ErrorOr;
using Voltiq.Application.Common.Interfaces;
using Voltiq.Domain.Enums;

namespace Voltiq.Application.Features.Materials.Commands.RegisterMaterial;

public sealed record RegisterMaterialCommand(
string Name,
decimal DefaultPrice,
MaterialUnit Unit) : IAuthenticatedRequest<ErrorOr<MaterialResponse>>
{
public Guid UserId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Mappings.Materials;
using Voltiq.Domain.Entities;
using Voltiq.Domain.Interfaces;
using Voltiq.Domain.Interfaces.Repositories.Material;

namespace Voltiq.Application.Features.Materials.Commands.RegisterMaterial;

public sealed class RegisterMaterialCommandHandler(
IMaterialWriteOnlyRepository materialWriteOnlyRepository,
IUnitOfWork unitOfWork)
: IRequestHandler<RegisterMaterialCommand, ErrorOr<MaterialResponse>>
{
public async Task<ErrorOr<MaterialResponse>> Handle(RegisterMaterialCommand request,
CancellationToken cancellationToken)
{
var material = Material.Register(request.UserId, request.Name, request.DefaultPrice, request.Unit);

await materialWriteOnlyRepository.AddAsync(material, cancellationToken);
await unitOfWork.SaveChangesAsync(cancellationToken);

return material.ToResponse();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FluentValidation;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Materials.Commands.RegisterMaterial;

public sealed class RegisterMaterialCommandValidator : AbstractValidator<RegisterMaterialCommand>
{
public RegisterMaterialCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage(ResourceErrorMessages.MATERIAL_NOME_OBRIGATORIO);

RuleFor(x => x.DefaultPrice)
.GreaterThan(0).WithMessage(ResourceErrorMessages.MATERIAL_PRECO_INVALIDO);

RuleFor(x => x.Unit)
.IsInEnum().WithMessage(ResourceErrorMessages.MATERIAL_UNIDADE_INVALIDA);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Voltiq.Domain.Enums;

namespace Voltiq.Application.Features.Materials.Commands.RegisterMaterial;

public sealed record RegisterMaterialRequest(
string Name,
decimal DefaultPrice,
MaterialUnit Unit);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using ErrorOr;
using Voltiq.Application.Common.Interfaces;
using Voltiq.Domain.Enums;

namespace Voltiq.Application.Features.Materials.Commands.UpdateMaterial;

public sealed record UpdateMaterialCommand(
Guid Id,
string Name,
decimal DefaultPrice,
MaterialUnit Unit) : IAuthenticatedRequest<ErrorOr<Updated>>
{
public Guid UserId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using ErrorOr;
using MediatR;
using Voltiq.Domain.Interfaces;
using Voltiq.Domain.Interfaces.Repositories.Material;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Materials.Commands.UpdateMaterial;

public sealed class UpdateMaterialCommandHandler(
IMaterialUpdateOnlyRepository materialUpdateOnlyRepository,
IUnitOfWork unitOfWork)
: IRequestHandler<UpdateMaterialCommand, ErrorOr<Updated>>
{
public async Task<ErrorOr<Updated>> Handle(UpdateMaterialCommand request,
CancellationToken cancellationToken)
{
var material = await materialUpdateOnlyRepository.GetByIdAndUserIdAsync(
request.Id, request.UserId, cancellationToken);

if (material is null)
return Error.NotFound(description: ResourceErrorMessages.MATERIAL_NAO_ENCONTRADO);

material.Update(request.Name, request.DefaultPrice, request.Unit);

await unitOfWork.SaveChangesAsync(cancellationToken);

return Result.Updated;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FluentValidation;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Materials.Commands.UpdateMaterial;

public sealed class UpdateMaterialCommandValidator : AbstractValidator<UpdateMaterialCommand>
{
public UpdateMaterialCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage(ResourceErrorMessages.MATERIAL_NOME_OBRIGATORIO);

RuleFor(x => x.DefaultPrice)
.GreaterThan(0).WithMessage(ResourceErrorMessages.MATERIAL_PRECO_INVALIDO);

RuleFor(x => x.Unit)
.IsInEnum().WithMessage(ResourceErrorMessages.MATERIAL_UNIDADE_INVALIDA);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Voltiq.Domain.Enums;

namespace Voltiq.Application.Features.Materials.Commands.UpdateMaterial;

public sealed record UpdateMaterialRequest(
string Name,
decimal DefaultPrice,
MaterialUnit Unit);
10 changes: 10 additions & 0 deletions src/Voltiq.Application/Features/Materials/MaterialResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Voltiq.Domain.Enums;

namespace Voltiq.Application.Features.Materials;

public sealed record MaterialResponse(
Guid Id,
string Name,
decimal DefaultPrice,
MaterialUnit Unit,
bool IsActive);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ErrorOr;
using Voltiq.Application.Common.Interfaces;

namespace Voltiq.Application.Features.Materials.Queries.GetMaterialById;

public sealed record GetMaterialByIdQuery(Guid Id) : IAuthenticatedRequest<ErrorOr<MaterialResponse>>
{
public Guid UserId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Mappings.Materials;
using Voltiq.Domain.Interfaces.Repositories.Material;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Materials.Queries.GetMaterialById;

public sealed class GetMaterialByIdQueryHandler(IMaterialReadOnlyRepository materialReadOnlyRepository)
: IRequestHandler<GetMaterialByIdQuery, ErrorOr<MaterialResponse>>
{
public async Task<ErrorOr<MaterialResponse>> Handle(GetMaterialByIdQuery request,
CancellationToken cancellationToken)
{
var material = await materialReadOnlyRepository.GetByIdAndUserIdAsync(
request.Id, request.UserId, cancellationToken);

if (material is null)
return Error.NotFound(description: ResourceErrorMessages.MATERIAL_NAO_ENCONTRADO);

return material.ToResponse();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ErrorOr;
using Voltiq.Application.Common.Interfaces;

namespace Voltiq.Application.Features.Materials.Queries.GetMaterials;

public sealed record GetMaterialsQuery : IAuthenticatedRequest<ErrorOr<IReadOnlyList<MaterialResponse>>>
{
public Guid UserId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Mappings.Materials;
using Voltiq.Domain.Interfaces.Repositories.Material;

namespace Voltiq.Application.Features.Materials.Queries.GetMaterials;

public sealed class GetMaterialsQueryHandler(IMaterialReadOnlyRepository materialReadOnlyRepository)
: IRequestHandler<GetMaterialsQuery, ErrorOr<IReadOnlyList<MaterialResponse>>>
{
public async Task<ErrorOr<IReadOnlyList<MaterialResponse>>> Handle(GetMaterialsQuery request,
CancellationToken cancellationToken)
{
var materials = await materialReadOnlyRepository.GetByUserIdAsync(request.UserId, cancellationToken);

return materials.Select(m => m.ToResponse()).ToList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Voltiq.Application.Features.Materials;
using Voltiq.Application.Features.Materials.Commands.RegisterMaterial;
using Voltiq.Application.Features.Materials.Commands.UpdateMaterial;
using Voltiq.Domain.Entities;

namespace Voltiq.Application.Mappings.Materials;

public static class MaterialMappingExtensions
{
extension(RegisterMaterialRequest request)
{
public RegisterMaterialCommand ToCommand() =>
new(request.Name, request.DefaultPrice, request.Unit);
}

extension(UpdateMaterialRequest request)
{
public UpdateMaterialCommand ToCommand(Guid id) =>
new(id, request.Name, request.DefaultPrice, request.Unit);
}

extension(Material material)
{
public MaterialResponse ToResponse() =>
new(material.Id, material.Name, material.DefaultPrice, material.Unit, material.IsActive);
}
}
16 changes: 16 additions & 0 deletions src/Voltiq.Domain/Entities/Material.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,20 @@ public static Material Register(Guid userId, string name, decimal defaultPrice,
public void Deactivate() => IsActive = false;

public void Activate() => IsActive = true;

public void Update(string name, decimal defaultPrice, MaterialUnit unit)
{
if (string.IsNullOrWhiteSpace(name))
throw new DomainException(ResourceErrorMessages.MATERIAL_NOME_OBRIGATORIO);

if (!Enum.IsDefined(unit))
throw new DomainException(ResourceErrorMessages.MATERIAL_UNIDADE_INVALIDA);

if (defaultPrice <= 0)
throw new DomainException(ResourceErrorMessages.MATERIAL_PRECO_INVALIDO);

Name = name.Trim();
DefaultPrice = defaultPrice;
Unit = unit;
}
}
Loading
Loading