diff --git a/.github/workflows/build_test.yaml b/.github/workflows/build_test.yaml index 665e5de..d4e0010 100644 --- a/.github/workflows/build_test.yaml +++ b/.github/workflows/build_test.yaml @@ -3,11 +3,11 @@ on: push: branches: - - master + - master pull_request: branches: - - '**' - + - '**' + jobs: build: name: Restore, Build and Test @@ -18,21 +18,21 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - + - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' - + - name: Restore Packages run: dotnet restore - + - name: Build run: dotnet build --configuration Release - + - name: Test - run: dotnet test --configuration Release --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage" - + run: dotnet test --configuration Release --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage" --settings coverlet.runsettings + - name: Combine Coverage Reports uses: danielpalme/ReportGenerator-GitHub-Action@5.2.4 with: @@ -43,7 +43,7 @@ jobs: title: "Code Coverage" tag: "${{ github.run_number }}_${{ github.run_id }}" toolpath: "reportgeneratortool" - + - name: Upload Combined Coverage XML uses: actions/upload-artifact@v4 with: diff --git a/StudioManager.API.Contracts/Common/NamedBaseDto.cs b/StudioManager.API.Contracts/Common/NamedBaseDto.cs new file mode 100644 index 0000000..64f46c6 --- /dev/null +++ b/StudioManager.API.Contracts/Common/NamedBaseDto.cs @@ -0,0 +1,7 @@ +namespace StudioManager.API.Contracts.Common; + +public class NamedBaseDto +{ + public Guid Id { get; init; } + public string Name { get; init; } = null!; +} diff --git a/StudioManager.API.Contracts/EquipmentTypes/EquipmentTypeReadDto.cs b/StudioManager.API.Contracts/EquipmentTypes/EquipmentTypeReadDto.cs index 29a408f..3329d16 100644 --- a/StudioManager.API.Contracts/EquipmentTypes/EquipmentTypeReadDto.cs +++ b/StudioManager.API.Contracts/EquipmentTypes/EquipmentTypeReadDto.cs @@ -1,3 +1,3 @@ namespace StudioManager.API.Contracts.EquipmentTypes; -public sealed record EquipmentTypeReadDto(Guid Id, string Name); \ No newline at end of file +public sealed record EquipmentTypeReadDto(Guid Id, string Name); diff --git a/StudioManager.API.Contracts/EquipmentTypes/EquipmentTypeWriteDto.cs b/StudioManager.API.Contracts/EquipmentTypes/EquipmentTypeWriteDto.cs index 8837ad6..7f6e974 100644 --- a/StudioManager.API.Contracts/EquipmentTypes/EquipmentTypeWriteDto.cs +++ b/StudioManager.API.Contracts/EquipmentTypes/EquipmentTypeWriteDto.cs @@ -1,3 +1,3 @@ namespace StudioManager.API.Contracts.EquipmentTypes; -public sealed record EquipmentTypeWriteDto(string Name); \ No newline at end of file +public sealed record EquipmentTypeWriteDto(string Name); diff --git a/StudioManager.API.Contracts/Equipments/EquipmentReadDto.cs b/StudioManager.API.Contracts/Equipments/EquipmentReadDto.cs index a73dc79..f871ea9 100644 --- a/StudioManager.API.Contracts/Equipments/EquipmentReadDto.cs +++ b/StudioManager.API.Contracts/Equipments/EquipmentReadDto.cs @@ -2,4 +2,10 @@ namespace StudioManager.API.Contracts.Equipments; -public sealed record EquipmentReadDto(Guid Id, string Name, int Quantity, int InitialQuantity, string ImageUrl, EquipmentTypeReadDto EquipmentType); \ No newline at end of file +public sealed record EquipmentReadDto( + Guid Id, + string Name, + int Quantity, + int InitialQuantity, + string ImageUrl, + EquipmentTypeReadDto EquipmentType); diff --git a/StudioManager.API.Contracts/Equipments/EquipmentWriteDto.cs b/StudioManager.API.Contracts/Equipments/EquipmentWriteDto.cs index b1258ea..c90a39e 100644 --- a/StudioManager.API.Contracts/Equipments/EquipmentWriteDto.cs +++ b/StudioManager.API.Contracts/Equipments/EquipmentWriteDto.cs @@ -1,3 +1,3 @@ namespace StudioManager.API.Contracts.Equipments; -public sealed record EquipmentWriteDto(string Name, Guid EquipmentTypeId, int Quantity/*, byte[] Image*/); \ No newline at end of file +public sealed record EquipmentWriteDto(string Name, Guid EquipmentTypeId, int Quantity /*, string ImageUrl*/); diff --git a/StudioManager.API.Contracts/Pagination/PaginationDetailsDto.cs b/StudioManager.API.Contracts/Pagination/PaginationDetailsDto.cs index a92c271..14d8a20 100644 --- a/StudioManager.API.Contracts/Pagination/PaginationDetailsDto.cs +++ b/StudioManager.API.Contracts/Pagination/PaginationDetailsDto.cs @@ -5,4 +5,4 @@ public sealed class PaginationDetailsDto public int Limit { get; set; } public int Page { get; set; } public int Total { get; set; } -} \ No newline at end of file +} diff --git a/StudioManager.API.Contracts/Pagination/PaginationDto.cs b/StudioManager.API.Contracts/Pagination/PaginationDto.cs index 759779c..bdf3830 100644 --- a/StudioManager.API.Contracts/Pagination/PaginationDto.cs +++ b/StudioManager.API.Contracts/Pagination/PaginationDto.cs @@ -4,17 +4,30 @@ namespace StudioManager.API.Contracts.Pagination; public sealed class PaginationDto { - [Range(0, int.MaxValue)] public int Limit { get; private init; } = 25; + public const int DefaultLimit = 50; + public const int DefaultPage = 1; - [Range(0, int.MaxValue)] public int Page { get; private init; } = 1; + private int? _limit; + private int? _page; - public int GetOffset() + [Range(0, int.MaxValue)] + public int? Limit + { + get => _limit ?? DefaultLimit; + set => _limit = value; + } + + [Range(0, int.MaxValue)] + public int? Page { - return Limit * (Page - 1); + get => _page ?? DefaultPage; + set => _page = value; } - public static PaginationDto Default() + public int GetOffset() { - return new PaginationDto { Limit = 25, Page = 1 }; + if (!Page.HasValue) return 0; + + return Limit!.Value * (Page!.Value - 1); } -} \ No newline at end of file +} diff --git a/StudioManager.API.Contracts/Pagination/PagingResultDto.cs b/StudioManager.API.Contracts/Pagination/PagingResultDto.cs index da65f59..a23f873 100644 --- a/StudioManager.API.Contracts/Pagination/PagingResultDto.cs +++ b/StudioManager.API.Contracts/Pagination/PagingResultDto.cs @@ -4,4 +4,4 @@ public sealed class PagingResultDto { public List Data { get; set; } = default!; public PaginationDetailsDto Pagination { get; set; } = default!; -} \ No newline at end of file +} diff --git a/StudioManager.API.Contracts/Reservations/ReservationReadDto.cs b/StudioManager.API.Contracts/Reservations/ReservationReadDto.cs new file mode 100644 index 0000000..e6b21ec --- /dev/null +++ b/StudioManager.API.Contracts/Reservations/ReservationReadDto.cs @@ -0,0 +1,10 @@ +using StudioManager.API.Contracts.Common; + +namespace StudioManager.API.Contracts.Reservations; + +public sealed record ReservationReadDto( + Guid Id, + DateOnly StartDate, + DateOnly EndDate, + int Quantity, + NamedBaseDto Equipment); diff --git a/StudioManager.API.Contracts/Reservations/ReservationWriteDto.cs b/StudioManager.API.Contracts/Reservations/ReservationWriteDto.cs new file mode 100644 index 0000000..6fbc2bc --- /dev/null +++ b/StudioManager.API.Contracts/Reservations/ReservationWriteDto.cs @@ -0,0 +1,3 @@ +namespace StudioManager.API.Contracts.Reservations; + +public sealed record ReservationWriteDto(DateOnly StartDate, DateOnly EndDate, int Quantity, Guid EquipmentId); diff --git a/StudioManager.API.Contracts/StudioManager.API.Contracts.csproj b/StudioManager.API.Contracts/StudioManager.API.Contracts.csproj index dd3257e..3a63532 100644 --- a/StudioManager.API.Contracts/StudioManager.API.Contracts.csproj +++ b/StudioManager.API.Contracts/StudioManager.API.Contracts.csproj @@ -6,11 +6,4 @@ enable - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/StudioManager.API/BackgroundServices/FinishedReservationsBackgroundService.cs b/StudioManager.API/BackgroundServices/FinishedReservationsBackgroundService.cs new file mode 100644 index 0000000..4838a28 --- /dev/null +++ b/StudioManager.API/BackgroundServices/FinishedReservationsBackgroundService.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Domain.Filters; +using StudioManager.Infrastructure; +using StudioManager.Infrastructure.Common; +using StudioManager.Notifications.Equipment; + +namespace StudioManager.API.BackgroundServices; + +[ExcludeFromCodeCoverage] +//TODO: Create read lock for this service +public sealed class FinishedReservationsBackgroundService( + IDbContextFactory dbContextFactory) + : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromDays(1), stoppingToken); + + await using var dbContext = await dbContextFactory.CreateDbContextAsync(stoppingToken); + { + await ReturnReservationsAsync(dbContext, stoppingToken); + } + } + } + + private static async Task ReturnReservationsAsync(DbContextBase dbContext, CancellationToken cancellationToken) + { + var filter = new ReservationFilter { EndDate = DateOnly.FromDateTime(DateTime.Today) }; + + var reservations = await dbContext.GetReservationsAsync(filter, cancellationToken); + + if (reservations.Count == 0) return; + + foreach (var reservation in reservations) + reservation.AddDomainEvent(new EquipmentReturnedEvent(reservation.EquipmentId, 0)); + + await dbContext.SaveChangesAsync(cancellationToken); + } +} diff --git a/StudioManager.API/Base/CoreController.cs b/StudioManager.API/Base/CoreController.cs index 4604712..35550a1 100644 --- a/StudioManager.API/Base/CoreController.cs +++ b/StudioManager.API/Base/CoreController.cs @@ -11,7 +11,7 @@ namespace StudioManager.API.Base; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -[Produces(contentType: "application/json", "application/problem+json")] +[Produces("application/json", "application/problem+json")] [ExcludeFromCodeCoverage] //[Authorize(Policy = "AuthorizedUser")] public abstract class CoreController(ISender sender) : ControllerBase @@ -22,7 +22,7 @@ internal async Task SendAsync(IRequest request) return CreateResult(result); } - + internal async Task SendAsync(IRequest> request) { var result = await sender.Send(request); @@ -40,50 +40,38 @@ private static IResult CreateResult(IRequestResult requestResult) _ => FromUnexpectedErrorResult(requestResult) }; } - + private static IResult FromSucceededResult(IRequestResult requestResult) { - if (!requestResult.Succeeded) - { - throw new InvalidOperationException(EX.SUCCESS_FROM_ERROR); - } - + if (!requestResult.Succeeded) throw new InvalidOperationException(EX.SUCCESS_FROM_ERROR); + return Results.Ok(requestResult.Data); } - + private static IResult FromNotFoundResult(IRequestResult requestResult) { - if (requestResult.Succeeded) - { - throw new InvalidOperationException(EX.ERROR_FROM_SUCCESS); - } + if (requestResult.Succeeded) throw new InvalidOperationException(EX.ERROR_FROM_SUCCESS); return Results.Problem( statusCode: StatusCodes.Status404NotFound, detail: requestResult.Error); } - + private static IResult FromConflictResult(IRequestResult requestResult) { - if (requestResult.Succeeded) - { - throw new InvalidOperationException(EX.ERROR_FROM_SUCCESS); - } + if (requestResult.Succeeded) throw new InvalidOperationException(EX.ERROR_FROM_SUCCESS); return Results.Problem( statusCode: StatusCodes.Status409Conflict, detail: requestResult.Error); } - + private static IResult FromUnexpectedErrorResult(IRequestResult requestResult) { - if (requestResult.Succeeded) - { - throw new InvalidOperationException(EX.ERROR_FROM_SUCCESS); - } + if (requestResult.Succeeded) throw new InvalidOperationException(EX.ERROR_FROM_SUCCESS); return Results.Problem( statusCode: StatusCodes.Status500InternalServerError, detail: requestResult.Error); } -} \ No newline at end of file +} diff --git a/StudioManager.API/Behaviours/RequestLoggingBehavior.cs b/StudioManager.API/Behaviours/RequestLoggingBehavior.cs index a719e5c..7528a7c 100644 --- a/StudioManager.API/Behaviours/RequestLoggingBehavior.cs +++ b/StudioManager.API/Behaviours/RequestLoggingBehavior.cs @@ -12,7 +12,8 @@ public class RequestLoggingBehavior( : IPipelineBehavior where TRequest : IRequest { - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + public async Task Handle(TRequest request, RequestHandlerDelegate next, + CancellationToken cancellationToken) { var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -39,4 +40,4 @@ public async Task Handle(TRequest request, RequestHandlerDelegate( : IPipelineBehavior where TRequest : IRequest { - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + public async Task Handle(TRequest request, RequestHandlerDelegate next, + CancellationToken cancellationToken) { - if (!validators.Any()) - { - return await next(); - } - + if (!validators.Any()) return await next(); + var context = new ValidationContext(request); var validationFailures = await Task.WhenAll( @@ -31,11 +29,8 @@ public async Task Handle(TRequest request, RequestHandlerDelegate TryHandleAsync( CancellationToken cancellationToken) { if (exception is ValidationException validationException) - { return await HandleValidationExceptionAsync(httpContext, validationException, cancellationToken); - } - - logger.LogError(exception, "[ERROR]: Error occurred while handling request {@Request}",exception.TargetSite?.DeclaringType?.FullName); + + logger.LogError(exception, "[ERROR]: Error occurred while handling request {@Request}", + exception.TargetSite?.DeclaringType?.FullName); var problemDetails = new ProblemDetails { @@ -33,10 +33,11 @@ public async ValueTask TryHandleAsync( await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); return true; } - - private static async Task HandleValidationExceptionAsync(HttpContext httpContext, ValidationException exception, CancellationToken cancellationToken) + + private static async Task HandleValidationExceptionAsync(HttpContext httpContext, + ValidationException exception, CancellationToken cancellationToken) { - var problemDetails = new ValidationProblemDetails(exception.Errors.ToDictionary(x => x.PropertyName, x => new[] { x.ErrorMessage })) + var problemDetails = new ValidationProblemDetails(GroupValidationErrors(exception.Errors)) { Status = StatusCodes.Status400BadRequest, Title = "Validation error", @@ -49,4 +50,17 @@ private static async Task HandleValidationExceptionAsync(HttpContext httpC await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); return true; } -} \ No newline at end of file + + private static Dictionary GroupValidationErrors(IEnumerable validationErrors) + { + var final = new Dictionary(); + + foreach (var error in validationErrors) + if (final.TryGetValue(error.PropertyName, out var value)) + final[error.PropertyName] = value.Append(error.ErrorMessage).ToArray(); + else + final[error.PropertyName] = [error.ErrorMessage]; + + return final; + } +} diff --git a/StudioManager.API/Common/NamedSwaggerGenOptions.cs b/StudioManager.API/Common/NamedSwaggerGenOptions.cs index 57f41fe..f477efe 100644 --- a/StudioManager.API/Common/NamedSwaggerGenOptions.cs +++ b/StudioManager.API/Common/NamedSwaggerGenOptions.cs @@ -15,18 +15,16 @@ public void Configure(SwaggerGenOptions options) { // add swagger document for every API version discovered foreach (var description in provider.ApiVersionDescriptions) - { options.SwaggerDoc( description.GroupName, CreateVersionInfo(description)); - } } public void Configure(string? name, SwaggerGenOptions options) { Configure(options); } - + private static OpenApiInfo CreateVersionInfo( ApiVersionDescription description) { @@ -35,10 +33,7 @@ private static OpenApiInfo CreateVersionInfo( Title = "Studio Manager API " + description.GroupName, Version = description.ApiVersion.ToString() }; - if (description.IsDeprecated) - { - info.Description += " This API version has been deprecated."; - } + if (description.IsDeprecated) info.Description += " This API version has been deprecated."; return info; } -} \ No newline at end of file +} diff --git a/StudioManager.API/Common/SwaggerConfiguration.cs b/StudioManager.API/Common/SwaggerConfiguration.cs index 799cb6f..ee69e5d 100644 --- a/StudioManager.API/Common/SwaggerConfiguration.cs +++ b/StudioManager.API/Common/SwaggerConfiguration.cs @@ -8,22 +8,22 @@ public static class SwaggerConfiguration { public static void ConfigureSwagger(this IServiceCollection services) { - services.AddApiVersioning(options => - { - options.ReportApiVersions = true; - options.ApiVersionReader = ApiVersionReader.Combine( - new UrlSegmentApiVersionReader(), - new HeaderApiVersionReader("x-api-version")); - }) - .AddMvc() - .AddApiExplorer(x => - { - x.GroupNameFormat = "'v'VVV"; - x.SubstituteApiVersionInUrl = true; - }); + services.AddApiVersioning(options => + { + options.ReportApiVersions = true; + options.ApiVersionReader = ApiVersionReader.Combine( + new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("x-api-version")); + }) + .AddMvc() + .AddApiExplorer(x => + { + x.GroupNameFormat = "'v'VVV"; + x.SubstituteApiVersionInUrl = true; + }); services.AddSwaggerGen(); services.ConfigureOptions(); } -} \ No newline at end of file +} diff --git a/StudioManager.API/Controllers/V1/EquipmentTypesController.cs b/StudioManager.API/Controllers/V1/EquipmentTypesController.cs index d0a15c1..4a19cbb 100644 --- a/StudioManager.API/Controllers/V1/EquipmentTypesController.cs +++ b/StudioManager.API/Controllers/V1/EquipmentTypesController.cs @@ -25,7 +25,7 @@ public async Task CreateEquipmentTypeAsync([FromBody] EquipmentTypeWrit var command = new CreateEquipmentTypeCommand(dto); return await SendAsync(command); } - + [HttpPut("{id:guid}")] [ProducesResponseType(typeof(CommandResult), StatusCodes.Status200OK)] public async Task UpdateEquipmentTypeAsync( @@ -35,7 +35,7 @@ public async Task UpdateEquipmentTypeAsync( var command = new UpdateEquipmentTypeCommand(id, dto); return await SendAsync(command); } - + [HttpGet] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetEquipmentTypesAsync( @@ -45,7 +45,7 @@ public async Task GetEquipmentTypesAsync( var query = new GetEquipmentTypesQuery { Filter = filter }; return await SendAsync(query); } - + [HttpDelete("{id:guid}")] [ProducesResponseType(typeof(CommandResult), StatusCodes.Status200OK)] public async Task DeleteEquipmentTypeAsync(Guid id) @@ -53,7 +53,7 @@ public async Task DeleteEquipmentTypeAsync(Guid id) var command = new DeleteEquipmentTypeCommand(id); return await SendAsync(command); } - + private static EquipmentTypeFilter CreateFilter(string? ft) { return new EquipmentTypeFilter @@ -61,4 +61,4 @@ private static EquipmentTypeFilter CreateFilter(string? ft) Search = ft }; } -} \ No newline at end of file +} diff --git a/StudioManager.API/Controllers/V1/EquipmentController.cs b/StudioManager.API/Controllers/V1/EquipmentsController.cs similarity index 90% rename from StudioManager.API/Controllers/V1/EquipmentController.cs rename to StudioManager.API/Controllers/V1/EquipmentsController.cs index 87c6bf0..6e8b2a1 100644 --- a/StudioManager.API/Controllers/V1/EquipmentController.cs +++ b/StudioManager.API/Controllers/V1/EquipmentsController.cs @@ -15,9 +15,9 @@ namespace StudioManager.API.Controllers.V1; [ApiVersion("1.0")] -[Route("api/v{v:apiVersion}/Equipments")] +[Route("api/v{v:apiVersion}/[controller]")] [ExcludeFromCodeCoverage] -public class EquipmentController(ISender sender) : CoreController(sender) +public class EquipmentsController(ISender sender) : CoreController(sender) { [HttpPost] [ProducesResponseType(typeof(CommandResult), StatusCodes.Status200OK)] @@ -26,7 +26,7 @@ public async Task CreateEquipment([FromBody] EquipmentWriteDto dto) var command = new CreateEquipmentCommand(dto); return await SendAsync(command); } - + [HttpDelete("{id:guid}")] [ProducesResponseType(typeof(CommandResult), StatusCodes.Status200OK)] public async Task DeleteEquipment(Guid id) @@ -34,7 +34,7 @@ public async Task DeleteEquipment(Guid id) var command = new DeleteEquipmentCommand(id); return await SendAsync(command); } - + [HttpPut("{id:guid}")] [ProducesResponseType(typeof(CommandResult), StatusCodes.Status200OK)] public async Task UpdateEquipment(Guid id, [FromBody] EquipmentWriteDto dto) @@ -42,15 +42,14 @@ public async Task UpdateEquipment(Guid id, [FromBody] EquipmentWriteDto var command = new UpdateEquipmentCommand(id, dto); return await SendAsync(command); } - + [HttpGet] [ProducesResponseType(typeof(QueryResult>), StatusCodes.Status200OK)] public async Task GetEquipments( - [FromQuery] PaginationDto? pagination, + [FromQuery] PaginationDto pagination, [FromQuery] IEnumerable equipmentTypes, [FromQuery] string? ft) { - pagination ??= PaginationDto.Default(); var filter = CreateFilter(ft, equipmentTypes); var command = new GetAllEquipmentsQuery(filter, pagination); return await SendAsync(command); @@ -66,4 +65,4 @@ private static EquipmentFilter CreateFilter( EquipmentTypeIds = equipmentTypes }; } -} \ No newline at end of file +} diff --git a/StudioManager.API/Controllers/V1/ReservationsController.cs b/StudioManager.API/Controllers/V1/ReservationsController.cs new file mode 100644 index 0000000..5f5b9b8 --- /dev/null +++ b/StudioManager.API/Controllers/V1/ReservationsController.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.CodeAnalysis; +using Asp.Versioning; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using StudioManager.API.Base; +using StudioManager.API.Contracts.Pagination; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Application.Reservations.Create; +using StudioManager.Application.Reservations.Delete; +using StudioManager.Application.Reservations.GetAll; +using StudioManager.Application.Reservations.Update; +using StudioManager.Domain.Common.Results; +using StudioManager.Domain.Filters; + +namespace StudioManager.API.Controllers.V1; + +[ApiVersion("1.0")] +[Route("api/v{v:apiVersion}/[controller]")] +[ExcludeFromCodeCoverage] +public sealed class ReservationsController(ISender sender) : CoreController(sender) +{ + [HttpPost] + [ProducesResponseType(typeof(CommandResult), StatusCodes.Status200OK)] + public async Task CreateReservation([FromBody] ReservationWriteDto reservation) + { + return await SendAsync(new CreateReservationCommand(reservation)); + } + + [HttpPut("{id:guid}")] + [ProducesResponseType(typeof(CommandResult), StatusCodes.Status200OK)] + public async Task UpdateReservation(Guid id, [FromBody] ReservationWriteDto reservation) + { + return await SendAsync(new UpdateReservationCommand(id, reservation)); + } + + [HttpDelete("{id:guid}")] + [ProducesResponseType(typeof(CommandResult), StatusCodes.Status200OK)] + public async Task DeleteReservation(Guid id) + { + return await SendAsync(new DeleteReservationCommand(id)); + } + + [HttpGet] + [ProducesResponseType(typeof(QueryResult>), StatusCodes.Status200OK)] + public async Task GetAllReservations( + [FromQuery] string? ft, + [FromQuery] DateOnly? startDate, + [FromQuery] DateOnly? endDate, + [FromQuery] PaginationDto pagination) + { + var filter = new ReservationFilter + { + Search = ft, + StartDate = startDate, + EndDate = endDate + }; + + return await SendAsync(new GetAllReservationsQuery(filter, pagination)); + } +} diff --git a/StudioManager.API/DependencyInjection.cs b/StudioManager.API/DependencyInjection.cs index fdffe6b..8a116a6 100644 --- a/StudioManager.API/DependencyInjection.cs +++ b/StudioManager.API/DependencyInjection.cs @@ -9,12 +9,9 @@ public static class DependencyInjection { public static void RegisterApi(this IServiceCollection services) { - services.AddMediatR(cfg => - { - cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()); - }); - + services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()); }); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestLoggingBehavior<,>)); } -} \ No newline at end of file +} diff --git a/StudioManager.API/Program.cs b/StudioManager.API/Program.cs index 7d8acaa..d0d7df9 100644 --- a/StudioManager.API/Program.cs +++ b/StudioManager.API/Program.cs @@ -2,9 +2,9 @@ using Asp.Versioning.ApiExplorer; using Microsoft.EntityFrameworkCore; using StudioManager.API; -using StudioManager.Infrastructure; using StudioManager.API.Common; using StudioManager.Application; +using StudioManager.Infrastructure; var builder = WebApplication.CreateBuilder(args); @@ -32,6 +32,8 @@ builder.Services.AddExceptionHandler(); builder.Services.AddProblemDetails(); +// builder.Services.AddHostedService(); TODO: Implement redis cache for read lock + var app = builder.Build(); app.UseHttpsRedirection(); @@ -51,10 +53,8 @@ { var provider = app.Services.GetRequiredService(); foreach (var description in provider.ApiVersionDescriptions) - { options.SwaggerEndpoint( - $"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); - } + $"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); }); app.MapControllers(); @@ -69,4 +69,4 @@ await app.RunAsync(); [ExcludeFromCodeCoverage] -public partial class Program; \ No newline at end of file +public partial class Program; diff --git a/StudioManager.API/StudioManager.API.csproj b/StudioManager.API/StudioManager.API.csproj index 40c081a..5d236d5 100644 --- a/StudioManager.API/StudioManager.API.csproj +++ b/StudioManager.API/StudioManager.API.csproj @@ -8,29 +8,25 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + - - + + - - - + + + diff --git a/StudioManager.Application.Tests/EquipmentTypes/CreateEquipmentTypeCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/EquipmentTypes/CreateEquipmentTypeCommandHandlerTests/Handle.cs index 8152550..a3b897f 100644 --- a/StudioManager.Application.Tests/EquipmentTypes/CreateEquipmentTypeCommandHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/EquipmentTypes/CreateEquipmentTypeCommandHandlerTests/Handle.cs @@ -23,7 +23,7 @@ public async Task SetUpAsync() _testDbContextFactory = new TestDbContextFactory(connectionString); _testCandidate = new CreateEquipmentTypeCommandHandler(_testDbContextFactory); } - + [Test] public async Task should_return_conflict_when_the_name_is_duplicated_async() { @@ -35,7 +35,7 @@ public async Task should_return_conflict_when_the_name_is_duplicated_async() } var command = new CreateEquipmentTypeCommand(new EquipmentTypeWriteDto(equipmentType.Name)); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -47,7 +47,7 @@ public async Task should_return_conflict_when_the_name_is_duplicated_async() result.Error.Should().NotBeNullOrWhiteSpace(); result.Error.Should().Be(DB.EQUIPMENT_TYPE_DUPLICATE_NAME); } - + [Test] public async Task should_return_success_async() { @@ -56,11 +56,11 @@ public async Task should_return_success_async() { await ClearTableContentsForAsync(dbContext); } - + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); var command = new CreateEquipmentTypeCommand(new EquipmentTypeWriteDto(equipmentType.Name)); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -72,7 +72,7 @@ public async Task should_return_success_async() result.Succeeded.Should().BeTrue(); result.StatusCode.Should().Be(OkStatusCode); result.Error.Should().BeNullOrWhiteSpace(); - + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) { var databaseCheck = await dbContext.EquipmentTypes.FindAsync(id); @@ -80,4 +80,4 @@ public async Task should_return_success_async() databaseCheck!.Name.Should().Be(equipmentType.Name); } } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/EquipmentTypes/CreateEquipmentTypeCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/EquipmentTypes/CreateEquipmentTypeCommandHandlerTests/Validator.cs index 6f9f066..e7a0a86 100644 --- a/StudioManager.Application.Tests/EquipmentTypes/CreateEquipmentTypeCommandHandlerTests/Validator.cs +++ b/StudioManager.Application.Tests/EquipmentTypes/CreateEquipmentTypeCommandHandlerTests/Validator.cs @@ -21,9 +21,10 @@ public async Task validator_should_return_validation_error_when_name_is_empty() // Assert result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == "EquipmentType.Name" && x.ErrorMessage == "'Name' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "EquipmentType.Name" && x.ErrorMessage == "'Name' must not be empty."); } - + [Test] public async Task validator_should_return_success_when_name_is_not_empty() { @@ -39,4 +40,4 @@ public async Task validator_should_return_success_when_name_is_not_empty() result.IsValid.Should().BeTrue(); result.Errors.Should().BeEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/EquipmentTypes/DeleteEquipmentTypeCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/EquipmentTypes/DeleteEquipmentTypeCommandHandlerTests/Handle.cs index 784de1e..a8b4aa3 100644 --- a/StudioManager.Application.Tests/EquipmentTypes/DeleteEquipmentTypeCommandHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/EquipmentTypes/DeleteEquipmentTypeCommandHandlerTests/Handle.cs @@ -13,8 +13,8 @@ public sealed class Handle : IntegrationTestBase { private static DeleteEquipmentTypeCommandHandler _testCandidate = null!; private static TestDbContextFactory _testDbContextFactory = null!; - - + + [SetUp] public async Task SetUpAsync() { @@ -22,7 +22,7 @@ public async Task SetUpAsync() _testDbContextFactory = new TestDbContextFactory(connectionString); _testCandidate = new DeleteEquipmentTypeCommandHandler(_testDbContextFactory); } - + [Test] public async Task should_return_not_found_when_updating_non_existing_entity_async() { @@ -33,9 +33,9 @@ public async Task should_return_not_found_when_updating_non_existing_entity_asyn } var id = Guid.NewGuid(); - + var command = new DeleteEquipmentTypeCommand(id); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -47,7 +47,7 @@ public async Task should_return_not_found_when_updating_non_existing_entity_asyn result.Error.Should().NotBeNullOrWhiteSpace(); result.Error.Should().Be($"[NOT FOUND] {nameof(EquipmentType)} with id '{id}' does not exist"); } - + [Test] public async Task should_return_success_async() { @@ -59,7 +59,7 @@ public async Task should_return_success_async() } var command = new DeleteEquipmentTypeCommand(equipmentType.Id); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -69,11 +69,11 @@ public async Task should_return_success_async() result.Succeeded.Should().BeTrue(); result.StatusCode.Should().Be(OkStatusCode); result.Error.Should().BeNullOrWhiteSpace(); - + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) { var databaseCheck = await dbContext.EquipmentTypes.FindAsync(equipmentType.Id); databaseCheck.Should().BeNull(); } } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/EquipmentTypes/DeleteEquipmentTypeCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/EquipmentTypes/DeleteEquipmentTypeCommandHandlerTests/Validator.cs index 0262065..4a7afc6 100644 --- a/StudioManager.Application.Tests/EquipmentTypes/DeleteEquipmentTypeCommandHandlerTests/Validator.cs +++ b/StudioManager.Application.Tests/EquipmentTypes/DeleteEquipmentTypeCommandHandlerTests/Validator.cs @@ -20,7 +20,7 @@ public async Task validator_should_return_validation_error_when_name_is_empty() result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(x => x.PropertyName == "Id" && x.ErrorMessage == "'Id' must not be empty."); } - + [Test] public async Task validator_should_return_success_when_name_is_not_empty() { @@ -35,4 +35,4 @@ public async Task validator_should_return_success_when_name_is_not_empty() result.IsValid.Should().BeTrue(); result.Errors.Should().BeEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/EquipmentTypes/GetEquipmentTypesQueryHandlerTests/Handle.cs b/StudioManager.Application.Tests/EquipmentTypes/GetEquipmentTypesQueryHandlerTests/Handle.cs index 1c8905b..3756aa2 100644 --- a/StudioManager.Application.Tests/EquipmentTypes/GetEquipmentTypesQueryHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/EquipmentTypes/GetEquipmentTypesQueryHandlerTests/Handle.cs @@ -31,12 +31,12 @@ public async Task should_return_empty_collection_when_there_is_no_data_async() { await ClearTableContentsForAsync(dbContext); } - + var query = new GetEquipmentTypesQuery { Filter = new EquipmentTypeFilter() }; - + // Act var result = await _testCandidate.Handle(query, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Error.Should().BeNullOrWhiteSpace(); @@ -46,7 +46,7 @@ public async Task should_return_empty_collection_when_there_is_no_data_async() result.Data.Should().BeEmpty(); result.Data.Should().BeOfType>(); } - + [Test] public async Task should_return_mapped_data_async() { @@ -57,12 +57,12 @@ public async Task should_return_mapped_data_async() var equipmentTypes = Enumerable.Range(0, 5).Select(x => EquipmentType.Create(x.ToString())).ToArray(); await AddEntitiesToTable(dbContext, equipmentTypes); } - + var query = new GetEquipmentTypesQuery { Filter = new EquipmentTypeFilter() }; - + // Act var result = await _testCandidate.Handle(query, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Error.Should().BeNullOrWhiteSpace(); @@ -73,7 +73,7 @@ public async Task should_return_mapped_data_async() result.Data!.Count.Should().Be(5); result.Data.Should().BeOfType>(); } - + [Test] public async Task should_return_mapped_and_filtered_data_async() { @@ -84,12 +84,12 @@ public async Task should_return_mapped_and_filtered_data_async() var equipmentTypes = Enumerable.Range(0, 5).Select(x => EquipmentType.Create(x.ToString())).ToArray(); await AddEntitiesToTable(dbContext, equipmentTypes); } - + var query = new GetEquipmentTypesQuery { Filter = new EquipmentTypeFilter { Name = "1" } }; - + // Act var result = await _testCandidate.Handle(query, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Error.Should().BeNullOrWhiteSpace(); @@ -100,4 +100,4 @@ public async Task should_return_mapped_and_filtered_data_async() result.Data!.Count.Should().Be(1); result.Data.Should().BeOfType>(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/EquipmentTypes/UpdateEquipmentTypeCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/EquipmentTypes/UpdateEquipmentTypeCommandHandlerTests/Handle.cs index e18ca56..c529618 100644 --- a/StudioManager.Application.Tests/EquipmentTypes/UpdateEquipmentTypeCommandHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/EquipmentTypes/UpdateEquipmentTypeCommandHandlerTests/Handle.cs @@ -23,7 +23,7 @@ public async Task SetUpAsync() _testDbContextFactory = new TestDbContextFactory(connectionString); _testCandidate = new UpdateEquipmentTypeCommandHandler(_testDbContextFactory); } - + [Test] public async Task should_return_conflict_when_the_name_is_duplicated_async() { @@ -35,7 +35,7 @@ public async Task should_return_conflict_when_the_name_is_duplicated_async() } var command = new UpdateEquipmentTypeCommand(Guid.NewGuid(), new EquipmentTypeWriteDto(equipmentType.Name)); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -47,7 +47,7 @@ public async Task should_return_conflict_when_the_name_is_duplicated_async() result.Error.Should().NotBeNullOrWhiteSpace(); result.Error.Should().Be(DB.EQUIPMENT_TYPE_DUPLICATE_NAME); } - + [Test] public async Task should_return_not_found_when_updating_non_existing_entity_async() { @@ -56,10 +56,10 @@ public async Task should_return_not_found_when_updating_non_existing_entity_asyn { await ClearTableContentsForAsync(dbContext); } - + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); var command = new UpdateEquipmentTypeCommand(equipmentType.Id, new EquipmentTypeWriteDto(equipmentType.Name)); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -69,9 +69,10 @@ public async Task should_return_not_found_when_updating_non_existing_entity_asyn result.Succeeded.Should().BeFalse(); result.StatusCode.Should().Be(NotFoundStatusCode); result.Error.Should().NotBeNullOrWhiteSpace(); - result.Error.Should().Be($"[NOT FOUND] {equipmentType.GetType().Name} with id '{equipmentType.Id}' does not exist"); + result.Error.Should() + .Be($"[NOT FOUND] {equipmentType.GetType().Name} with id '{equipmentType.Id}' does not exist"); } - + [Test] public async Task should_return_success_async() { @@ -81,9 +82,10 @@ public async Task should_return_success_async() { await AddEntitiesToTable(dbContext, equipmentType); } + equipmentType.Update("Updated-Equipment-Type"); var command = new UpdateEquipmentTypeCommand(equipmentType.Id, new EquipmentTypeWriteDto(equipmentType.Name)); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -93,7 +95,7 @@ public async Task should_return_success_async() result.Succeeded.Should().BeTrue(); result.StatusCode.Should().Be(OkStatusCode); result.Error.Should().BeNullOrWhiteSpace(); - + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) { var databaseCheck = await dbContext.EquipmentTypes.FindAsync(equipmentType.Id); @@ -101,4 +103,4 @@ public async Task should_return_success_async() databaseCheck!.Name.Should().Be(equipmentType.Name); } } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/EquipmentTypes/UpdateEquipmentTypeCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/EquipmentTypes/UpdateEquipmentTypeCommandHandlerTests/Validator.cs index 8a4eed4..a0effbd 100644 --- a/StudioManager.Application.Tests/EquipmentTypes/UpdateEquipmentTypeCommandHandlerTests/Validator.cs +++ b/StudioManager.Application.Tests/EquipmentTypes/UpdateEquipmentTypeCommandHandlerTests/Validator.cs @@ -22,9 +22,10 @@ public async Task validator_should_return_validation_errors_when_model_is_invali // Assert result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(x => x.PropertyName == "Id" && x.ErrorMessage == "'Id' must not be empty."); - result.Errors.Should().Contain(x => x.PropertyName == "EquipmentType.Name" && x.ErrorMessage == "'Name' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "EquipmentType.Name" && x.ErrorMessage == "'Name' must not be empty."); } - + [Test] public async Task validator_should_return_validation_error_when_id_is_empty() { @@ -40,7 +41,7 @@ public async Task validator_should_return_validation_error_when_id_is_empty() result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(x => x.PropertyName == "Id" && x.ErrorMessage == "'Id' must not be empty."); } - + [Test] public async Task validator_should_return_validation_error_when_name_is_empty() { @@ -54,9 +55,10 @@ public async Task validator_should_return_validation_error_when_name_is_empty() // Assert result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == "EquipmentType.Name" && x.ErrorMessage == "'Name' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "EquipmentType.Name" && x.ErrorMessage == "'Name' must not be empty."); } - + [Test] public async Task validator_should_return_success_when_name_is_not_empty() { @@ -72,4 +74,4 @@ public async Task validator_should_return_success_when_name_is_not_empty() result.IsValid.Should().BeTrue(); result.Errors.Should().BeEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Handle.cs index 76b24d2..59d8409 100644 --- a/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Handle.cs @@ -29,10 +29,10 @@ public async Task should_return_error_when_equipment_type_does_not_exist_async() // Arrange var dto = new EquipmentWriteDto("Equipment-Test-Name", Guid.NewGuid(), 1); var command = new CreateEquipmentCommand(dto); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Succeeded.Should().BeFalse(); @@ -41,7 +41,7 @@ public async Task should_return_error_when_equipment_type_does_not_exist_async() result.Error.Should().NotBeNullOrWhiteSpace(); result.Error.Should().Be($"[NOT FOUND] {nameof(EquipmentType)} with id '{dto.EquipmentTypeId}' does not exist"); } - + [Test] public async Task should_return_error_when_equipment_has_duplicated_name_and_type_async() { @@ -54,23 +54,23 @@ public async Task should_return_error_when_equipment_has_duplicated_name_and_typ await AddEntitiesToTable(dbContext, equipmentType); await AddEntitiesToTable(dbContext, existing); } - + var dto = new EquipmentWriteDto("Equipment-Test-Name", existing.EquipmentTypeId, 1); var command = new CreateEquipmentCommand(dto); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Succeeded.Should().BeFalse(); result.StatusCode.Should().Be(ConflictStatusCode); result.Data.Should().BeNull(); result.Error.Should().NotBeNullOrWhiteSpace(); - result.Error.Should().Be(string.Format(DB_FORMAT.EQUIPMENT_DUPLICATE_NAME_TYPE, + result.Error.Should().Be(string.Format(DB_FORMAT.EQUIPMENT_DUPLICATE_NAME_TYPE, existing.Name, existing.EquipmentTypeId)); } - + [Test] public async Task should_return_success_async() { @@ -83,13 +83,13 @@ public async Task should_return_success_async() await ClearTableContentsForAsync(dbContext); await AddEntitiesToTable(dbContext, equipmentType); } - + var dto = new EquipmentWriteDto("Equipment-Test-Name", equipmentType.Id, 1); var command = new CreateEquipmentCommand(dto); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Succeeded.Should().BeTrue(); @@ -100,4 +100,4 @@ public async Task should_return_success_async() parseResult.Should().BeTrue(); guid.Should().NotBeEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Validator.cs index ebae975..7b2c874 100644 --- a/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Validator.cs +++ b/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Validator.cs @@ -21,11 +21,16 @@ public async Task validator_should_return_validation_errors_when_model_is_invali // Assert result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.Quantity" && x.ErrorMessage == "'Quantity' must be greater than or equal to '1'."); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.EquipmentTypeId" && x.ErrorMessage == "'Equipment Type Id' must not be empty."); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.Name" && x.ErrorMessage == "'Name' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.Quantity" && + x.ErrorMessage == "'Quantity' must be greater than or equal to '1'."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.EquipmentTypeId" && + x.ErrorMessage == "'Equipment Type Id' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.Name" && x.ErrorMessage == "'Name' must not be empty."); } - + [Test] public async Task validator_should_return_validation_error_when_quantity_is_lower_than_1() { @@ -39,9 +44,11 @@ public async Task validator_should_return_validation_error_when_quantity_is_lowe // Assert result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.Quantity" && x.ErrorMessage == "'Quantity' must be greater than or equal to '1'."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.Quantity" && + x.ErrorMessage == "'Quantity' must be greater than or equal to '1'."); } - + [Test] public async Task validator_should_return_validation_error_when_equipment_type_id_is_empty() { @@ -55,9 +62,11 @@ public async Task validator_should_return_validation_error_when_equipment_type_i // Assert result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.EquipmentTypeId" && x.ErrorMessage == "'Equipment Type Id' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.EquipmentTypeId" && + x.ErrorMessage == "'Equipment Type Id' must not be empty."); } - + [Test] public async Task validator_should_return_validation_error_when_name_is_empty() { @@ -71,9 +80,10 @@ public async Task validator_should_return_validation_error_when_name_is_empty() // Assert result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.Name" && x.ErrorMessage == "'Name' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.Name" && x.ErrorMessage == "'Name' must not be empty."); } - + [Test] public async Task validator_should_return_success_when_model_is_valid() { @@ -89,4 +99,4 @@ public async Task validator_should_return_success_when_model_is_valid() result.IsValid.Should().BeTrue(); result.Errors.Should().BeEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Handle.cs index 76dd9e8..610bb5b 100644 --- a/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Handle.cs @@ -14,8 +14,8 @@ public sealed class Handle : IntegrationTestBase { private static DeleteEquipmentCommandHandler _testCandidate = null!; private static TestDbContextFactory _testDbContextFactory = null!; - - + + [SetUp] public async Task SetUpAsync() { @@ -23,7 +23,7 @@ public async Task SetUpAsync() _testDbContextFactory = new TestDbContextFactory(connectionString); _testCandidate = new DeleteEquipmentCommandHandler(_testDbContextFactory); } - + [Test] public async Task should_return_not_found_when_updating_non_existing_entity_async() { @@ -34,9 +34,9 @@ public async Task should_return_not_found_when_updating_non_existing_entity_asyn } var id = Guid.NewGuid(); - + var command = new DeleteEquipmentCommand(id); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -48,7 +48,7 @@ public async Task should_return_not_found_when_updating_non_existing_entity_asyn result.Error.Should().NotBeNullOrWhiteSpace(); result.Error.Should().Be($"[NOT FOUND] {nameof(Equipment)} with id '{id}' does not exist"); } - + [Test] public async Task should_return_error_when_initial_count_is_invalid_async() { @@ -67,7 +67,7 @@ public async Task should_return_error_when_initial_count_is_invalid_async() } var command = new DeleteEquipmentCommand(equipment.Id); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -81,7 +81,7 @@ public async Task should_return_error_when_initial_count_is_invalid_async() equipment.InitialQuantity, equipment.Quantity)); } - + [Test] public async Task should_return_success_async() { @@ -97,7 +97,7 @@ public async Task should_return_success_async() } var command = new DeleteEquipmentCommand(equipment.Id); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); @@ -107,11 +107,11 @@ public async Task should_return_success_async() result.Succeeded.Should().BeTrue(); result.StatusCode.Should().Be(OkStatusCode); result.Error.Should().BeNullOrWhiteSpace(); - + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) { var databaseCheck = await dbContext.Equipments.FindAsync(equipment.Id); databaseCheck.Should().BeNull(); } } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Validator.cs index 9650507..daa946d 100644 --- a/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Validator.cs +++ b/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Validator.cs @@ -20,7 +20,7 @@ public async Task validator_should_return_validation_error_when_name_is_empty() result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(x => x.PropertyName == "Id" && x.ErrorMessage == "'Id' must not be empty."); } - + [Test] public async Task validator_should_return_success_when_name_is_not_empty() { @@ -35,4 +35,4 @@ public async Task validator_should_return_success_when_name_is_not_empty() result.IsValid.Should().BeTrue(); result.Errors.Should().BeEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Equipments/GetAllEquipmentsQueryHandlerTests/Handle.cs b/StudioManager.Application.Tests/Equipments/GetAllEquipmentsQueryHandlerTests/Handle.cs index 590225d..e38a559 100644 --- a/StudioManager.Application.Tests/Equipments/GetAllEquipmentsQueryHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/Equipments/GetAllEquipmentsQueryHandlerTests/Handle.cs @@ -32,11 +32,11 @@ public async Task SetUpAsync() public async Task should_return_empty_data_with_pagination_async() { // Arrange - var query = new GetAllEquipmentsQuery(new EquipmentFilter(), PaginationDto.Default()); - + var query = new GetAllEquipmentsQuery(new EquipmentFilter(), new PaginationDto()); + // Act var result = await _testCandidate.Handle(query, CancellationToken.None); - + // Assert result.Should().NotBeNull(); result.Error.Should().BeNullOrWhiteSpace(); @@ -47,7 +47,7 @@ public async Task should_return_empty_data_with_pagination_async() result.Data!.Data.Should().BeEmpty(); result.Data.Data.Should().BeOfType>(); } - + [Test] public async Task should_return_mapped_data_with_pagination_async() { @@ -64,11 +64,12 @@ public async Task should_return_mapped_data_with_pagination_async() .ToArray(); await AddEntitiesToTable(dbContext, equipments); } - var query = new GetAllEquipmentsQuery(new EquipmentFilter(), PaginationDto.Default()); - + + var query = new GetAllEquipmentsQuery(new EquipmentFilter(), new PaginationDto()); + // Act var result = await _testCandidate.Handle(query, CancellationToken.None); - + // Assert result.Should().NotBeNull(); result.Error.Should().BeNullOrWhiteSpace(); @@ -77,11 +78,11 @@ public async Task should_return_mapped_data_with_pagination_async() result.Data.Should().NotBeNull(); result.Data.Should().BeOfType>(); result.Data!.Pagination.Should().NotBeNull(); - result.Data.Pagination.Limit.Should().Be(25); - result.Data.Pagination.Page.Should().Be(1); + result.Data.Pagination.Limit.Should().Be(PaginationDto.DefaultLimit); + result.Data.Pagination.Page.Should().Be(PaginationDto.DefaultPage); result.Data.Pagination.Total.Should().Be(5); result.Data.Data.Should().NotBeEmpty(); result.Data.Data.Should().HaveCount(5); result.Data.Data.Should().BeOfType>(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Handle.cs index e1624e6..bd7886d 100644 --- a/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Handle.cs @@ -29,10 +29,10 @@ public async Task should_return_not_found_when_equipment_type_does_not_exist_asy // Arrange var dto = new EquipmentWriteDto("Test-Equipment", Guid.NewGuid(), 1); var command = new UpdateEquipmentCommand(Guid.NewGuid(), dto); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Succeeded.Should().BeFalse(); @@ -41,7 +41,7 @@ public async Task should_return_not_found_when_equipment_type_does_not_exist_asy result.Error.Should().NotBeNullOrWhiteSpace(); result.Error.Should().Be($"[NOT FOUND] {nameof(EquipmentType)} with id '{dto.EquipmentTypeId}' does not exist"); } - + [Test] public async Task should_return_conflict_when_equipment_name_and_type_is_not_unique_async() { @@ -54,23 +54,23 @@ public async Task should_return_conflict_when_equipment_name_and_type_is_not_uni await AddEntitiesToTable(dbContext, equipmentType); await AddEntitiesToTable(dbContext, equipment); } - + var dto = new EquipmentWriteDto(equipment.Name, equipmentType.Id, 100); var command = new UpdateEquipmentCommand(Guid.NewGuid(), dto); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Succeeded.Should().BeFalse(); result.StatusCode.Should().Be(ConflictStatusCode); result.Data.Should().BeNull(); result.Error.Should().NotBeNullOrWhiteSpace(); - result.Error.Should().Be(string.Format(DB_FORMAT.EQUIPMENT_DUPLICATE_NAME_TYPE, + result.Error.Should().Be(string.Format(DB_FORMAT.EQUIPMENT_DUPLICATE_NAME_TYPE, equipment.Name, equipmentType.Id)); } - + [Test] public async Task should_return_not_found_when_equipment_does_not_exist_async() { @@ -85,13 +85,13 @@ public async Task should_return_not_found_when_equipment_does_not_exist_async() await AddEntitiesToTable(dbContext, equipmentType); await AddEntitiesToTable(dbContext, equipment); } - + var dto = new EquipmentWriteDto("Test-Equipment-Updated", equipmentType.Id, 100); var command = new UpdateEquipmentCommand(Guid.NewGuid(), dto); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Succeeded.Should().BeFalse(); @@ -100,7 +100,7 @@ public async Task should_return_not_found_when_equipment_does_not_exist_async() result.Error.Should().NotBeNullOrWhiteSpace(); result.Error.Should().Be($"[NOT FOUND] {nameof(Equipment)} with id '{command.Id}' does not exist"); } - + [Test] public async Task should_return_success_async() { @@ -113,13 +113,13 @@ public async Task should_return_success_async() await AddEntitiesToTable(dbContext, equipmentType); await AddEntitiesToTable(dbContext, equipment); } - + var dto = new EquipmentWriteDto("Test-Equipment-Updated", equipmentType.Id, 100); var command = new UpdateEquipmentCommand(equipment.Id, dto); - + // Act var result = await _testCandidate.Handle(command, Cts.Token); - + // Assert result.Should().NotBeNull(); result.Succeeded.Should().BeTrue(); @@ -127,4 +127,4 @@ public async Task should_return_success_async() result.Data.Should().BeNull(); result.Error.Should().BeNullOrWhiteSpace(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Validator.cs index 1d930dd..9f200ea 100644 --- a/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Validator.cs +++ b/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Validator.cs @@ -9,9 +9,9 @@ namespace StudioManager.Application.Tests.Equipments.UpdateEquipmentCommandHandl public sealed class Validator { private const string Name = "Validation-Name"; - private static readonly Guid EquipmentTypeId = Guid.NewGuid(); private const int Quantity = 1; - + private static readonly Guid EquipmentTypeId = Guid.NewGuid(); + [Test] public async Task should_return_error_when_id_is_empty_async() { @@ -19,9 +19,10 @@ public async Task should_return_error_when_id_is_empty_async() var command = new UpdateEquipmentCommand(Guid.Empty, new EquipmentWriteDto(Name, EquipmentTypeId, Quantity)); var result = await validator.ValidateAsync(command); result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == nameof(command.Id) && x.ErrorMessage == "'Id' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(command.Id) && x.ErrorMessage == "'Id' must not be empty."); } - + [Test] public async Task should_return_error_when_equipment_is_null_async() { @@ -29,9 +30,10 @@ public async Task should_return_error_when_equipment_is_null_async() var command = new UpdateEquipmentCommand(EquipmentTypeId, null!); var result = await validator.ValidateAsync(command); result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == nameof(command.Equipment) && x.ErrorMessage == "'Equipment' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(command.Equipment) && x.ErrorMessage == "'Equipment' must not be empty."); } - + [Test] public async Task should_return_error_when_equipment_model_is_invalid_async() { @@ -39,8 +41,13 @@ public async Task should_return_error_when_equipment_model_is_invalid_async() var command = new UpdateEquipmentCommand(EquipmentTypeId, new EquipmentWriteDto(string.Empty, Guid.Empty, 0)); var result = await validator.ValidateAsync(command); result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.Name" && x.ErrorMessage == "'Name' must not be empty."); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.EquipmentTypeId" && x.ErrorMessage == "'Equipment Type Id' must not be empty."); - result.Errors.Should().Contain(x => x.PropertyName == "Equipment.Quantity" && x.ErrorMessage == "'Quantity' must be greater than or equal to '1'."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.Name" && x.ErrorMessage == "'Name' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.EquipmentTypeId" && + x.ErrorMessage == "'Equipment Type Id' must not be empty."); + result.Errors.Should().Contain(x => + x.PropertyName == "Equipment.Quantity" && + x.ErrorMessage == "'Quantity' must be greater than or equal to '1'."); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReservationChangedEventHandlerTests.cs b/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReservationChangedEventHandlerTests.cs new file mode 100644 index 0000000..7a49b2e --- /dev/null +++ b/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReservationChangedEventHandlerTests.cs @@ -0,0 +1,67 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using NUnit.Framework; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Application.EventHandlers.Equipments; +using StudioManager.Application.Tests.Reservations.Common; +using StudioManager.Infrastructure; +using StudioManager.Notifications.Equipment; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.EventHandlers.Equipments; + +public sealed class EquipmentReservationChangedEventHandlerTests : IntegrationTestBase +{ + private static TestDbContextFactory _dbContextFactory = null!; + private static FakeLogger _logger = null!; + private static EquipmentReservationChangedEventHandler _testCandidate = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _dbContextFactory = new TestDbContextFactory(connectionString); + _logger = new FakeLogger(); + _testCandidate = new EquipmentReservationChangedEventHandler(_dbContextFactory, _logger); + } + + [Test] + public async Task should_do_nothing_when_equipment_not_found() + { + // Arrange + var notification = new EquipmentReservationChangedEvent(Guid.NewGuid(), 1, 1); + + // Act + await _testCandidate.Handle(notification, CancellationToken.None); + + // Assert + _logger.Collector.GetSnapshot().Should().Contain(x => x.Level == LogLevel.Warning); + } + + [Test] + public async Task should_change_equipment_quantity_when_equipment_not_found() + { + // Arrange + Guid equipmentId; + await using (var dbContext = await _dbContextFactory.CreateDbContextAsync(CancellationToken.None)) + { + var reservation = await ReservationTestHelper.AddReservationAsync(dbContext); + equipmentId = reservation.EquipmentId; + } + + var notification = new EquipmentReservationChangedEvent(equipmentId, 1, 0); + + // Act + await _testCandidate.Handle(notification, CancellationToken.None); + + // Assert + _logger.Collector.GetSnapshot().Should().NotContain(x => x.Level == LogLevel.Warning); + await using (var dbContext = await _dbContextFactory.CreateDbContextAsync(CancellationToken.None)) + { + var equipment = await dbContext.GetEquipmentAsync(equipmentId, CancellationToken.None); + equipment!.Quantity.Should().Be(99); + } + } +} diff --git a/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReservedEventHandlerTests.cs b/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReservedEventHandlerTests.cs new file mode 100644 index 0000000..fd43435 --- /dev/null +++ b/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReservedEventHandlerTests.cs @@ -0,0 +1,67 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using NUnit.Framework; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Application.EventHandlers.Equipments; +using StudioManager.Application.Tests.Reservations.Common; +using StudioManager.Infrastructure; +using StudioManager.Notifications.Equipment; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.EventHandlers.Equipments; + +public sealed class EquipmentReservedEventHandlerTests : IntegrationTestBase +{ + private static TestDbContextFactory _dbContextFactory = null!; + private static FakeLogger _logger = null!; + private static EquipmentReservedEventHandler _testCandidate = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _dbContextFactory = new TestDbContextFactory(connectionString); + _logger = new FakeLogger(); + _testCandidate = new EquipmentReservedEventHandler(_dbContextFactory, _logger); + } + + [Test] + public async Task should_do_nothing_when_equipment_not_found() + { + // Arrange + var notification = new EquipmentReservedEvent(Guid.NewGuid(), 1); + + // Act + await _testCandidate.Handle(notification, CancellationToken.None); + + // Assert + _logger.Collector.GetSnapshot().Should().Contain(x => x.Level == LogLevel.Warning); + } + + [Test] + public async Task should_change_equipment_quantity_when_equipment_not_found() + { + // Arrange + Guid equipmentId; + await using (var dbContext = await _dbContextFactory.CreateDbContextAsync(CancellationToken.None)) + { + var reservation = await ReservationTestHelper.AddReservationAsync(dbContext); + equipmentId = reservation.EquipmentId; + } + + var notification = new EquipmentReservedEvent(equipmentId, 1); + + // Act + await _testCandidate.Handle(notification, CancellationToken.None); + + // Assert + _logger.Collector.GetSnapshot().Should().NotContain(x => x.Level == LogLevel.Warning); + await using (var dbContext = await _dbContextFactory.CreateDbContextAsync(CancellationToken.None)) + { + var equipment = await dbContext.GetEquipmentAsync(equipmentId, CancellationToken.None); + equipment!.Quantity.Should().Be(99); + } + } +} diff --git a/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReturnedEventHandlerTests.cs b/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReturnedEventHandlerTests.cs new file mode 100644 index 0000000..9bc3379 --- /dev/null +++ b/StudioManager.Application.Tests/EventHandlers/Equipments/EquipmentReturnedEventHandlerTests.cs @@ -0,0 +1,72 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using NUnit.Framework; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Application.EventHandlers.Equipments; +using StudioManager.Application.Tests.Reservations.Common; +using StudioManager.Infrastructure; +using StudioManager.Notifications.Equipment; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.EventHandlers.Equipments; + +public sealed class EquipmentReturnedEventHandlerTests : IntegrationTestBase +{ + private static TestDbContextFactory _dbContextFactory = null!; + private static FakeLogger _logger = null!; + private static EquipmentReturnedEventHandler _testCandidate = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _dbContextFactory = new TestDbContextFactory(connectionString); + _logger = new FakeLogger(); + _testCandidate = new EquipmentReturnedEventHandler(_dbContextFactory, _logger); + } + + [Test] + public async Task should_do_nothing_when_equipment_not_found() + { + // Arrange + var notification = new EquipmentReturnedEvent(Guid.NewGuid(), 1); + + // Act + await _testCandidate.Handle(notification, CancellationToken.None); + + // Assert + _logger.Collector.GetSnapshot().Should().Contain(x => x.Level == LogLevel.Warning); + } + + [Test] + public async Task should_change_equipment_quantity_when_equipment_not_found() + { + // Arrange + Guid equipmentId; + await using (var dbContext = await _dbContextFactory.CreateDbContextAsync(CancellationToken.None)) + { + var reservation = await ReservationTestHelper.AddReservationAsync(dbContext); + equipmentId = reservation.EquipmentId; + + await dbContext.Equipments.Where(x => x.Id == equipmentId) + .ExecuteUpdateAsync(x => x.SetProperty(y => y.Quantity, 99)); + await dbContext.SaveChangesAsync(); + } + + var notification = new EquipmentReturnedEvent(equipmentId, 1); + + // Act + await _testCandidate.Handle(notification, CancellationToken.None); + + // Assert + _logger.Collector.GetSnapshot().Should().NotContain(x => x.Level == LogLevel.Warning); + await using (var dbContext = await _dbContextFactory.CreateDbContextAsync(CancellationToken.None)) + { + var equipment = await dbContext.GetEquipmentAsync(equipmentId, CancellationToken.None); + equipment!.Quantity.Should().Be(100); + } + } +} diff --git a/StudioManager.Application.Tests/Mapps/EquipmentTypes/EquipmentTypeProjectionTests.cs b/StudioManager.Application.Tests/Mapps/EquipmentTypes/EquipmentTypeProjectionTests.cs index b396705..5ed5693 100644 --- a/StudioManager.Application.Tests/Mapps/EquipmentTypes/EquipmentTypeProjectionTests.cs +++ b/StudioManager.Application.Tests/Mapps/EquipmentTypes/EquipmentTypeProjectionTests.cs @@ -16,10 +16,10 @@ public void should_map_equipment_type_to_equipment_type_projection() // Arrange var equipmentType = EquipmentType.Create("Test Equipment Type"); var mapper = MappingTestHelper.Mapper; - + // Act var result = mapper.Map(equipmentType); - + // Assert result.Should().NotBeNull(); result.Id.Should().NotBeEmpty(); @@ -27,4 +27,4 @@ public void should_map_equipment_type_to_equipment_type_projection() result.Name.Should().NotBeNullOrWhiteSpace(); result.Name.Should().Be(equipmentType.Name); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Mapps/Equipments/EquipmentProjectionTests.cs b/StudioManager.Application.Tests/Mapps/Equipments/EquipmentProjectionTests.cs index 58e8675..d7a6b05 100644 --- a/StudioManager.Application.Tests/Mapps/Equipments/EquipmentProjectionTests.cs +++ b/StudioManager.Application.Tests/Mapps/Equipments/EquipmentProjectionTests.cs @@ -19,10 +19,10 @@ public void should_map_equipment_to_equipment_projection() var equipment = Equipment.Create("Test Equipment", equipmentType.Id, 1); equipment.EquipmentType = equipmentType; var mapper = MappingTestHelper.Mapper; - + // Act var result = mapper.Map(equipment); - + // Assert result.Should().NotBeNull(); result.Id.Should().NotBeEmpty(); @@ -37,4 +37,4 @@ public void should_map_equipment_to_equipment_projection() result.EquipmentType.Name.Should().NotBeNullOrWhiteSpace(); result.EquipmentType.Name.Should().Be(equipmentType.Name); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Mapps/MapperProfilesTests.cs b/StudioManager.Application.Tests/Mapps/MapperProfilesTests.cs index 197330b..352845d 100644 --- a/StudioManager.Application.Tests/Mapps/MapperProfilesTests.cs +++ b/StudioManager.Application.Tests/Mapps/MapperProfilesTests.cs @@ -13,4 +13,4 @@ public void mapper_configuration_is_valid() var configuration = MappingTestHelper.MapperConfiguration; configuration.AssertConfigurationIsValid(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application.Tests/Mapps/Reservations/ReservationProjectionTests.cs b/StudioManager.Application.Tests/Mapps/Reservations/ReservationProjectionTests.cs new file mode 100644 index 0000000..aed0468 --- /dev/null +++ b/StudioManager.Application.Tests/Mapps/Reservations/ReservationProjectionTests.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Domain.Entities; +using StudioManager.Tests.Common.AutoMapperExtensions; + +namespace StudioManager.Application.Tests.Mapps.Reservations; + +[ExcludeFromCodeCoverage] +public sealed class ReservationProjectionTests +{ + [Test] + public void should_map_reservation_to_reservation_read_dto() + { + // Arrange + var startDate = DateOnly.FromDateTime(DateTime.UtcNow); + var equipment = Equipment.Create("Test Equipment", Guid.Empty, 10); + var reservation = Reservation.Create(startDate, startDate, 10, equipment.Id); + reservation.GetType().GetProperty(nameof(Reservation.Equipment))!.SetValue(reservation, equipment); + + var mapper = MappingTestHelper.Mapper; + + // Act + var result = mapper.Map(reservation); + + // Assert + result.Should().NotBeNull(); + result.Id.Should().NotBeEmpty(); + result.Quantity.Should().Be(reservation.Quantity); + result.StartDate.Should().Be(reservation.StartDate); + result.EndDate.Should().Be(reservation.EndDate); + result.Equipment.Should().NotBeNull(); + result.Equipment.Id.Should().Be(equipment.Id); + result.Equipment.Name.Should().Be(equipment.Name); + } +} diff --git a/StudioManager.Application.Tests/Reservations/Common/ReservationTestHelper.cs b/StudioManager.Application.Tests/Reservations/Common/ReservationTestHelper.cs new file mode 100644 index 0000000..b64b8b7 --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/Common/ReservationTestHelper.cs @@ -0,0 +1,69 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using StudioManager.Domain.Entities; +using StudioManager.Infrastructure.Common; + +namespace StudioManager.Application.Tests.Reservations.Common; + +[ExcludeFromCodeCoverage] +internal static class ReservationTestHelper +{ + internal static async Task AddEquipmentAsync(DbContextBase dbContext) + { + await dbContext.EquipmentTypes.ExecuteDeleteAsync(); + await dbContext.Equipments.ExecuteDeleteAsync(); + var equipmentType = EquipmentType.Create("Test Equipment Type"); + var equipment = Equipment.Create("Test Equipment", equipmentType.Id, 100); + + await dbContext.EquipmentTypes.AddAsync(equipmentType); + await dbContext.Equipments.AddAsync(equipment); + await dbContext.SaveChangesAsync(); + return equipment; + } + + internal static async Task AddReservationForDetailsAsync( + DbContextBase dbContext, + DateOnly startDate, + DateOnly endDate, + int? quantity = null) + { + var equipment = await AddEquipmentAsync(dbContext); + + var reservation = Reservation.Create( + startDate, + endDate, + quantity ?? equipment.InitialQuantity, + equipment.Id); + + await dbContext.Reservations.AddAsync(reservation); + await dbContext.SaveChangesAsync(); + return reservation; + } + + internal static async Task AddReservationForEquipmentAsync(DbContextBase dbContext, + Guid equipmentId) + { + var reservation = Reservation.Create( + DateOnly.FromDateTime(DateTime.UtcNow), + DateOnly.FromDateTime(DateTime.UtcNow), + 1, + equipmentId); + + await dbContext.Reservations.AddAsync(reservation); + await dbContext.SaveChangesAsync(); + } + + internal static async Task AddReservationAsync(DbContextBase dbContext) + { + var equipment = await AddEquipmentAsync(dbContext); + var reservation = Reservation.Create( + DateOnly.FromDateTime(DateTime.UtcNow), + DateOnly.FromDateTime(DateTime.UtcNow), + 1, + equipment.Id); + + await dbContext.Reservations.AddAsync(reservation); + await dbContext.SaveChangesAsync(); + return reservation; + } +} diff --git a/StudioManager.Application.Tests/Reservations/CreateReservationCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Reservations/CreateReservationCommandHandlerTests/Handle.cs new file mode 100644 index 0000000..6596cb0 --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/CreateReservationCommandHandlerTests/Handle.cs @@ -0,0 +1,113 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Application.Reservations.Create; +using StudioManager.Application.Tests.Reservations.Common; +using StudioManager.Domain.Entities; +using StudioManager.Domain.ErrorMessages; +using StudioManager.Infrastructure; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.Reservations.CreateReservationCommandHandlerTests; + +public sealed class Handle : IntegrationTestBase +{ + private static readonly DateOnly ValidDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)); + private static TestDbContextFactory _dbContextFactory = null!; + private static CreateReservationCommandHandler _testCandidate = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _dbContextFactory = new TestDbContextFactory(connectionString); + _testCandidate = new CreateReservationCommandHandler(_dbContextFactory); + } + + [Test] + public async Task should_return_conflict_when_equipment_not_found() + { + // Arrange + var command = new CreateReservationCommand(new ReservationWriteDto(ValidDate, ValidDate, 1, Guid.NewGuid())); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(DB.RESERVATION_EQUIPMENT_NOT_FOUND); + } + + [Test] + public async Task should_return_conflict_when_equipment_quantity_insufficient() + { + // Arrange + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var equipment = await ReservationTestHelper.AddEquipmentAsync(dbContext); + + var command = + new CreateReservationCommand(new ReservationWriteDto(ValidDate, ValidDate, equipment.InitialQuantity + 1, + equipment.Id)); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(DB.RESERVATION_EQUIPMENT_QUANTITY_INSUFFICIENT); + } + + [Test] + public async Task should_return_error_when_other_reservations_reserved_all_equipment_async() + { + // Arrange + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var reservation = + await ReservationTestHelper.AddReservationForDetailsAsync(dbContext, ValidDate, ValidDate.AddDays(10)); + + var command = new CreateReservationCommand(new ReservationWriteDto(ValidDate.AddDays(1), ValidDate.AddDays(2), + reservation.Quantity, reservation.EquipmentId)); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(DB.RESERVATION_EQUIPMENT_USED_BY_OTHERS_IN_PERIOD); + } + + [Test] + public async Task should_return_success_when_reservation_is_valid() + { + // Arrange + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + await ClearTableContentsForAsync(dbContext); + var equipment = await ReservationTestHelper.AddEquipmentAsync(dbContext); + + var command = + new CreateReservationCommand(new ReservationWriteDto(ValidDate, ValidDate.AddDays(1), 1, equipment.Id)); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeTrue(); + result.StatusCode.Should().Be(OkStatusCode); + result.Data.Should().NotBeNull(); + result.Error.Should().BeNullOrWhiteSpace(); + } +} diff --git a/StudioManager.Application.Tests/Reservations/CreateReservationCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Reservations/CreateReservationCommandHandlerTests/Validator.cs new file mode 100644 index 0000000..b5fe0e8 --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/CreateReservationCommandHandlerTests/Validator.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using NUnit.Framework; +using StudioManager.Application.Reservations.Create; +using StudioManager.Application.Reservations.Validators; + +namespace StudioManager.Application.Tests.Reservations.CreateReservationCommandHandlerTests; + +[ExcludeFromCodeCoverage] +public sealed class Validator +{ + [Test] + public async Task should_return_error_when_reservation_is_null_async() + { + // Arrange + var command = new CreateReservationCommand(null!); + var validator = new CreateReservationCommandValidator(new ReservationWriteDtoValidator()); + + // Act + var result = await validator.ValidateAsync(command); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().HaveCount(1); + result.Errors.Should().Contain(x => + x.PropertyName == "Reservation" && x.ErrorMessage == "'Reservation' must not be empty."); + } +} diff --git a/StudioManager.Application.Tests/Reservations/DeleteReservationCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Reservations/DeleteReservationCommandHandlerTests/Handle.cs new file mode 100644 index 0000000..5888e60 --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/DeleteReservationCommandHandlerTests/Handle.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using StudioManager.Application.Reservations.Delete; +using StudioManager.Application.Tests.Reservations.Common; +using StudioManager.Domain.Entities; +using StudioManager.Infrastructure; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.Reservations.DeleteReservationCommandHandlerTests; + +public sealed class Handle : IntegrationTestBase +{ + private static readonly DateOnly ValidDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)); + private static TestDbContextFactory _dbContextFactory = null!; + private static DeleteReservationCommandHandler _testCandidate = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _dbContextFactory = new TestDbContextFactory(connectionString); + _testCandidate = new DeleteReservationCommandHandler(_dbContextFactory); + } + + [Test] + public async Task should_return_not_found_when_removing_non_existing_reservation_async() + { + // Arrange + var command = new DeleteReservationCommand(Guid.NewGuid()); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(NotFoundStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be($"[NOT FOUND] {nameof(Reservation)} with id '{command.Id}' does not exist"); + } + + [Test] + public async Task should_return_success_async() + { + // Arrange + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var reservation = + await ReservationTestHelper.AddReservationForDetailsAsync(dbContext, ValidDate, ValidDate.AddDays(1), 1); + + var command = new DeleteReservationCommand(reservation.Id); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeTrue(); + result.StatusCode.Should().Be(OkStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().BeNullOrWhiteSpace(); + + var count = await dbContext.Reservations.CountAsync(); + count.Should().Be(0); + } +} diff --git a/StudioManager.Application.Tests/Reservations/DeleteReservationCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Reservations/DeleteReservationCommandHandlerTests/Validator.cs new file mode 100644 index 0000000..76090f9 --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/DeleteReservationCommandHandlerTests/Validator.cs @@ -0,0 +1,27 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.Application.Reservations.Delete; + +namespace StudioManager.Application.Tests.Reservations.DeleteReservationCommandHandlerTests; + +public sealed class Validator +{ + [Test] + public async Task should_return_error_when_id_is_empty() + { + // Arrange + var command = new DeleteReservationCommand(Guid.Empty); + var validator = new DeleteReservationCommandValidator(); + + // Act + var result = await validator.ValidateAsync(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().HaveCount(1); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(DeleteReservationCommand.Id) && x.ErrorMessage == "'Id' must not be empty."); + } +} diff --git a/StudioManager.Application.Tests/Reservations/GetAllReservationsQueryHandlerTests/Handle.cs b/StudioManager.Application.Tests/Reservations/GetAllReservationsQueryHandlerTests/Handle.cs new file mode 100644 index 0000000..30a65c1 --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/GetAllReservationsQueryHandlerTests/Handle.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Pagination; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Application.Reservations.GetAll; +using StudioManager.Application.Tests.Reservations.Common; +using StudioManager.Domain.Filters; +using StudioManager.Infrastructure; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.AutoMapperExtensions; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.Reservations.GetAllReservationsQueryHandlerTests; + +public sealed class Handle : IntegrationTestBase +{ + private static TestDbContextFactory _dbContextFactory = null!; + private static GetAllReservationsQueryHandler _testCandidate = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _dbContextFactory = new TestDbContextFactory(connectionString); + _testCandidate = new GetAllReservationsQueryHandler(_dbContextFactory, MappingTestHelper.Mapper); + + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + + var equipment = await ReservationTestHelper.AddEquipmentAsync(dbContext); + await ReservationTestHelper.AddReservationForEquipmentAsync(dbContext, equipment.Id); + await ReservationTestHelper.AddReservationForEquipmentAsync(dbContext, equipment.Id); + await ReservationTestHelper.AddReservationForEquipmentAsync(dbContext, equipment.Id); + } + + [Test] + public async Task should_return_data_async() + { + // Arrange + var query = new GetAllReservationsQuery(new ReservationFilter(), new PaginationDto()); + + // Act + var result = await _testCandidate.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeTrue(); + result.Error.Should().BeNullOrWhiteSpace(); + result.Data.Should().NotBeNull(); + result.Data!.Data.Should().NotBeEmpty(); + result.Data.Data.Should().HaveCount(3); + result.Data.Data.Should().BeOfType>(); + } +} diff --git a/StudioManager.Application.Tests/Reservations/UpdateReservationCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Reservations/UpdateReservationCommandHandlerTests/Handle.cs new file mode 100644 index 0000000..d6250f4 --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/UpdateReservationCommandHandlerTests/Handle.cs @@ -0,0 +1,139 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Application.Reservations.Update; +using StudioManager.Application.Tests.Reservations.Common; +using StudioManager.Domain.Entities; +using StudioManager.Domain.ErrorMessages; +using StudioManager.Infrastructure; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.Reservations.UpdateReservationCommandHandlerTests; + +public sealed class Handle : IntegrationTestBase +{ + private static readonly DateOnly ValidDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)); + private static TestDbContextFactory _dbContextFactory = null!; + private static UpdateReservationCommandHandler _testCandidate = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _dbContextFactory = new TestDbContextFactory(connectionString); + _testCandidate = new UpdateReservationCommandHandler(_dbContextFactory); + } + + [Test] + public async Task should_return_conflict_when_equipment_not_found() + { + // Arrange + var command = new UpdateReservationCommand(Guid.NewGuid(), + new ReservationWriteDto(ValidDate, ValidDate, 1, Guid.NewGuid())); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(DB.RESERVATION_EQUIPMENT_NOT_FOUND); + } + + [Test] + public async Task should_return_conflict_when_equipment_quantity_insufficient() + { + // Arrange + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var reservation = + await ReservationTestHelper.AddReservationForDetailsAsync(dbContext, ValidDate, ValidDate.AddDays(1)); + + var command = new UpdateReservationCommand(reservation.Id, + new ReservationWriteDto(ValidDate, ValidDate, 1000, reservation.EquipmentId)); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(DB.RESERVATION_EQUIPMENT_QUANTITY_INSUFFICIENT); + } + + [Test] + public async Task should_return_error_when_other_reservations_reserved_all_equipment_async() + { + // Arrange + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var reservation = + await ReservationTestHelper.AddReservationForDetailsAsync(dbContext, ValidDate, ValidDate.AddDays(10)); + + var command = new UpdateReservationCommand(Guid.NewGuid(), + new ReservationWriteDto(ValidDate.AddDays(1), ValidDate.AddDays(2), reservation.Quantity, + reservation.EquipmentId)); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(DB.RESERVATION_EQUIPMENT_USED_BY_OTHERS_IN_PERIOD); + } + + [Test] + public async Task should_return_error_when_reservation_not_found_valid() + { + // Arrange + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + await ClearTableContentsForAsync(dbContext); + var equipment = await ReservationTestHelper.AddEquipmentAsync(dbContext); + + var command = new UpdateReservationCommand(Guid.NewGuid(), + new ReservationWriteDto(ValidDate, ValidDate.AddDays(1), 1, equipment.Id)); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(NotFoundStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be($"[NOT FOUND] {nameof(Reservation)} with id '{command.Id}' does not exist"); + } + + [Test] + public async Task should_return_success_when_reservation_updated() + { + // Arrange + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + await ClearTableContentsForAsync(dbContext); + var reservation = + await ReservationTestHelper.AddReservationForDetailsAsync(dbContext, ValidDate, ValidDate.AddDays(1), 1); + + var command = new UpdateReservationCommand(reservation.Id, + new ReservationWriteDto(ValidDate, ValidDate.AddDays(1), 1, reservation.EquipmentId)); + + // Act + var result = await _testCandidate.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeTrue(); + result.StatusCode.Should().Be(OkStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().BeNull(); + } +} diff --git a/StudioManager.Application.Tests/Reservations/UpdateReservationCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Reservations/UpdateReservationCommandHandlerTests/Validator.cs new file mode 100644 index 0000000..5f85b48 --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/UpdateReservationCommandHandlerTests/Validator.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Application.Reservations.Update; +using StudioManager.Application.Reservations.Validators; + +namespace StudioManager.Application.Tests.Reservations.UpdateReservationCommandHandlerTests; + +public sealed class Validator +{ + [Test] + public async Task should_return_error_when_command_id_is_empty_invalid_async() + { + // Arrange + var command = new UpdateReservationCommand(Guid.Empty, new ReservationWriteDto( + DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)), + DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)), + 1, + Guid.NewGuid())); + + var validator = new UpdateReservationCommandValidator(new ReservationWriteDtoValidator()); + + // Act + var result = await validator.ValidateAsync(command); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(command.Id) && x.ErrorMessage == "'Id' must not be empty."); + } + + [Test] + public async Task should_return_error_when_command_reservation_is_invalid_async() + { + // Arrange + var command = new UpdateReservationCommand(Guid.NewGuid(), null!); + + var validator = new UpdateReservationCommandValidator(new ReservationWriteDtoValidator()); + + // Act + var result = await validator.ValidateAsync(command); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(command.Reservation) && x.ErrorMessage == "'Reservation' must not be empty."); + } +} diff --git a/StudioManager.Application.Tests/Reservations/Validators/ReservationWriteDtoValidatorTests.cs b/StudioManager.Application.Tests/Reservations/Validators/ReservationWriteDtoValidatorTests.cs new file mode 100644 index 0000000..9ba621f --- /dev/null +++ b/StudioManager.Application.Tests/Reservations/Validators/ReservationWriteDtoValidatorTests.cs @@ -0,0 +1,160 @@ +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Application.Reservations.Validators; + +namespace StudioManager.Application.Tests.Reservations.Validators; + +[ExcludeFromCodeCoverage] +public sealed class ReservationWriteDtoValidatorTests +{ + private static readonly DateOnly ValidDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)); + private static readonly DateOnly InvalidDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)); + private static readonly DateOnly Today = DateOnly.FromDateTime(DateTime.UtcNow); + private static readonly DateOnly EmptyDate = DateOnly.MinValue; + + [Test] + public async Task should_return_error_when_start_date_is_lower_than_today_async() + { + // Arrange + var dto = new ReservationWriteDto(InvalidDate, ValidDate, 1, Guid.NewGuid()); + var validator = new ReservationWriteDtoValidator(); + + // Act + var result = await validator.ValidateAsync(dto); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().HaveCount(1); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(ReservationWriteDto.StartDate) && + x.ErrorMessage == $"'Start Date' must be greater than '{Today}'."); + } + + [Test] + public async Task should_return_error_when_start_date_is_empty_async() + { + // Arrange + var dto = new ReservationWriteDto(EmptyDate, ValidDate, 1, Guid.NewGuid()); + var validator = new ReservationWriteDtoValidator(); + + // Act + var result = await validator.ValidateAsync(dto); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(ReservationWriteDto.StartDate) && + x.ErrorMessage == "'Start Date' must not be empty."); + } + + [Test] + public async Task should_return_error_when_start_date_is_greater_than_end_day_async() + { + // Arrange + var dto = new ReservationWriteDto(ValidDate.AddDays(1), ValidDate, 1, Guid.NewGuid()); + var validator = new ReservationWriteDtoValidator(); + + // Act + var result = await validator.ValidateAsync(dto); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(ReservationWriteDto.StartDate) && + x.ErrorMessage == $"'Start Date' must be less than '{ValidDate}'."); + } + + [Test] + public async Task should_return_error_when_end_date_is_lower_than_today_async() + { + // Arrange + var dto = new ReservationWriteDto(InvalidDate, Today.AddDays(-1), 1, Guid.NewGuid()); + var validator = new ReservationWriteDtoValidator(); + + // Act + var result = await validator.ValidateAsync(dto); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(ReservationWriteDto.EndDate) && + x.ErrorMessage == $"'End Date' must be greater than '{Today}'."); + } + + [Test] + public async Task should_return_error_when_end_date_is_empty_async() + { + // Arrange + var dto = new ReservationWriteDto(EmptyDate, EmptyDate, 1, Guid.NewGuid()); + var validator = new ReservationWriteDtoValidator(); + + // Act + var result = await validator.ValidateAsync(dto); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(ReservationWriteDto.EndDate) && x.ErrorMessage == "'End Date' must not be empty."); + } + + [Test] + public async Task should_return_error_when_end_date_is_lower_than_end_day_async() + { + // Arrange + var dto = new ReservationWriteDto(ValidDate.AddDays(1), ValidDate, 1, Guid.NewGuid()); + var validator = new ReservationWriteDtoValidator(); + + // Act + var result = await validator.ValidateAsync(dto); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(ReservationWriteDto.EndDate) && + x.ErrorMessage == $"'End Date' must be greater than '{ValidDate.AddDays(1)}'."); + } + + [Test] + public async Task should_return_error_when_equipmentId_is_empty_async() + { + // Arrange + var dto = new ReservationWriteDto(ValidDate, ValidDate.AddDays(1), 1, Guid.Empty); + var validator = new ReservationWriteDtoValidator(); + + // Act + var result = await validator.ValidateAsync(dto); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(ReservationWriteDto.EquipmentId) && + x.ErrorMessage == "'Equipment Id' must not be empty."); + } + + [Test] + public async Task should_return_error_when_quantity_is_zero_async() + { + // Arrange + var dto = new ReservationWriteDto(ValidDate, ValidDate.AddDays(1), 0, Guid.NewGuid()); + var validator = new ReservationWriteDtoValidator(); + + // Act + var result = await validator.ValidateAsync(dto); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().Contain(x => + x.PropertyName == nameof(ReservationWriteDto.Quantity) && + x.ErrorMessage == "'Quantity' must be greater than '0'."); + } +} diff --git a/StudioManager.Application.Tests/StudioManager.Application.Tests.csproj b/StudioManager.Application.Tests/StudioManager.Application.Tests.csproj index 5127e70..7b6402e 100644 --- a/StudioManager.Application.Tests/StudioManager.Application.Tests.csproj +++ b/StudioManager.Application.Tests/StudioManager.Application.Tests.csproj @@ -11,23 +11,24 @@ - all - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + - all - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + diff --git a/StudioManager.Application/DbContextExtensions/EquipmentDatabaseExtensions.cs b/StudioManager.Application/DbContextExtensions/EquipmentDatabaseExtensions.cs index 9321e4b..38e0d6b 100644 --- a/StudioManager.Application/DbContextExtensions/EquipmentDatabaseExtensions.cs +++ b/StudioManager.Application/DbContextExtensions/EquipmentDatabaseExtensions.cs @@ -17,13 +17,13 @@ public static class EquipmentDatabaseExtensions .Where(filter.ToQuery()) .FirstOrDefaultAsync(cancellationToken); } - + public static async Task GetEquipmentAsync( - this DbContextBase dbContext, + this DbContextBase dbContext, Guid id, CancellationToken cancellationToken = default) { var filter = new EquipmentFilter { Id = id }; return await dbContext.GetEquipmentAsync(filter, cancellationToken); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/DbContextExtensions/EquipmentTypeDatabaseExtensions.cs b/StudioManager.Application/DbContextExtensions/EquipmentTypeDatabaseExtensions.cs index 18b218e..8508e31 100644 --- a/StudioManager.Application/DbContextExtensions/EquipmentTypeDatabaseExtensions.cs +++ b/StudioManager.Application/DbContextExtensions/EquipmentTypeDatabaseExtensions.cs @@ -16,7 +16,7 @@ public static async Task EquipmentTypeExistsAsync( .EquipmentTypes .AnyAsync(filter.ToQuery(), cancellationToken); } - + public static async Task GetEquipmentTypeAsync( this DbContextBase dbContext, EquipmentTypeFilter filter, @@ -26,4 +26,4 @@ public static async Task EquipmentTypeExistsAsync( .EquipmentTypes .FirstOrDefaultAsync(filter.ToQuery(), cancellationToken); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/DbContextExtensions/PaginationExtensions.cs b/StudioManager.Application/DbContextExtensions/PaginationExtensions.cs index 7b12dcb..fafd2ba 100644 --- a/StudioManager.Application/DbContextExtensions/PaginationExtensions.cs +++ b/StudioManager.Application/DbContextExtensions/PaginationExtensions.cs @@ -8,13 +8,6 @@ namespace StudioManager.Application.DbContextExtensions; [ExcludeFromCodeCoverage] public static class PaginationExtensions { - private static IQueryable ApplyPaging(this IQueryable queryable, PaginationDto paginationDto) - { - return paginationDto.Limit == 0 - ? queryable - : queryable.Skip(paginationDto.GetOffset()).Take(paginationDto.Limit); - } - public static async Task>> ApplyPagingAsync( this IQueryable queryable, PaginationDto pagination) @@ -25,6 +18,13 @@ public static async Task>> ApplyPagingAsync( return CreateResult(data, count, pagination); } + private static IQueryable ApplyPaging(this IQueryable queryable, PaginationDto paginationDto) + { + return paginationDto.Limit is null or 0 + ? queryable + : queryable.Skip(paginationDto.GetOffset()).Take(paginationDto.Limit.Value); + } + private static QueryResult> CreateResult(List data, int count, PaginationDto pagination) { return QueryResult.Success(new PagingResultDto @@ -32,10 +32,10 @@ private static QueryResult> CreateResult(List data, int Data = data, Pagination = new PaginationDetailsDto { - Limit = pagination.Limit, - Page = pagination.Page, + Limit = pagination.Limit!.Value, + Page = pagination.Page!.Value, Total = count } }); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/DbContextExtensions/ReservationDatabaseExtensions.cs b/StudioManager.Application/DbContextExtensions/ReservationDatabaseExtensions.cs new file mode 100644 index 0000000..beb706b --- /dev/null +++ b/StudioManager.Application/DbContextExtensions/ReservationDatabaseExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using StudioManager.Domain.Entities; +using StudioManager.Domain.Filters; +using StudioManager.Infrastructure.Common; + +namespace StudioManager.Application.DbContextExtensions; + +public static class ReservationDatabaseExtensions +{ + public static async Task> GetReservationsAsync( + this DbContextBase dbContext, + ReservationFilter filter, + CancellationToken cancellationToken) + { + return await dbContext.Reservations + .Where(filter.ToQuery()) + .ToListAsync(cancellationToken); + } + + public static async Task GetReservationAsync( + this DbContextBase dbContext, + Guid id, + CancellationToken cancellationToken) + { + var filter = new ReservationFilter { Id = id }; + return await dbContext.Reservations + .Where(filter.ToQuery()) + .FirstOrDefaultAsync(cancellationToken); + } +} diff --git a/StudioManager.Application/DependencyInjection.cs b/StudioManager.Application/DependencyInjection.cs index 7cc96ea..a0be3c5 100644 --- a/StudioManager.Application/DependencyInjection.cs +++ b/StudioManager.Application/DependencyInjection.cs @@ -9,11 +9,8 @@ public static class DependencyInjection { public static void RegisterApplication(this IServiceCollection services) { - services.AddAutoMapper(opt => - { - opt.AddMaps(AppDomain.CurrentDomain.GetAssemblies()); - }); + services.AddAutoMapper(opt => { opt.AddMaps(AppDomain.CurrentDomain.GetAssemblies()); }); services.AddValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies(), includeInternalTypes: true); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommand.cs b/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommand.cs index 4a1b406..8ae134d 100644 --- a/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommand.cs +++ b/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommand.cs @@ -4,4 +4,4 @@ namespace StudioManager.Application.EquipmentTypes.Create; -public sealed record CreateEquipmentTypeCommand(EquipmentTypeWriteDto EquipmentType) : IRequest; \ No newline at end of file +public sealed record CreateEquipmentTypeCommand(EquipmentTypeWriteDto EquipmentType) : IRequest; diff --git a/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommandHandler.cs b/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommandHandler.cs index 923bf2a..782a88a 100644 --- a/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommandHandler.cs +++ b/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommandHandler.cs @@ -15,28 +15,21 @@ public sealed class CreateEquipmentTypeCommandHandler( { public async Task Handle(CreateEquipmentTypeCommand request, CancellationToken cancellationToken) { - try - { - await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); - var filter = CreateFilter(); - var exists = await dbContext.EquipmentTypeExistsAsync(filter, cancellationToken); + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var filter = CreateFilter(); + var exists = await dbContext.EquipmentTypeExistsAsync(filter, cancellationToken); - if (exists) - { - return CommandResult.Conflict(DB.EQUIPMENT_TYPE_DUPLICATE_NAME); - } - - var equipmentType = EquipmentType.Create(request.EquipmentType.Name); - await dbContext.EquipmentTypes.AddAsync(equipmentType, cancellationToken); - await dbContext.SaveChangesAsync(cancellationToken); + if (exists) return CommandResult.Conflict(DB.EQUIPMENT_TYPE_DUPLICATE_NAME); - return CommandResult.Success(equipmentType.Id); - } - catch (DbUpdateException e) + var equipmentType = EquipmentType.Create(request.EquipmentType.Name); + await dbContext.EquipmentTypes.AddAsync(equipmentType, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + return CommandResult.Success(equipmentType.Id); + + EquipmentTypeFilter CreateFilter() { - return CommandResult.UnexpectedError(e); + return new EquipmentTypeFilter { ExactName = request.EquipmentType.Name }; } - - EquipmentTypeFilter CreateFilter() => new() { ExactName = request.EquipmentType.Name }; } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommandValidator.cs b/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommandValidator.cs index 9cebf2b..32dfb90 100644 --- a/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommandValidator.cs +++ b/StudioManager.Application/EquipmentTypes/Create/CreateEquipmentTypeCommandValidator.cs @@ -9,4 +9,4 @@ public CreateEquipmentTypeCommandValidator(EquipmentTypeWriteDtoValidator dtoVal { RuleFor(x => x.EquipmentType).SetValidator(dtoValidator); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommand.cs b/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommand.cs index 6e7299b..c103325 100644 --- a/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommand.cs +++ b/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommand.cs @@ -3,4 +3,4 @@ namespace StudioManager.Application.EquipmentTypes.Delete; -public sealed record DeleteEquipmentTypeCommand(Guid Id) : IRequest; \ No newline at end of file +public sealed record DeleteEquipmentTypeCommand(Guid Id) : IRequest; diff --git a/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommandHandler.cs b/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommandHandler.cs index 1d7861c..5a55dde 100644 --- a/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommandHandler.cs +++ b/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommandHandler.cs @@ -14,24 +14,14 @@ public sealed class DeleteEquipmentTypeCommandHandler( { public async Task Handle(DeleteEquipmentTypeCommand request, CancellationToken cancellationToken) { - try - { - await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); - var filter = new EquipmentTypeFilter { Id = request.Id }; - var existing = await dbContext.GetEquipmentTypeAsync(filter, cancellationToken); - - if (existing is null) - { - return CommandResult.NotFound(request.Id); - } + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var filter = new EquipmentTypeFilter { Id = request.Id }; + var existing = await dbContext.GetEquipmentTypeAsync(filter, cancellationToken); - dbContext.EquipmentTypes.Remove(existing); - await dbContext.SaveChangesAsync(cancellationToken); - return CommandResult.Success(); - } - catch (DbUpdateException e) - { - return CommandResult.UnexpectedError(e); - } + if (existing is null) return CommandResult.NotFound(request.Id); + + dbContext.EquipmentTypes.Remove(existing); + await dbContext.SaveChangesAsync(cancellationToken); + return CommandResult.Success(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommandValidator.cs b/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommandValidator.cs index 05caa59..ca24da6 100644 --- a/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommandValidator.cs +++ b/StudioManager.Application/EquipmentTypes/Delete/DeleteEquipmentTypeCommandValidator.cs @@ -9,4 +9,4 @@ public DeleteEquipmentTypeCommandValidator() RuleFor(x => x.Id) .NotEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/GetAll/GetEquipmentTypesQuery.cs b/StudioManager.Application/EquipmentTypes/GetAll/GetEquipmentTypesQuery.cs index 741bd5d..8bde8ef 100644 --- a/StudioManager.Application/EquipmentTypes/GetAll/GetEquipmentTypesQuery.cs +++ b/StudioManager.Application/EquipmentTypes/GetAll/GetEquipmentTypesQuery.cs @@ -8,4 +8,4 @@ namespace StudioManager.Application.EquipmentTypes.GetAll; public sealed class GetEquipmentTypesQuery : IRequest>> { public required EquipmentTypeFilter Filter { get; init; } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/GetAll/GetEquipmentTypesQueryHandler.cs b/StudioManager.Application/EquipmentTypes/GetAll/GetEquipmentTypesQueryHandler.cs index 4a94b4b..f9d54e2 100644 --- a/StudioManager.Application/EquipmentTypes/GetAll/GetEquipmentTypesQueryHandler.cs +++ b/StudioManager.Application/EquipmentTypes/GetAll/GetEquipmentTypesQueryHandler.cs @@ -19,10 +19,11 @@ public async Task>> Handle( { await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); var data = await dbContext.EquipmentTypes + .AsNoTracking() .Where(request.Filter.ToQuery()) .ProjectTo(mapper.ConfigurationProvider) .ToListAsync(cancellationToken); return QueryResult.Success(data); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/MapperProjections/EquipmentTypeProjection.cs b/StudioManager.Application/EquipmentTypes/MapperProjections/EquipmentTypeProjection.cs index cb8d3c3..2132a71 100644 --- a/StudioManager.Application/EquipmentTypes/MapperProjections/EquipmentTypeProjection.cs +++ b/StudioManager.Application/EquipmentTypes/MapperProjections/EquipmentTypeProjection.cs @@ -10,4 +10,4 @@ public EquipmentTypeProjection() { CreateMap(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommand.cs b/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommand.cs index c9c0847..4a4e4b7 100644 --- a/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommand.cs +++ b/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommand.cs @@ -4,4 +4,4 @@ namespace StudioManager.Application.EquipmentTypes.Update; -public sealed record UpdateEquipmentTypeCommand(Guid Id, EquipmentTypeWriteDto EquipmentType) : IRequest; \ No newline at end of file +public sealed record UpdateEquipmentTypeCommand(Guid Id, EquipmentTypeWriteDto EquipmentType) : IRequest; diff --git a/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommandHandler.cs b/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommandHandler.cs index 04660e8..8968eb4 100644 --- a/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommandHandler.cs +++ b/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommandHandler.cs @@ -15,36 +15,30 @@ public sealed class UpdateEquipmentTypeCommandHandler( { public async Task Handle(UpdateEquipmentTypeCommand request, CancellationToken cancellationToken) { - try + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var filter = CreateUniqueFilter(); + var exists = await dbContext.EquipmentTypeExistsAsync(filter, cancellationToken); + + if (exists) return CommandResult.Conflict(DB.EQUIPMENT_TYPE_DUPLICATE_NAME); + + filter = CreateFilter(); + var dbEquipmentType = await dbContext.EquipmentTypes.FirstOrDefaultAsync(filter.ToQuery(), cancellationToken); + if (dbEquipmentType is null) return CommandResult.NotFound(request.Id); + + dbEquipmentType.Update(request.EquipmentType.Name); + + await dbContext.SaveChangesAsync(cancellationToken); + return CommandResult.Success(); + + EquipmentTypeFilter CreateFilter() { - await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); - - var filter = CreateUniqueFilter(); - var exists = await dbContext.EquipmentTypeExistsAsync(filter, cancellationToken); - - if (exists) - { - return CommandResult.Conflict(DB.EQUIPMENT_TYPE_DUPLICATE_NAME); - } - - filter = CreateFilter(); - var dbEquipmentType = await dbContext.EquipmentTypes.FirstOrDefaultAsync(filter.ToQuery(), cancellationToken); - if (dbEquipmentType is null) - { - return CommandResult.NotFound(request.Id); - } - - dbEquipmentType.Update(request.EquipmentType.Name); - - await dbContext.SaveChangesAsync(cancellationToken); - return CommandResult.Success(); + return new EquipmentTypeFilter { Id = request.Id }; } - catch (DbUpdateException e) + + EquipmentTypeFilter CreateUniqueFilter() { - return CommandResult.UnexpectedError(e); + return new EquipmentTypeFilter { NotId = request.Id, ExactName = request.EquipmentType.Name }; } - - EquipmentTypeFilter CreateFilter() => new() { Id = request.Id }; - EquipmentTypeFilter CreateUniqueFilter() => new() { NotId = request.Id, ExactName = request.EquipmentType.Name}; } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommandValidator.cs b/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommandValidator.cs index 00ddff6..9c23f1b 100644 --- a/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommandValidator.cs +++ b/StudioManager.Application/EquipmentTypes/Update/UpdateEquipmentTypeCommandValidator.cs @@ -10,4 +10,4 @@ public UpdateEquipmentTypeCommandValidator(EquipmentTypeWriteDtoValidator dtoVal RuleFor(x => x.EquipmentType).SetValidator(dtoValidator); RuleFor(x => x.Id).NotEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EquipmentTypes/Validators/EquipmentTypeWriteDtoValidator.cs b/StudioManager.Application/EquipmentTypes/Validators/EquipmentTypeWriteDtoValidator.cs index 1aa9644..a7a3d50 100644 --- a/StudioManager.Application/EquipmentTypes/Validators/EquipmentTypeWriteDtoValidator.cs +++ b/StudioManager.Application/EquipmentTypes/Validators/EquipmentTypeWriteDtoValidator.cs @@ -10,4 +10,4 @@ public EquipmentTypeWriteDtoValidator() RuleFor(x => x.Name) .NotEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/Common/EquipmentChecker.cs b/StudioManager.Application/Equipments/Common/EquipmentChecker.cs index 97cbb5e..3119e4b 100644 --- a/StudioManager.Application/Equipments/Common/EquipmentChecker.cs +++ b/StudioManager.Application/Equipments/Common/EquipmentChecker.cs @@ -18,10 +18,7 @@ internal static async Task CheckEquipmentReferencesAsync( { var result = await CheckIfEquipmentTypeExistAsync(dbContext, dto.EquipmentTypeId, cancellationToken); - if (!result.Succeeded) - { - return CheckResult.Fail(result.CommandResult); - } + if (!result.Succeeded) return CheckResult.Fail(result.CommandResult); var filter = new EquipmentFilter { @@ -29,24 +26,24 @@ internal static async Task CheckEquipmentReferencesAsync( ExactName = dto.Name, EquipmentTypeId = dto.EquipmentTypeId }; - + return await EquipmentHasUniqueName(dbContext, filter, cancellationToken); } - + private static async Task EquipmentHasUniqueName(DbContextBase dbContext, EquipmentFilter filter, CancellationToken cancellationToken = default) { - var existing = await dbContext.GetEquipmentAsync(filter, cancellationToken); - + var existing = await dbContext.GetEquipmentAsync(filter, cancellationToken); + return existing is null ? CheckResult.Success() : CheckResult.Fail( CommandResult.Conflict( - string.Format(DB_FORMAT.EQUIPMENT_DUPLICATE_NAME_TYPE, + string.Format(DB_FORMAT.EQUIPMENT_DUPLICATE_NAME_TYPE, existing.Name, existing.EquipmentTypeId))); } - + private static async Task CheckIfEquipmentTypeExistAsync( DbContextBase dbContext, Guid equipmentTypeId, @@ -59,4 +56,4 @@ private static async Task CheckIfEquipmentTypeExistAsync( ? CheckResult.Success() : CheckResult.Fail(CommandResult.NotFound(equipmentTypeId)); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/Create/CreateEquipmentCommand.cs b/StudioManager.Application/Equipments/Create/CreateEquipmentCommand.cs index 91cc7b7..258817f 100644 --- a/StudioManager.Application/Equipments/Create/CreateEquipmentCommand.cs +++ b/StudioManager.Application/Equipments/Create/CreateEquipmentCommand.cs @@ -4,4 +4,4 @@ namespace StudioManager.Application.Equipments.Create; -public sealed record CreateEquipmentCommand(EquipmentWriteDto Equipment) : IRequest; \ No newline at end of file +public sealed record CreateEquipmentCommand(EquipmentWriteDto Equipment) : IRequest; diff --git a/StudioManager.Application/Equipments/Create/CreateEquipmentCommandHandler.cs b/StudioManager.Application/Equipments/Create/CreateEquipmentCommandHandler.cs index d9a23f2..50e0ca7 100644 --- a/StudioManager.Application/Equipments/Create/CreateEquipmentCommandHandler.cs +++ b/StudioManager.Application/Equipments/Create/CreateEquipmentCommandHandler.cs @@ -13,31 +13,21 @@ public sealed class CreateEquipmentCommandHandler( { public async Task Handle(CreateEquipmentCommand request, CancellationToken cancellationToken) { - try - { - await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); - var checkResult = await EquipmentChecker. - CheckEquipmentReferencesAsync(dbContext, null, request.Equipment, cancellationToken); - - if (!checkResult.Succeeded) - { - return checkResult.CommandResult; - } - - var equipment = Equipment.Create( - request.Equipment.Name, - request.Equipment.EquipmentTypeId, - request.Equipment.Quantity); + var checkResult = + await EquipmentChecker.CheckEquipmentReferencesAsync(dbContext, null, request.Equipment, cancellationToken); - await dbContext.Equipments.AddAsync(equipment, cancellationToken); - await dbContext.SaveChangesAsync(cancellationToken); - - return CommandResult.Success(equipment.Id); - } - catch (DbUpdateException e) - { - return CommandResult.UnexpectedError(e); - } + if (!checkResult.Succeeded) return checkResult.CommandResult; + + var equipment = Equipment.Create( + request.Equipment.Name, + request.Equipment.EquipmentTypeId, + request.Equipment.Quantity); + + await dbContext.Equipments.AddAsync(equipment, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + return CommandResult.Success(equipment.Id); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/Create/CreateEquipmentCommandValidator.cs b/StudioManager.Application/Equipments/Create/CreateEquipmentCommandValidator.cs index cc16ee9..2753e98 100644 --- a/StudioManager.Application/Equipments/Create/CreateEquipmentCommandValidator.cs +++ b/StudioManager.Application/Equipments/Create/CreateEquipmentCommandValidator.cs @@ -9,4 +9,4 @@ public CreateEquipmentCommandValidator(EquipmentWriteDtoValidator dtoValidator) { RuleFor(x => x.Equipment).SetValidator(dtoValidator); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommand.cs b/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommand.cs index 71c7c35..4c55a5f 100644 --- a/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommand.cs +++ b/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommand.cs @@ -3,4 +3,4 @@ namespace StudioManager.Application.Equipments.Delete; -public sealed record DeleteEquipmentCommand(Guid Id) : IRequest; \ No newline at end of file +public sealed record DeleteEquipmentCommand(Guid Id) : IRequest; diff --git a/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommandHandler.cs b/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommandHandler.cs index d3a2238..110bae2 100644 --- a/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommandHandler.cs +++ b/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommandHandler.cs @@ -15,34 +15,25 @@ public sealed class DeleteEquipmentCommandHandler( { public async Task Handle(DeleteEquipmentCommand request, CancellationToken cancellationToken) { - try - { - await using var context = await dbContextFactory.CreateDbContextAsync(cancellationToken); - var filter = new EquipmentFilter { Id = request.Id }; - var equipment = await context.GetEquipmentAsync(filter, cancellationToken); + await using var context = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var filter = new EquipmentFilter { Id = request.Id }; + var equipment = await context.GetEquipmentAsync(filter, cancellationToken); - if (equipment is null) - { - return CommandResult.NotFound(request.Id); - } - - if (!HasInitialQuantity(equipment)) - { - return CommandResult.Conflict( - string.Format(DB_FORMAT.EQUIPMENT_QUANTITY_MISSING_WHEN_REMOVING, - equipment.InitialQuantity, - equipment.Quantity)); - } + if (equipment is null) return CommandResult.NotFound(request.Id); - context.Equipments.Remove(equipment); - await context.SaveChangesAsync(cancellationToken); - return CommandResult.Success(); - } - catch (Exception ex) + if (!HasInitialQuantity(equipment)) + return CommandResult.Conflict( + string.Format(DB_FORMAT.EQUIPMENT_QUANTITY_MISSING_WHEN_REMOVING, + equipment.InitialQuantity, + equipment.Quantity)); + + context.Equipments.Remove(equipment); + await context.SaveChangesAsync(cancellationToken); + return CommandResult.Success(); + + bool HasInitialQuantity(Equipment eq) { - return CommandResult.UnexpectedError(ex.Message); + return eq.InitialQuantity == eq.Quantity; } - - bool HasInitialQuantity(Equipment eq) => eq.InitialQuantity == eq.Quantity; } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommandValidator.cs b/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommandValidator.cs index d539c39..10e9b8d 100644 --- a/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommandValidator.cs +++ b/StudioManager.Application/Equipments/Delete/DeleteEquipmentCommandValidator.cs @@ -8,4 +8,4 @@ public DeleteEquipmentCommandValidator() { RuleFor(x => x.Id).NotEmpty(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQuery.cs b/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQuery.cs index 06d3f1d..201564f 100644 --- a/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQuery.cs +++ b/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQuery.cs @@ -13,4 +13,4 @@ public sealed class GetAllEquipmentsQuery( { public EquipmentFilter Filter { get; } = filter; public PaginationDto Pagination { get; } = pagination; -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQueryHandler.cs b/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQueryHandler.cs index a64fab5..8f88c05 100644 --- a/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQueryHandler.cs +++ b/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQueryHandler.cs @@ -15,13 +15,15 @@ public sealed class GetAllEquipmentsQueryHandler( IDbContextFactory dbContextFactory) : IRequestHandler>> { - public async Task>> Handle(GetAllEquipmentsQuery request, CancellationToken cancellationToken) + public async Task>> Handle(GetAllEquipmentsQuery request, + CancellationToken cancellationToken) { await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); var data = dbContext.Equipments + .AsNoTracking() .Where(request.Filter.ToQuery()) .ProjectTo(mapper.ConfigurationProvider); return await data.ApplyPagingAsync(request.Pagination); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/MapperProjections/EquipmentProjection.cs b/StudioManager.Application/Equipments/MapperProjections/EquipmentProjection.cs index 80f2bb7..6f6286e 100644 --- a/StudioManager.Application/Equipments/MapperProjections/EquipmentProjection.cs +++ b/StudioManager.Application/Equipments/MapperProjections/EquipmentProjection.cs @@ -1,4 +1,5 @@ using AutoMapper; +using StudioManager.API.Contracts.Common; using StudioManager.API.Contracts.Equipments; using StudioManager.Domain.Entities; @@ -9,5 +10,6 @@ public sealed class EquipmentProjection : Profile public EquipmentProjection() { CreateMap(); + CreateMap(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/Update/UpdateEquipmentCommand.cs b/StudioManager.Application/Equipments/Update/UpdateEquipmentCommand.cs index 1f6465a..9eacc0c 100644 --- a/StudioManager.Application/Equipments/Update/UpdateEquipmentCommand.cs +++ b/StudioManager.Application/Equipments/Update/UpdateEquipmentCommand.cs @@ -4,4 +4,4 @@ namespace StudioManager.Application.Equipments.Update; -public sealed record UpdateEquipmentCommand(Guid Id, EquipmentWriteDto Equipment) : IRequest; \ No newline at end of file +public sealed record UpdateEquipmentCommand(Guid Id, EquipmentWriteDto Equipment) : IRequest; diff --git a/StudioManager.Application/Equipments/Update/UpdateEquipmentCommandHandler.cs b/StudioManager.Application/Equipments/Update/UpdateEquipmentCommandHandler.cs index 8d4aac7..0d21670 100644 --- a/StudioManager.Application/Equipments/Update/UpdateEquipmentCommandHandler.cs +++ b/StudioManager.Application/Equipments/Update/UpdateEquipmentCommandHandler.cs @@ -14,36 +14,23 @@ public sealed class UpdateEquipmentCommandHandler( { public async Task Handle(UpdateEquipmentCommand request, CancellationToken cancellationToken) { - try - { - await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); - - var checkResult = await EquipmentChecker.CheckEquipmentReferencesAsync( - dbContext, - request.Id, - request.Equipment, - cancellationToken); - - if (!checkResult.Succeeded) - { - return checkResult.CommandResult; - } - - var equipment = await dbContext.GetEquipmentAsync(request.Id, cancellationToken); - - if (equipment is null) - { - return CommandResult.NotFound(request.Id); - } - - equipment.Update(request.Equipment.Name, request.Equipment.EquipmentTypeId, request.Equipment.Quantity); - await dbContext.SaveChangesAsync(cancellationToken); - - return CommandResult.Success(); - } - catch (DbUpdateException e) - { - return CommandResult.UnexpectedError(e); - } + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var checkResult = await EquipmentChecker.CheckEquipmentReferencesAsync( + dbContext, + request.Id, + request.Equipment, + cancellationToken); + + if (!checkResult.Succeeded) return checkResult.CommandResult; + + var equipment = await dbContext.GetEquipmentAsync(request.Id, cancellationToken); + + if (equipment is null) return CommandResult.NotFound(request.Id); + + equipment.Update(request.Equipment.Name, request.Equipment.EquipmentTypeId, request.Equipment.Quantity); + await dbContext.SaveChangesAsync(cancellationToken); + + return CommandResult.Success(); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/Update/UpdateEquipmentCommandValidator.cs b/StudioManager.Application/Equipments/Update/UpdateEquipmentCommandValidator.cs index 08829a0..c83154c 100644 --- a/StudioManager.Application/Equipments/Update/UpdateEquipmentCommandValidator.cs +++ b/StudioManager.Application/Equipments/Update/UpdateEquipmentCommandValidator.cs @@ -11,4 +11,4 @@ public UpdateEquipmentCommandValidator(EquipmentWriteDtoValidator dtoValidator) RuleFor(x => x.Equipment).NotNull(); RuleFor(x => x.Equipment).SetValidator(dtoValidator); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/Equipments/Validators/EquipmentWriteDtoValidator.cs b/StudioManager.Application/Equipments/Validators/EquipmentWriteDtoValidator.cs index 5e531e9..ddad59b 100644 --- a/StudioManager.Application/Equipments/Validators/EquipmentWriteDtoValidator.cs +++ b/StudioManager.Application/Equipments/Validators/EquipmentWriteDtoValidator.cs @@ -12,4 +12,4 @@ public EquipmentWriteDtoValidator() RuleFor(x => x.Quantity).GreaterThanOrEqualTo(1); RuleFor(x => x.Quantity).LessThanOrEqualTo(int.MaxValue); } -} \ No newline at end of file +} diff --git a/StudioManager.Application/EventHandlers/Equipments/EquipmentReservationChangedEventHandler.cs b/StudioManager.Application/EventHandlers/Equipments/EquipmentReservationChangedEventHandler.cs new file mode 100644 index 0000000..77559d4 --- /dev/null +++ b/StudioManager.Application/EventHandlers/Equipments/EquipmentReservationChangedEventHandler.cs @@ -0,0 +1,31 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Infrastructure; +using StudioManager.Notifications.Equipment; + +namespace StudioManager.Application.EventHandlers.Equipments; + +public sealed class EquipmentReservationChangedEventHandler( + IDbContextFactory dbContextFactory, + ILogger logger) + : INotificationHandler +{ + public async Task Handle(EquipmentReservationChangedEvent notification, CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var equipment = await dbContext.GetEquipmentAsync(notification.EquipmentId, cancellationToken); + + if (equipment is null) + { + logger.LogWarning("Equipment with Id '{EquipmentId}' was not found when handling {Notification}", + notification.EquipmentId, nameof(EquipmentReservationChangedEvent)); + return; + } + + equipment.Reserve(notification.Quantity, notification.InitialQuantity); + await dbContext.SaveChangesAsync(cancellationToken); + } +} diff --git a/StudioManager.Application/EventHandlers/Equipments/EquipmentReservedEventHandler.cs b/StudioManager.Application/EventHandlers/Equipments/EquipmentReservedEventHandler.cs new file mode 100644 index 0000000..7dfb3a1 --- /dev/null +++ b/StudioManager.Application/EventHandlers/Equipments/EquipmentReservedEventHandler.cs @@ -0,0 +1,31 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Infrastructure; +using StudioManager.Notifications.Equipment; + +namespace StudioManager.Application.EventHandlers.Equipments; + +public sealed class EquipmentReservedEventHandler( + IDbContextFactory dbContextFactory, + ILogger logger) + : INotificationHandler +{ + public async Task Handle(EquipmentReservedEvent notification, CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var equipment = await dbContext.GetEquipmentAsync(notification.Id, cancellationToken); + + if (equipment is null) + { + logger.LogWarning("Equipment with Id '{EquipmentId}' was not found when handling {Notification}", + notification.Id, nameof(EquipmentReservedEvent)); + return; + } + + equipment.Reserve(notification.Quantity); + await dbContext.SaveChangesAsync(cancellationToken); + } +} diff --git a/StudioManager.Application/EventHandlers/Equipments/EquipmentReturnedEventHandler.cs b/StudioManager.Application/EventHandlers/Equipments/EquipmentReturnedEventHandler.cs new file mode 100644 index 0000000..6eaf35e --- /dev/null +++ b/StudioManager.Application/EventHandlers/Equipments/EquipmentReturnedEventHandler.cs @@ -0,0 +1,31 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Infrastructure; +using StudioManager.Notifications.Equipment; + +namespace StudioManager.Application.EventHandlers.Equipments; + +public sealed class EquipmentReturnedEventHandler( + IDbContextFactory dbContextFactory, + ILogger logger) + : INotificationHandler +{ + public async Task Handle(EquipmentReturnedEvent notification, CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var equipment = await dbContext.GetEquipmentAsync(notification.Id, cancellationToken); + + if (equipment is null) + { + logger.LogWarning("Equipment with Id '{EquipmentId}' was not found when handling {Notification}", + notification.Id, nameof(EquipmentReturnedEvent)); + return; + } + + equipment.Reserve(0, notification.Quantity); + await dbContext.SaveChangesAsync(cancellationToken); + } +} diff --git a/StudioManager.Application/Reservations/Common/ReservationsChecker.cs b/StudioManager.Application/Reservations/Common/ReservationsChecker.cs new file mode 100644 index 0000000..d80104d --- /dev/null +++ b/StudioManager.Application/Reservations/Common/ReservationsChecker.cs @@ -0,0 +1,41 @@ +using StudioManager.API.Contracts.Reservations; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Domain.Common.Results; +using StudioManager.Domain.ErrorMessages; +using StudioManager.Domain.Filters; +using StudioManager.Infrastructure.Common; + +namespace StudioManager.Application.Reservations.Common; + +public static class ReservationsChecker +{ + public static async Task CheckReservationAsync( + DbContextBase dbContext, + ReservationWriteDto newReservation, + Guid? notId = null, + CancellationToken cancellationToken = default) + { + var equipment = await dbContext.GetEquipmentAsync(newReservation.EquipmentId, cancellationToken); + + if (equipment is null) return CommandResult.Conflict(DB.RESERVATION_EQUIPMENT_NOT_FOUND); + + if (equipment.Quantity - newReservation.Quantity < 0) + return CommandResult.Conflict(DB.RESERVATION_EQUIPMENT_QUANTITY_INSUFFICIENT); + + var existingReservationsFilter = new ReservationFilter + { + NotId = notId, + MinEndDate = newReservation.StartDate, + MaxStartDate = newReservation.EndDate + }; + + var reservations = await dbContext.GetReservationsAsync(existingReservationsFilter, cancellationToken); + + if (reservations.Count <= 0) return CommandResult.Success(); + + var reservedQuantities = reservations.Sum(r => r.Quantity); + return equipment.Quantity - reservedQuantities - newReservation.Quantity < 0 + ? CommandResult.Conflict(DB.RESERVATION_EQUIPMENT_USED_BY_OTHERS_IN_PERIOD) + : CommandResult.Success(); + } +} diff --git a/StudioManager.Application/Reservations/Create/CreateReservationCommand.cs b/StudioManager.Application/Reservations/Create/CreateReservationCommand.cs new file mode 100644 index 0000000..ccef50a --- /dev/null +++ b/StudioManager.Application/Reservations/Create/CreateReservationCommand.cs @@ -0,0 +1,7 @@ +using MediatR; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Domain.Common.Results; + +namespace StudioManager.Application.Reservations.Create; + +public sealed record CreateReservationCommand(ReservationWriteDto Reservation) : IRequest; diff --git a/StudioManager.Application/Reservations/Create/CreateReservationCommandHandler.cs b/StudioManager.Application/Reservations/Create/CreateReservationCommandHandler.cs new file mode 100644 index 0000000..394cc16 --- /dev/null +++ b/StudioManager.Application/Reservations/Create/CreateReservationCommandHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using StudioManager.Application.Reservations.Common; +using StudioManager.Domain.Common.Results; +using StudioManager.Domain.Entities; +using StudioManager.Infrastructure; +using StudioManager.Notifications.Equipment; + +namespace StudioManager.Application.Reservations.Create; + +public sealed class CreateReservationCommandHandler( + IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle(CreateReservationCommand request, CancellationToken cancellationToken) + { + var reservation = request.Reservation; + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var checkResult = + await ReservationsChecker.CheckReservationAsync(dbContext, reservation, null, cancellationToken); + + if (!checkResult.Succeeded) return checkResult; + + var dbReservation = Reservation.Create( + reservation.StartDate, + reservation.EndDate, + reservation.Quantity, + reservation.EquipmentId); + + await dbContext.Reservations.AddAsync(dbReservation, cancellationToken); + + dbReservation.AddDomainEvent(new EquipmentReservedEvent(reservation.EquipmentId, reservation.Quantity)); + await dbContext.SaveChangesAsync(cancellationToken); + + return CommandResult.Success(dbReservation.Id); + } +} diff --git a/StudioManager.Application/Reservations/Create/CreateReservationCommandValidator.cs b/StudioManager.Application/Reservations/Create/CreateReservationCommandValidator.cs new file mode 100644 index 0000000..77b9cef --- /dev/null +++ b/StudioManager.Application/Reservations/Create/CreateReservationCommandValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using StudioManager.Application.Reservations.Validators; + +namespace StudioManager.Application.Reservations.Create; + +public sealed class CreateReservationCommandValidator : AbstractValidator +{ + public CreateReservationCommandValidator(ReservationWriteDtoValidator dtoValidator) + { + RuleFor(x => x.Reservation).NotNull(); + RuleFor(x => x.Reservation).SetValidator(dtoValidator); + } +} diff --git a/StudioManager.Application/Reservations/Delete/DeleteReservationCommand.cs b/StudioManager.Application/Reservations/Delete/DeleteReservationCommand.cs new file mode 100644 index 0000000..6929833 --- /dev/null +++ b/StudioManager.Application/Reservations/Delete/DeleteReservationCommand.cs @@ -0,0 +1,6 @@ +using MediatR; +using StudioManager.Domain.Common.Results; + +namespace StudioManager.Application.Reservations.Delete; + +public sealed record DeleteReservationCommand(Guid Id) : IRequest; diff --git a/StudioManager.Application/Reservations/Delete/DeleteReservationCommandHandler.cs b/StudioManager.Application/Reservations/Delete/DeleteReservationCommandHandler.cs new file mode 100644 index 0000000..7104322 --- /dev/null +++ b/StudioManager.Application/Reservations/Delete/DeleteReservationCommandHandler.cs @@ -0,0 +1,27 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Domain.Common.Results; +using StudioManager.Domain.Entities; +using StudioManager.Infrastructure; +using StudioManager.Notifications.Equipment; + +namespace StudioManager.Application.Reservations.Delete; + +public sealed class DeleteReservationCommandHandler( + IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle(DeleteReservationCommand request, CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var dbReservation = await dbContext.GetReservationAsync(request.Id, cancellationToken); + + if (dbReservation is null) return CommandResult.NotFound(request.Id); + dbReservation.AddDomainEvent( + new EquipmentReservationChangedEvent(dbReservation.EquipmentId, 0, dbReservation.Quantity)); + dbContext.Reservations.Remove(dbReservation); + await dbContext.SaveChangesAsync(cancellationToken); + return CommandResult.Success(); + } +} diff --git a/StudioManager.Application/Reservations/Delete/DeleteReservationCommandValidator.cs b/StudioManager.Application/Reservations/Delete/DeleteReservationCommandValidator.cs new file mode 100644 index 0000000..9a44ebc --- /dev/null +++ b/StudioManager.Application/Reservations/Delete/DeleteReservationCommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace StudioManager.Application.Reservations.Delete; + +public sealed class DeleteReservationCommandValidator : AbstractValidator +{ + public DeleteReservationCommandValidator() + { + RuleFor(x => x.Id).NotEmpty(); + } +} diff --git a/StudioManager.Application/Reservations/GetAll/GetAllReservationsQuery.cs b/StudioManager.Application/Reservations/GetAll/GetAllReservationsQuery.cs new file mode 100644 index 0000000..432d437 --- /dev/null +++ b/StudioManager.Application/Reservations/GetAll/GetAllReservationsQuery.cs @@ -0,0 +1,16 @@ +using MediatR; +using StudioManager.API.Contracts.Pagination; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Domain.Common.Results; +using StudioManager.Domain.Filters; + +namespace StudioManager.Application.Reservations.GetAll; + +public sealed class GetAllReservationsQuery( + ReservationFilter filter, + PaginationDto pagination) + : IRequest>> +{ + public ReservationFilter Filter { get; } = filter; + public PaginationDto Pagination { get; } = pagination; +} diff --git a/StudioManager.Application/Reservations/GetAll/GetAllReservationsQueryHandler.cs b/StudioManager.Application/Reservations/GetAll/GetAllReservationsQueryHandler.cs new file mode 100644 index 0000000..f29ad9d --- /dev/null +++ b/StudioManager.Application/Reservations/GetAll/GetAllReservationsQueryHandler.cs @@ -0,0 +1,29 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using MediatR; +using Microsoft.EntityFrameworkCore; +using StudioManager.API.Contracts.Pagination; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Domain.Common.Results; +using StudioManager.Infrastructure; + +namespace StudioManager.Application.Reservations.GetAll; + +public sealed class GetAllReservationsQueryHandler( + IDbContextFactory dbContextFactory, + IMapper mapper) + : IRequestHandler>> +{ + public async Task>> Handle(GetAllReservationsQuery request, + CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var data = dbContext.Reservations + .AsNoTracking() + .Where(request.Filter.ToQuery()) + .ProjectTo(mapper.ConfigurationProvider); + + return await data.ApplyPagingAsync(request.Pagination); + } +} diff --git a/StudioManager.Application/Reservations/MapperProjections/ReservationProjection.cs b/StudioManager.Application/Reservations/MapperProjections/ReservationProjection.cs new file mode 100644 index 0000000..a576ea7 --- /dev/null +++ b/StudioManager.Application/Reservations/MapperProjections/ReservationProjection.cs @@ -0,0 +1,13 @@ +using AutoMapper; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Domain.Entities; + +namespace StudioManager.Application.Reservations.MapperProjections; + +public sealed class ReservationProjection : Profile +{ + public ReservationProjection() + { + CreateMap(); + } +} diff --git a/StudioManager.Application/Reservations/Update/UpdateReservationCommand.cs b/StudioManager.Application/Reservations/Update/UpdateReservationCommand.cs new file mode 100644 index 0000000..e39e102 --- /dev/null +++ b/StudioManager.Application/Reservations/Update/UpdateReservationCommand.cs @@ -0,0 +1,7 @@ +using MediatR; +using StudioManager.API.Contracts.Reservations; +using StudioManager.Domain.Common.Results; + +namespace StudioManager.Application.Reservations.Update; + +public sealed record UpdateReservationCommand(Guid Id, ReservationWriteDto Reservation) : IRequest; diff --git a/StudioManager.Application/Reservations/Update/UpdateReservationCommandHandler.cs b/StudioManager.Application/Reservations/Update/UpdateReservationCommandHandler.cs new file mode 100644 index 0000000..a745f13 --- /dev/null +++ b/StudioManager.Application/Reservations/Update/UpdateReservationCommandHandler.cs @@ -0,0 +1,36 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using StudioManager.Application.DbContextExtensions; +using StudioManager.Application.Reservations.Common; +using StudioManager.Domain.Common.Results; +using StudioManager.Domain.Entities; +using StudioManager.Infrastructure; + +namespace StudioManager.Application.Reservations.Update; + +public sealed class UpdateReservationCommandHandler( + IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle(UpdateReservationCommand request, CancellationToken cancellationToken) + { + var reservation = request.Reservation; + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var checkResult = + await ReservationsChecker.CheckReservationAsync(dbContext, reservation, request.Id, cancellationToken); + + if (!checkResult.Succeeded) return checkResult; + + var dbReservation = await dbContext.GetReservationAsync(request.Id, cancellationToken); + + if (dbReservation is null) return CommandResult.NotFound(request.Id); + + dbReservation.Update(reservation.StartDate, reservation.EndDate, reservation.Quantity, reservation.EquipmentId); + + dbContext.Reservations.Update(dbReservation); + await dbContext.SaveChangesAsync(cancellationToken); + + return CommandResult.Success(); + } +} diff --git a/StudioManager.Application/Reservations/Update/UpdateReservationCommandValidator.cs b/StudioManager.Application/Reservations/Update/UpdateReservationCommandValidator.cs new file mode 100644 index 0000000..713a240 --- /dev/null +++ b/StudioManager.Application/Reservations/Update/UpdateReservationCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using StudioManager.Application.Reservations.Validators; + +namespace StudioManager.Application.Reservations.Update; + +public sealed class UpdateReservationCommandValidator : AbstractValidator +{ + public UpdateReservationCommandValidator(ReservationWriteDtoValidator dtoValidator) + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.Reservation).NotNull(); + RuleFor(x => x.Reservation).SetValidator(dtoValidator); + } +} diff --git a/StudioManager.Application/Reservations/Validators/ReservationWriteDtoValidator.cs b/StudioManager.Application/Reservations/Validators/ReservationWriteDtoValidator.cs new file mode 100644 index 0000000..f96393b --- /dev/null +++ b/StudioManager.Application/Reservations/Validators/ReservationWriteDtoValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using StudioManager.API.Contracts.Reservations; + +namespace StudioManager.Application.Reservations.Validators; + +public sealed class ReservationWriteDtoValidator : AbstractValidator +{ + public ReservationWriteDtoValidator() + { + RuleFor(x => x.StartDate).NotEmpty(); + RuleFor(x => x.StartDate).LessThan(x => x.EndDate); + RuleFor(x => x.StartDate).GreaterThan(DateOnly.FromDateTime(DateTime.UtcNow)); + + RuleFor(x => x.EndDate).NotEmpty(); + RuleFor(x => x.EndDate).GreaterThan(DateOnly.FromDateTime(DateTime.UtcNow)); + RuleFor(x => x.EndDate).GreaterThan(x => x.StartDate); + + RuleFor(x => x.EquipmentId).NotEmpty(); + + RuleFor(x => x.Quantity).NotNull(); + RuleFor(x => x.Quantity).GreaterThan(0); + RuleFor(x => x.Quantity).LessThanOrEqualTo(int.MaxValue); + } +} diff --git a/StudioManager.Application/StudioManager.Application.csproj b/StudioManager.Application/StudioManager.Application.csproj index 9c04556..8519368 100644 --- a/StudioManager.Application/StudioManager.Application.csproj +++ b/StudioManager.Application/StudioManager.Application.csproj @@ -7,20 +7,16 @@ - - - + + + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + diff --git a/StudioManager.Domain/Common/Results/CheckResult.cs b/StudioManager.Domain/Common/Results/CheckResult.cs index 2a82908..5cd5bfe 100644 --- a/StudioManager.Domain/Common/Results/CheckResult.cs +++ b/StudioManager.Domain/Common/Results/CheckResult.cs @@ -5,14 +5,16 @@ namespace StudioManager.Domain.Common.Results; [ExcludeFromCodeCoverage] public sealed class CheckResult { - private CheckResult() { } - + private CheckResult() + { + } + public bool Succeeded { get; private set; } public T Data { get; private set; } = default!; public CommandResult CommandResult { get; private set; } = default!; /// - /// Creates successful result with data and success command result (with empty data) + /// Creates successful result with data and success command result (with empty data) /// /// data to return as Generic parameter /// successful check with data of type T @@ -27,7 +29,7 @@ public static CheckResult Success(T data) } /// - /// Returns failed result with NotFound command result + /// Returns failed result with NotFound command result /// /// id of entity that was not found /// Type of entity that was not found @@ -36,9 +38,9 @@ public static CheckResult NotFound(object? id = null) { return Fail(CommandResult.NotFound(id)); } - + /// - /// Returns failed result with Conflict command result + /// Returns failed result with Conflict command result /// /// Message to pass to conflict command resukt /// failed check with empty data and Conflict CommandResult @@ -46,7 +48,7 @@ public static CheckResult Conflict(string message) { return Fail(CommandResult.Conflict(message)); } - + private static CheckResult Fail(CommandResult result) { return new CheckResult @@ -60,13 +62,15 @@ private static CheckResult Fail(CommandResult result) public sealed class CheckResult { - private CheckResult() { } - + private CheckResult() + { + } + public bool Succeeded { get; private set; } public CommandResult CommandResult { get; private set; } = default!; /// - /// Creates successful result with success command result + /// Creates successful result with success command result /// /// successful check with success command result public static CheckResult Success() @@ -79,7 +83,7 @@ public static CheckResult Success() } /// - /// Returns failed result with NotFound command result + /// Returns failed result with NotFound command result /// /// Command result to return /// failed check with NotFound CommandResult @@ -91,4 +95,4 @@ public static CheckResult Fail(CommandResult result) CommandResult = result }; } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Common/Results/CommandResult.cs b/StudioManager.Domain/Common/Results/CommandResult.cs index 4800d46..7fe17a9 100644 --- a/StudioManager.Domain/Common/Results/CommandResult.cs +++ b/StudioManager.Domain/Common/Results/CommandResult.cs @@ -6,13 +6,15 @@ namespace StudioManager.Domain.Common.Results; [ExcludeFromCodeCoverage] public sealed class CommandResult : IRequestResult { - private CommandResult() { } - + private CommandResult() + { + } + public bool Succeeded { get; private init; } public HttpStatusCode StatusCode { get; private init; } public object? Data { get; private init; } public string? Error { get; private init; } - + public static CommandResult Success(object? data = null) { return new CommandResult @@ -23,13 +25,13 @@ public static CommandResult Success(object? data = null) StatusCode = HttpStatusCode.OK }; } - + public static CommandResult NotFound(object? id = null) { var message = id is not null ? $"[NOT FOUND] {typeof(T).Name} with id '{id}' does not exist" : $"[NOT FOUND] {typeof(T).Name} does not exist"; - + return new CommandResult { Succeeded = false, @@ -38,7 +40,7 @@ public static CommandResult NotFound(object? id = null) StatusCode = HttpStatusCode.NotFound }; } - + public static CommandResult Conflict(string error) { return new CommandResult @@ -49,7 +51,7 @@ public static CommandResult Conflict(string error) StatusCode = HttpStatusCode.Conflict }; } - + public static CommandResult UnexpectedError(string error) { return new CommandResult @@ -60,13 +62,13 @@ public static CommandResult UnexpectedError(string error) StatusCode = HttpStatusCode.InternalServerError }; } - + public static CommandResult UnexpectedError(Exception error) { var message = error.InnerException is not null ? $"[EX]: {error.InnerException.Message} {Environment.NewLine} [INNER]: {error.InnerException.Message}" : error.Message; - + return UnexpectedError(message); } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Common/Results/IRequestResult.cs b/StudioManager.Domain/Common/Results/IRequestResult.cs index 889be8c..a1de2dc 100644 --- a/StudioManager.Domain/Common/Results/IRequestResult.cs +++ b/StudioManager.Domain/Common/Results/IRequestResult.cs @@ -8,4 +8,4 @@ public interface IRequestResult public HttpStatusCode StatusCode { get; } public T? Data { get; } public string? Error { get; } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Common/Results/QueryResult.cs b/StudioManager.Domain/Common/Results/QueryResult.cs index 93b8600..d4ca1c7 100644 --- a/StudioManager.Domain/Common/Results/QueryResult.cs +++ b/StudioManager.Domain/Common/Results/QueryResult.cs @@ -10,7 +10,7 @@ public class QueryResult : IRequestResult public HttpStatusCode StatusCode { get; private init; } public T? Data { get; private init; } public string? Error { get; private init; } - + public static QueryResult Success(T data) { return new QueryResult @@ -21,7 +21,7 @@ public static QueryResult Success(T data) StatusCode = HttpStatusCode.OK }; } - + public static QueryResult NotFound(string error) { return new QueryResult @@ -32,7 +32,7 @@ public static QueryResult NotFound(string error) StatusCode = HttpStatusCode.NotFound }; } - + public static QueryResult Conflict(string error) { return new QueryResult @@ -43,7 +43,7 @@ public static QueryResult Conflict(string error) StatusCode = HttpStatusCode.Conflict }; } - + public static QueryResult UnexpectedError(string error) { return new QueryResult @@ -62,4 +62,4 @@ public static QueryResult Success(T data) { return QueryResult.Success(data); } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Entities/EntityBase.cs b/StudioManager.Domain/Entities/EntityBase.cs index efd7429..2f2cc61 100644 --- a/StudioManager.Domain/Entities/EntityBase.cs +++ b/StudioManager.Domain/Entities/EntityBase.cs @@ -1,6 +1,22 @@ -namespace StudioManager.Domain.Entities; +using System.ComponentModel.DataAnnotations.Schema; +using MediatR; + +namespace StudioManager.Domain.Entities; public abstract class EntityBase { - public Guid Id { get; set; } -} \ No newline at end of file + private readonly List _entityEvents = []; + public Guid Id { get; protected init; } + + [NotMapped] public IReadOnlyCollection DomainEvents => _entityEvents; + + public void AddDomainEvent(INotification domainEvent) + { + _entityEvents.Add(domainEvent); + } + + public void ClearDomainEvents() + { + _entityEvents.Clear(); + } +} diff --git a/StudioManager.Domain/Entities/Equipment.cs b/StudioManager.Domain/Entities/Equipment.cs index 8fdb8ef..8387134 100644 --- a/StudioManager.Domain/Entities/Equipment.cs +++ b/StudioManager.Domain/Entities/Equipment.cs @@ -10,22 +10,13 @@ public sealed class Equipment : EntityBase public int InitialQuantity { get; private set; } public string ImageUrl { get; private set; } = default!; - - #region EntityRelations - - public EquipmentType EquipmentType { get; set; } = default!; - - #endregion - - public void Reserve(int quantity) + public void Reserve(int quantity, int initialQuantity = 0) { + Quantity += initialQuantity; Quantity -= quantity; - if (Quantity < 0) - { - throw new InvalidOperationException(EX.EQUIPMENT_NEGATIVE_QUANTITY); - } + if (Quantity < 0) throw new InvalidOperationException(EX.EQUIPMENT_NEGATIVE_QUANTITY); } - + public static Equipment Create(string name, Guid equipmentTypeId, int quantity) { const string imagePlaceholder = "https://cdn3.iconfinder.com/data/icons/objects/512/equipment-512.png"; @@ -39,7 +30,7 @@ public static Equipment Create(string name, Guid equipmentTypeId, int quantity) ImageUrl = imagePlaceholder }; } - + public void Update(string name, Guid equipmentTypeId, int quantity) { Name = name; @@ -47,4 +38,40 @@ public void Update(string name, Guid equipmentTypeId, int quantity) Quantity = quantity; InitialQuantity = quantity; } -} \ No newline at end of file + + + + + #region EntityRelations + + + + + + + + + + + + + + + public EquipmentType EquipmentType { get; set; } = default!; + public ICollection Reservations { get; set; } = new List(); + + + + + + + + + + + + + + + #endregion +} diff --git a/StudioManager.Domain/Entities/EquipmentType.cs b/StudioManager.Domain/Entities/EquipmentType.cs index df3adaf..ff10a92 100644 --- a/StudioManager.Domain/Entities/EquipmentType.cs +++ b/StudioManager.Domain/Entities/EquipmentType.cs @@ -3,18 +3,50 @@ public sealed class EquipmentType : EntityBase { public string Name { get; private set; } = default!; - + + + + #region EntityRelations - + + + + + + + + + + + + + + public ICollection Equipments { get; set; } = new List(); + + + + + + + + + + + + + #endregion - + + + + public void Update(string name) { Name = name; } - + public static EquipmentType Create(string name) { return new EquipmentType @@ -23,4 +55,4 @@ public static EquipmentType Create(string name) Name = name }; } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Entities/Reservation.cs b/StudioManager.Domain/Entities/Reservation.cs new file mode 100644 index 0000000..d579f03 --- /dev/null +++ b/StudioManager.Domain/Entities/Reservation.cs @@ -0,0 +1,79 @@ +using StudioManager.Notifications.Equipment; + +namespace StudioManager.Domain.Entities; + +public sealed class Reservation : EntityBase +{ + public DateOnly StartDate { get; private set; } + public DateOnly EndDate { get; private set; } + public int Quantity { get; private set; } + public Guid EquipmentId { get; private set; } + + + + + #region EntityRelations + + + + + + + + + + + + + + + public Equipment Equipment { get; init; } = null!; + + + + + + + + + + + + + + + #endregion + + + + + public static Reservation Create( + DateOnly startDate, + DateOnly endDate, + int quantity, + Guid equipmentId) + { + return new Reservation + { + Id = Guid.NewGuid(), + StartDate = startDate, + EndDate = endDate, + Quantity = quantity, + EquipmentId = equipmentId + }; + } + + public void Update( + DateOnly startDate, + DateOnly endDate, + int quantity, + Guid equipmentId) + { + AddDomainEvent(new EquipmentReservationChangedEvent(equipmentId, quantity, Quantity)); + + StartDate = startDate; + EndDate = endDate; + Quantity = quantity; + EquipmentId = equipmentId; + } +} diff --git a/StudioManager.Domain/EntitiesConfiguration/EquipmentConfiguration.cs b/StudioManager.Domain/EntitiesConfiguration/EquipmentConfiguration.cs index 906b3cd..b589aff 100644 --- a/StudioManager.Domain/EntitiesConfiguration/EquipmentConfiguration.cs +++ b/StudioManager.Domain/EntitiesConfiguration/EquipmentConfiguration.cs @@ -11,21 +11,21 @@ public sealed class EquipmentConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); - + builder.Property(x => x.Name).IsRequired(); builder.HasIndex(x => x.Name, "IX_Equipment_Name"); - + builder.Property(x => x.Quantity).IsRequired(); builder.HasIndex(x => x.Quantity, "IX_Equipment_Quantity"); builder.Property(x => x.InitialQuantity).IsRequired(); - + builder.Ignore(x => x.ImageUrl); - + builder.Property(x => x.EquipmentTypeId).IsRequired(); builder.HasIndex(x => x.EquipmentTypeId, "IX_Equipment_EquipmentTypeId"); - - builder.HasIndex(x => new{ x.Name, x.EquipmentTypeId }, "IX_Equipment_Name_EquipmentTypeId").IsUnique(); + + builder.HasIndex(x => new { x.Name, x.EquipmentTypeId }, "IX_Equipment_Name_EquipmentTypeId").IsUnique(); builder.HasOne(e => e.EquipmentType) .WithMany(et => et.Equipments) @@ -33,4 +33,4 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(e => e.EquipmentTypeId) .OnDelete(DeleteBehavior.Restrict); } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/EntitiesConfiguration/EquipmentTypeConfiguration.cs b/StudioManager.Domain/EntitiesConfiguration/EquipmentTypeConfiguration.cs index 7322513..80e55de 100644 --- a/StudioManager.Domain/EntitiesConfiguration/EquipmentTypeConfiguration.cs +++ b/StudioManager.Domain/EntitiesConfiguration/EquipmentTypeConfiguration.cs @@ -14,4 +14,4 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(x => x.Name, "IX_EquipmentType_Name").IsUnique(); builder.Property(x => x.Name).IsRequired(); } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/EntitiesConfiguration/ReservationConfiguration.cs b/StudioManager.Domain/EntitiesConfiguration/ReservationConfiguration.cs new file mode 100644 index 0000000..b4b4bf6 --- /dev/null +++ b/StudioManager.Domain/EntitiesConfiguration/ReservationConfiguration.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using StudioManager.Domain.Entities; + +namespace StudioManager.Domain.EntitiesConfiguration; + +[ExcludeFromCodeCoverage] +public sealed class ReservationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + + builder.Property(x => x.Quantity).IsRequired(); + + builder.Property(x => x.EquipmentId).IsRequired(); + builder.HasIndex(x => x.EquipmentId, "IX_Reservations_EquipmentId"); + + builder.Property(x => x.StartDate).IsRequired(); + builder.Property(x => x.EndDate).IsRequired(); + builder.HasIndex(x => new { x.StartDate, x.EndDate }, "IX_Reservations_StartDate_EndDate"); + + builder.HasOne(r => r.Equipment) + .WithMany(e => e.Reservations) + .HasPrincipalKey(e => e.Id) + .HasForeignKey(r => r.EquipmentId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/StudioManager.Domain/ErrorMessages/DB.cs b/StudioManager.Domain/ErrorMessages/DB.cs index 3e8d0cc..e9bae0d 100644 --- a/StudioManager.Domain/ErrorMessages/DB.cs +++ b/StudioManager.Domain/ErrorMessages/DB.cs @@ -1,24 +1,123 @@ // ReSharper disable InconsistentNaming + namespace StudioManager.Domain.ErrorMessages; public static class DB { public const string HAS_OPEN_TRANSACTION = "Transaction is already open"; public const string NO_OPEN_TRANSACTION = "Transaction has not been started"; - + + + + #region EquipmentType - + + + + + + + + + + + + + + public const string EQUIPMENT_TYPE_DUPLICATE_NAME = "Equipment type with this name already exists"; - + + + + + + + + + + + + + + + #endregion + + + + + #region Reservations + + + + + + + + + + + + + + + public const string RESERVATION_EQUIPMENT_NOT_FOUND = "Cannot create reservation for equipment that does not exist"; + + public const string RESERVATION_EQUIPMENT_QUANTITY_INSUFFICIENT = + "Cannot create reservation, the quantity requested is greater than the available quantity"; + + public const string RESERVATION_EQUIPMENT_USED_BY_OTHERS_IN_PERIOD = + "Cannot create reservation, the equipment is already reserved by others in the specified period"; + + + + + + + + + + + + + + #endregion } public static class DB_FORMAT { #region Equipment - - public const string EQUIPMENT_QUANTITY_MISSING_WHEN_REMOVING = "Cannot remove equipment, the initial count ({0}) is not equal to the current count ({1})"; + + + + + + + + + + + + + + + public const string EQUIPMENT_QUANTITY_MISSING_WHEN_REMOVING = + "Cannot remove equipment, the initial count ({0}) is not equal to the current count ({1})"; + public const string EQUIPMENT_DUPLICATE_NAME_TYPE = "Equipment with name {0} and type {1} already exists"; - + + + + + + + + + + + + + + #endregion -} \ No newline at end of file +} diff --git a/StudioManager.Domain/ErrorMessages/EX.cs b/StudioManager.Domain/ErrorMessages/EX.cs index 3e477d6..559482a 100644 --- a/StudioManager.Domain/ErrorMessages/EX.cs +++ b/StudioManager.Domain/ErrorMessages/EX.cs @@ -1,4 +1,5 @@ // ReSharper disable InconsistentNaming + namespace StudioManager.Domain.ErrorMessages; public static class EX @@ -6,4 +7,4 @@ public static class EX public const string SUCCESS_FROM_ERROR = "Cannot create success result from error"; public const string ERROR_FROM_SUCCESS = "Cannot create error result from success"; public const string EQUIPMENT_NEGATIVE_QUANTITY = "Equipment quantity cannot be negative"; -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Filters/EquipmentFilter.cs b/StudioManager.Domain/Filters/EquipmentFilter.cs index 2cd3740..07078a3 100644 --- a/StudioManager.Domain/Filters/EquipmentFilter.cs +++ b/StudioManager.Domain/Filters/EquipmentFilter.cs @@ -11,6 +11,7 @@ public sealed class EquipmentFilter : IFilter public IEnumerable EquipmentTypeIds { get; init; } = []; public string? ExactName { get; init; } public string? Search { get; init; } + public Expression> ToQuery() { var lowerSearch = Search?.ToLower(); @@ -26,4 +27,4 @@ public Expression> ToQuery() x.Name.ToLower().Contains(lowerSearch)) && (!EquipmentTypeIds.Any() || EquipmentTypeIds.Contains(x.EquipmentTypeId)); } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Filters/EquipmentTypeFilter.cs b/StudioManager.Domain/Filters/EquipmentTypeFilter.cs index f4c0425..700e006 100644 --- a/StudioManager.Domain/Filters/EquipmentTypeFilter.cs +++ b/StudioManager.Domain/Filters/EquipmentTypeFilter.cs @@ -4,7 +4,8 @@ namespace StudioManager.Domain.Filters; -[SuppressMessage("Performance", "CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons")] +[SuppressMessage("Performance", + "CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons")] public sealed class EquipmentTypeFilter : IFilter { public Guid? Id { get; init; } @@ -12,20 +13,21 @@ public sealed class EquipmentTypeFilter : IFilter public string? Name { get; init; } public string? ExactName { get; init; } public string? Search { get; init; } - + public Expression> ToQuery() { var lowerSearch = Search?.ToLower(); var lowerName = Name?.ToLower(); var lowerExactName = ExactName?.ToLower(); - + return equipmentType => (!Id.HasValue || equipmentType.Id == Id) && (!NotId.HasValue || equipmentType.Id != NotId) && - (lowerExactName == null || lowerExactName.Length == 0 || equipmentType.Name.ToLower().Equals(lowerExactName)) && + (lowerExactName == null || lowerExactName.Length == 0 || + equipmentType.Name.ToLower().Equals(lowerExactName)) && (lowerName == null || lowerName.Length == 0 || equipmentType.Name.ToLower().Equals(lowerName)) && - (lowerSearch == null || lowerSearch.Length == 0 || + (lowerSearch == null || lowerSearch.Length == 0 || equipmentType.Id.ToString().ToLower().Equals(lowerSearch) || equipmentType.Name.ToLower().Contains(lowerSearch)); } -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Filters/IFilter.cs b/StudioManager.Domain/Filters/IFilter.cs index 188ac93..c80b0ab 100644 --- a/StudioManager.Domain/Filters/IFilter.cs +++ b/StudioManager.Domain/Filters/IFilter.cs @@ -5,4 +5,4 @@ namespace StudioManager.Domain.Filters; public interface IFilter { Expression> ToQuery(); -} \ No newline at end of file +} diff --git a/StudioManager.Domain/Filters/ReservationFilter.cs b/StudioManager.Domain/Filters/ReservationFilter.cs new file mode 100644 index 0000000..ad7d0b4 --- /dev/null +++ b/StudioManager.Domain/Filters/ReservationFilter.cs @@ -0,0 +1,32 @@ +using System.Linq.Expressions; +using StudioManager.Domain.Entities; + +namespace StudioManager.Domain.Filters; + +public sealed class ReservationFilter : IFilter +{ + public Guid? Id { get; init; } + public Guid? NotId { get; init; } + public DateOnly? StartDate { get; init; } + public DateOnly? EndDate { get; init; } + public string? Search { get; init; } + + public DateOnly? MaxStartDate { get; init; } + public DateOnly? MinEndDate { get; init; } + + public Expression> ToQuery() + { + var lowerSearch = Search?.ToLower(); + + return x => + (!Id.HasValue || x.Id == Id) && + (!NotId.HasValue || x.Id != NotId) && + (lowerSearch == null || lowerSearch.Length == 0 || + x.Id.ToString().ToLower().Equals(lowerSearch) || + x.Equipment.Name.ToLower().Contains(lowerSearch)) && + (!StartDate.HasValue || x.StartDate >= StartDate) && + (!EndDate.HasValue || x.EndDate <= EndDate) && + (!MaxStartDate.HasValue || x.StartDate <= MaxStartDate) && + (!MinEndDate.HasValue || x.EndDate >= MinEndDate); + } +} diff --git a/StudioManager.Domain/StudioManager.Domain.csproj b/StudioManager.Domain/StudioManager.Domain.csproj index 1882ba9..67d6ef2 100644 --- a/StudioManager.Domain/StudioManager.Domain.csproj +++ b/StudioManager.Domain/StudioManager.Domain.csproj @@ -7,11 +7,12 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + diff --git a/StudioManager.Infrastructure/Common/DatabaseConfiguration.cs b/StudioManager.Infrastructure/Common/DatabaseConfiguration.cs index 27a74ee..e8da285 100644 --- a/StudioManager.Infrastructure/Common/DatabaseConfiguration.cs +++ b/StudioManager.Infrastructure/Common/DatabaseConfiguration.cs @@ -6,6 +6,6 @@ namespace StudioManager.Infrastructure.Common; public class DatabaseConfiguration { public const string NodeName = "DatabaseConfiguration"; - + public DatabaseConfigurationNode Write { get; set; } = null!; -} \ No newline at end of file +} diff --git a/StudioManager.Infrastructure/Common/DatabaseConfigurationNode.cs b/StudioManager.Infrastructure/Common/DatabaseConfigurationNode.cs index 0ec88a0..977dd2c 100644 --- a/StudioManager.Infrastructure/Common/DatabaseConfigurationNode.cs +++ b/StudioManager.Infrastructure/Common/DatabaseConfigurationNode.cs @@ -4,7 +4,9 @@ namespace StudioManager.Infrastructure.Common; [ExcludeFromCodeCoverage] public sealed class DatabaseConfigurationNode( - string password, string userName, string connectionDetails) + string password, + string userName, + string connectionDetails) { private string Password { get; } = password; private string Username { get; } = userName; @@ -14,4 +16,4 @@ public string GetConnectionString() { return $"{ConnectionDetails}User Id = {Username};Password = {Password};"; } -} \ No newline at end of file +} diff --git a/StudioManager.Infrastructure/Common/DbContextBase.cs b/StudioManager.Infrastructure/Common/DbContextBase.cs index e9e457b..bc3d6ee 100644 --- a/StudioManager.Infrastructure/Common/DbContextBase.cs +++ b/StudioManager.Infrastructure/Common/DbContextBase.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; +using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using StudioManager.Domain.Entities; @@ -9,29 +10,25 @@ namespace StudioManager.Infrastructure.Common; [ExcludeFromCodeCoverage] public abstract class DbContextBase( - DbContextOptions options) : DbContext(options) + DbContextOptions options, + IMediator mediator) : DbContext(options) { + private IDbContextTransaction? _transaction; public DbSet EquipmentTypes { get; set; } public DbSet Equipments { get; set; } - + public DbSet Reservations { get; set; } + private bool HasOpenTransaction => _transaction is not null; - private IDbContextTransaction? _transaction; - + public async Task BeginTransactionAsync() { - if (HasOpenTransaction) - { - throw new InvalidOperationException(DB.HAS_OPEN_TRANSACTION); - } + if (HasOpenTransaction) throw new InvalidOperationException(DB.HAS_OPEN_TRANSACTION); _transaction = await Database.BeginTransactionAsync(); } - + public async Task CommitTransactionAsync() { - if (!HasOpenTransaction) - { - throw new InvalidOperationException(DB.NO_OPEN_TRANSACTION); - } + if (!HasOpenTransaction) throw new InvalidOperationException(DB.NO_OPEN_TRANSACTION); await _transaction!.CommitAsync(); _transaction.Dispose(); _transaction = null; @@ -42,4 +39,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); base.OnModelCreating(modelBuilder); } -} \ No newline at end of file + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + await mediator.DispatchDomainEventsAsync(this); + return await base.SaveChangesAsync(cancellationToken); + } +} diff --git a/StudioManager.Infrastructure/Common/MediatorExtensions.cs b/StudioManager.Infrastructure/Common/MediatorExtensions.cs new file mode 100644 index 0000000..fa04bd4 --- /dev/null +++ b/StudioManager.Infrastructure/Common/MediatorExtensions.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.CodeAnalysis; +using MediatR; +using StudioManager.Domain.Entities; + +namespace StudioManager.Infrastructure.Common; + +[ExcludeFromCodeCoverage] +public static class MediatorExtensions +{ + public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContextBase dbContext) + { + var domainEntities = dbContext.ChangeTracker + .Entries() + .Where(x => x.Entity.DomainEvents.Count != 0) + .ToList(); + + var domainEvents = domainEntities + .SelectMany(x => x.Entity.DomainEvents) + .ToList(); + + domainEntities.ForEach(e => e.Entity.ClearDomainEvents()); + + foreach (var @event in domainEvents) await mediator.Publish(@event); + } +} diff --git a/StudioManager.Infrastructure/DependencyInjection.cs b/StudioManager.Infrastructure/DependencyInjection.cs index 182ebb4..09108da 100644 --- a/StudioManager.Infrastructure/DependencyInjection.cs +++ b/StudioManager.Infrastructure/DependencyInjection.cs @@ -11,28 +11,28 @@ public static class DependencyInjection { public static void RegisterInfrastructure(this IServiceCollection services, IConfiguration configuration) { - var databaseConfiguration = configuration.GetSection(DatabaseConfiguration.NodeName).Get(); - + var databaseConfiguration = + configuration.GetSection(DatabaseConfiguration.NodeName).Get(); + ArgumentNullException.ThrowIfNull(databaseConfiguration, nameof(databaseConfiguration)); - + services.RegisterPooledDbContext(databaseConfiguration.Write); } - private static void RegisterPooledDbContext(this IServiceCollection services, DatabaseConfigurationNode connectionDetails) + private static void RegisterPooledDbContext(this IServiceCollection services, + DatabaseConfigurationNode connectionDetails) where TContext : DbContextBase { var connectionString = connectionDetails.GetConnectionString(); - + ArgumentException.ThrowIfNullOrWhiteSpace(connectionString, nameof(connectionString)); - + services.AddPooledDbContextFactory(options => { - options.UseNpgsql(connectionString, opt => - { - opt.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); - }); + options.UseNpgsql(connectionString, + opt => { opt.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); }); options.UseSnakeCaseNamingConvention(); }); } -} \ No newline at end of file +} diff --git a/StudioManager.Infrastructure/Migrations/20240518165326_AddReservation.Designer.cs b/StudioManager.Infrastructure/Migrations/20240518165326_AddReservation.Designer.cs new file mode 100644 index 0000000..44b49fe --- /dev/null +++ b/StudioManager.Infrastructure/Migrations/20240518165326_AddReservation.Designer.cs @@ -0,0 +1,154 @@ +// +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using StudioManager.Infrastructure; + +#nullable disable + +namespace StudioManager.Infrastructure.Migrations +{ + [ExcludeFromCodeCoverage] + [DbContext(typeof(StudioManagerDbContext))] + [Migration("20240518165326_AddReservation")] + partial class AddReservation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("StudioManager.Domain.Entities.Equipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("EquipmentTypeId") + .HasColumnType("uuid") + .HasColumnName("equipment_type_id"); + + b.Property("ImageUrl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("image_url"); + + b.Property("InitialQuantity") + .HasColumnType("integer") + .HasColumnName("initial_quantity"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.HasKey("Id") + .HasName("pk_equipments"); + + b.HasIndex("EquipmentTypeId") + .HasDatabaseName("ix_equipments_equipment_type_id"); + + b.ToTable("equipments", (string)null); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.EquipmentType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_equipment_types"); + + b.ToTable("equipment_types", (string)null); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_date"); + + b.Property("EquipmentId") + .HasColumnType("uuid") + .HasColumnName("equipment_id"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("pk_reservations"); + + b.HasIndex("EquipmentId") + .HasDatabaseName("ix_reservations_equipment_id"); + + b.ToTable("reservations", (string)null); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Equipment", b => + { + b.HasOne("StudioManager.Domain.Entities.EquipmentType", "EquipmentType") + .WithMany("Equipments") + .HasForeignKey("EquipmentTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_equipments_equipment_types_equipment_type_id"); + + b.Navigation("EquipmentType"); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Reservation", b => + { + b.HasOne("StudioManager.Domain.Entities.Equipment", "Equipment") + .WithMany("Reservations") + .HasForeignKey("EquipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reservations_equipments_equipment_id"); + + b.Navigation("Equipment"); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Equipment", b => + { + b.Navigation("Reservations"); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.EquipmentType", b => + { + b.Navigation("Equipments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/StudioManager.Infrastructure/Migrations/20240518165326_AddReservation.cs b/StudioManager.Infrastructure/Migrations/20240518165326_AddReservation.cs new file mode 100644 index 0000000..2915f5c --- /dev/null +++ b/StudioManager.Infrastructure/Migrations/20240518165326_AddReservation.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace StudioManager.Infrastructure.Migrations +{ + /// + public partial class AddReservation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "reservations", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + start_date = table.Column(type: "timestamp with time zone", nullable: false), + end_date = table.Column(type: "timestamp with time zone", nullable: false), + quantity = table.Column(type: "integer", nullable: false), + equipment_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_reservations", x => x.id); + table.ForeignKey( + name: "fk_reservations_equipments_equipment_id", + column: x => x.equipment_id, + principalTable: "equipments", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_reservations_equipment_id", + table: "reservations", + column: "equipment_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "reservations"); + } + } +} diff --git a/StudioManager.Infrastructure/Migrations/20240520182929_FixReservation.Designer.cs b/StudioManager.Infrastructure/Migrations/20240520182929_FixReservation.Designer.cs new file mode 100644 index 0000000..3513bf4 --- /dev/null +++ b/StudioManager.Infrastructure/Migrations/20240520182929_FixReservation.Designer.cs @@ -0,0 +1,152 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using StudioManager.Infrastructure; + +#nullable disable + +namespace StudioManager.Infrastructure.Migrations +{ + [DbContext(typeof(StudioManagerDbContext))] + [Migration("20240520182929_FixReservation")] + partial class FixReservation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("StudioManager.Domain.Entities.Equipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("EquipmentTypeId") + .HasColumnType("uuid") + .HasColumnName("equipment_type_id"); + + b.Property("ImageUrl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("image_url"); + + b.Property("InitialQuantity") + .HasColumnType("integer") + .HasColumnName("initial_quantity"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.HasKey("Id") + .HasName("pk_equipments"); + + b.HasIndex("EquipmentTypeId") + .HasDatabaseName("ix_equipments_equipment_type_id"); + + b.ToTable("equipments", (string)null); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.EquipmentType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_equipment_types"); + + b.ToTable("equipment_types", (string)null); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("EquipmentId") + .HasColumnType("uuid") + .HasColumnName("equipment_id"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("pk_reservations"); + + b.HasIndex("EquipmentId") + .HasDatabaseName("ix_reservations_equipment_id"); + + b.ToTable("reservations", (string)null); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Equipment", b => + { + b.HasOne("StudioManager.Domain.Entities.EquipmentType", "EquipmentType") + .WithMany("Equipments") + .HasForeignKey("EquipmentTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_equipments_equipment_types_equipment_type_id"); + + b.Navigation("EquipmentType"); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Reservation", b => + { + b.HasOne("StudioManager.Domain.Entities.Equipment", "Equipment") + .WithMany("Reservations") + .HasForeignKey("EquipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reservations_equipments_equipment_id"); + + b.Navigation("Equipment"); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Equipment", b => + { + b.Navigation("Reservations"); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.EquipmentType", b => + { + b.Navigation("Equipments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/StudioManager.Infrastructure/Migrations/20240520182929_FixReservation.cs b/StudioManager.Infrastructure/Migrations/20240520182929_FixReservation.cs new file mode 100644 index 0000000..c97f315 --- /dev/null +++ b/StudioManager.Infrastructure/Migrations/20240520182929_FixReservation.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace StudioManager.Infrastructure.Migrations +{ + /// + public partial class FixReservation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "start_date", + table: "reservations", + type: "date", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "end_date", + table: "reservations", + type: "date", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "start_date", + table: "reservations", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateOnly), + oldType: "date"); + + migrationBuilder.AlterColumn( + name: "end_date", + table: "reservations", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateOnly), + oldType: "date"); + } + } +} diff --git a/StudioManager.Infrastructure/Migrations/StudioManagerDbContextModelSnapshot.cs b/StudioManager.Infrastructure/Migrations/StudioManagerDbContextModelSnapshot.cs index bd51cbb..59e21b3 100644 --- a/StudioManager.Infrastructure/Migrations/StudioManagerDbContextModelSnapshot.cs +++ b/StudioManager.Infrastructure/Migrations/StudioManagerDbContextModelSnapshot.cs @@ -1,6 +1,5 @@ // using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -11,7 +10,6 @@ namespace StudioManager.Infrastructure.Migrations { - [ExcludeFromCodeCoverage] [DbContext(typeof(StudioManagerDbContext))] partial class StudioManagerDbContextModelSnapshot : ModelSnapshot { @@ -80,6 +78,38 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("equipment_types", (string)null); }); + modelBuilder.Entity("StudioManager.Domain.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("EquipmentId") + .HasColumnType("uuid") + .HasColumnName("equipment_id"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("pk_reservations"); + + b.HasIndex("EquipmentId") + .HasDatabaseName("ix_reservations_equipment_id"); + + b.ToTable("reservations", (string)null); + }); + modelBuilder.Entity("StudioManager.Domain.Entities.Equipment", b => { b.HasOne("StudioManager.Domain.Entities.EquipmentType", "EquipmentType") @@ -92,6 +122,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("EquipmentType"); }); + modelBuilder.Entity("StudioManager.Domain.Entities.Reservation", b => + { + b.HasOne("StudioManager.Domain.Entities.Equipment", "Equipment") + .WithMany("Reservations") + .HasForeignKey("EquipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reservations_equipments_equipment_id"); + + b.Navigation("Equipment"); + }); + + modelBuilder.Entity("StudioManager.Domain.Entities.Equipment", b => + { + b.Navigation("Reservations"); + }); + modelBuilder.Entity("StudioManager.Domain.Entities.EquipmentType", b => { b.Navigation("Equipments"); diff --git a/StudioManager.Infrastructure/StudioManager.Infrastructure.csproj b/StudioManager.Infrastructure/StudioManager.Infrastructure.csproj index ad3dcbf..679007e 100644 --- a/StudioManager.Infrastructure/StudioManager.Infrastructure.csproj +++ b/StudioManager.Infrastructure/StudioManager.Infrastructure.csproj @@ -7,23 +7,20 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + - + diff --git a/StudioManager.Infrastructure/StudioManagerDbContext.cs b/StudioManager.Infrastructure/StudioManagerDbContext.cs index 408b156..fd1bc23 100644 --- a/StudioManager.Infrastructure/StudioManagerDbContext.cs +++ b/StudioManager.Infrastructure/StudioManagerDbContext.cs @@ -1,9 +1,10 @@ using System.Diagnostics.CodeAnalysis; +using MediatR; using Microsoft.EntityFrameworkCore; using StudioManager.Infrastructure.Common; namespace StudioManager.Infrastructure; [ExcludeFromCodeCoverage] -public sealed class StudioManagerDbContext - (DbContextOptions options) : DbContextBase(options); \ No newline at end of file +public sealed class StudioManagerDbContext(DbContextOptions options, IMediator mediator) + : DbContextBase(options, mediator); diff --git a/StudioManager.Notifications/Equipment/EquipmentReservationChangedEvent.cs b/StudioManager.Notifications/Equipment/EquipmentReservationChangedEvent.cs new file mode 100644 index 0000000..358a1cb --- /dev/null +++ b/StudioManager.Notifications/Equipment/EquipmentReservationChangedEvent.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace StudioManager.Notifications.Equipment; + +public sealed class EquipmentReservationChangedEvent(Guid equipmentId, int quantity, int initialQuantity) + : INotification +{ + public Guid EquipmentId { get; } = equipmentId; + public int Quantity { get; } = quantity; + public int InitialQuantity { get; } = initialQuantity; +} diff --git a/StudioManager.Notifications/Equipment/EquipmentReservedEvent.cs b/StudioManager.Notifications/Equipment/EquipmentReservedEvent.cs new file mode 100644 index 0000000..e8922ff --- /dev/null +++ b/StudioManager.Notifications/Equipment/EquipmentReservedEvent.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace StudioManager.Notifications.Equipment; + +public sealed class EquipmentReservedEvent(Guid id, int quantity) : INotification +{ + public Guid Id { get; } = id; + public int Quantity { get; } = quantity; +} diff --git a/StudioManager.Notifications/Equipment/EquipmentReturnedEvent.cs b/StudioManager.Notifications/Equipment/EquipmentReturnedEvent.cs new file mode 100644 index 0000000..bca8058 --- /dev/null +++ b/StudioManager.Notifications/Equipment/EquipmentReturnedEvent.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace StudioManager.Notifications.Equipment; + +public sealed record EquipmentReturnedEvent(Guid Id, int Quantity) : INotification; diff --git a/StudioManager.Notifications/StudioManager.Notifications.csproj b/StudioManager.Notifications/StudioManager.Notifications.csproj new file mode 100644 index 0000000..ce0ab18 --- /dev/null +++ b/StudioManager.Notifications/StudioManager.Notifications.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/StudioManager.Tests.Common/AutoMapperExtensions/MappingTestHelper.cs b/StudioManager.Tests.Common/AutoMapperExtensions/MappingTestHelper.cs index 062f616..9320411 100644 --- a/StudioManager.Tests.Common/AutoMapperExtensions/MappingTestHelper.cs +++ b/StudioManager.Tests.Common/AutoMapperExtensions/MappingTestHelper.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using AutoMapper; +using StudioManager.Application; namespace StudioManager.Tests.Common.AutoMapperExtensions; @@ -10,17 +11,17 @@ public static class MappingTestHelper private static MapperConfiguration? _mapperConfiguration; public static MapperConfiguration MapperConfiguration => GetMapperConfiguration(); public static IMapper Mapper => GetMapper(); - + private static MapperConfiguration GetMapperConfiguration() { return _mapperConfiguration ??= new MapperConfiguration(cfg => { - cfg.AddMaps(typeof(Application.DependencyInjection).Assembly); + cfg.AddMaps(typeof(DependencyInjection).Assembly); }); } - + private static IMapper GetMapper() { return _mapper ??= MapperConfiguration.CreateMapper(); } -} \ No newline at end of file +} diff --git a/StudioManager.Tests.Common/DbContextExtensions/TestDbContextFactory.cs b/StudioManager.Tests.Common/DbContextExtensions/TestDbContextFactory.cs index 76d6c9a..c935d82 100644 --- a/StudioManager.Tests.Common/DbContextExtensions/TestDbContextFactory.cs +++ b/StudioManager.Tests.Common/DbContextExtensions/TestDbContextFactory.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; +using MediatR; using Microsoft.EntityFrameworkCore; +using Moq; namespace StudioManager.Tests.Common.DbContextExtensions; @@ -7,8 +9,9 @@ namespace StudioManager.Tests.Common.DbContextExtensions; public class TestDbContextFactory(string? connectionString) : IDbContextFactory where TContext : DbContext { - private readonly string? _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); - + private readonly string? _connectionString = + connectionString ?? throw new ArgumentNullException(nameof(connectionString)); + public TContext CreateDbContext() { throw new InvalidOperationException("This method should not be called. Use CreateDbContextAsync instead."); @@ -18,19 +21,23 @@ public Task CreateDbContextAsync(CancellationToken cancellationToken = { return Task.FromResult(CreateDbContext()); } - + private TDbContext CreateDbContext() where TDbContext : DbContext { var dbContextOptionsBuilder = new DbContextOptionsBuilder() .EnableSensitiveDataLogging(); dbContextOptionsBuilder.UseNpgsql(_connectionString, - npgsql => - { - npgsql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); - }).UseSnakeCaseNamingConvention(); + npgsql => { npgsql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); }) + .UseSnakeCaseNamingConvention(); + + var mediator = new Mock(); + mediator.Setup(x => x.Publish( + It.IsAny(), + It.IsAny())).Returns(Task.CompletedTask); - var dbContext = (TDbContext)Activator.CreateInstance(typeof(TDbContext), dbContextOptionsBuilder.Options)!; + var dbContext = + (TDbContext)Activator.CreateInstance(typeof(TDbContext), dbContextOptionsBuilder.Options, mediator.Object)!; return dbContext; } -} \ No newline at end of file +} diff --git a/StudioManager.Tests.Common/DbContextExtensions/TestDbMigrator.cs b/StudioManager.Tests.Common/DbContextExtensions/TestDbMigrator.cs index 78d557f..0c537d9 100644 --- a/StudioManager.Tests.Common/DbContextExtensions/TestDbMigrator.cs +++ b/StudioManager.Tests.Common/DbContextExtensions/TestDbMigrator.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; +using MediatR; using Microsoft.EntityFrameworkCore; +using Moq; using Testcontainers.PostgreSql; namespace StudioManager.Tests.Common.DbContextExtensions; @@ -23,7 +25,7 @@ public async Task MigrateDbAsync() await context.Database.MigrateAsync(); return _postgresContainer.GetConnectionString(); } - + public async Task ClearAsync() { await using var context = CreateDb(); @@ -41,12 +43,16 @@ private TContext CreateDb() .EnableSensitiveDataLogging(); dbContextOptionsBuilder.UseNpgsql(_postgresContainer.GetConnectionString(), - npgsql => - { - npgsql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); - }).UseSnakeCaseNamingConvention(); + npgsql => { npgsql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); }) + .UseSnakeCaseNamingConvention(); + + var mediator = new Mock(); + mediator.Setup(x => x.Publish( + It.IsAny(), + It.IsAny())).Returns(Task.CompletedTask); - var dbContext = (TContext)Activator.CreateInstance(typeof(TContext), dbContextOptionsBuilder.Options)!; + var dbContext = + (TContext)Activator.CreateInstance(typeof(TContext), dbContextOptionsBuilder.Options, mediator.Object)!; return dbContext; } -} \ No newline at end of file +} diff --git a/StudioManager.Tests.Common/IntegrationTestBase.cs b/StudioManager.Tests.Common/IntegrationTestBase.cs index 6eb8da9..95ea9d1 100644 --- a/StudioManager.Tests.Common/IntegrationTestBase.cs +++ b/StudioManager.Tests.Common/IntegrationTestBase.cs @@ -14,14 +14,12 @@ namespace StudioManager.Tests.Common; [ExcludeFromCodeCoverage] public abstract class IntegrationTestBase { - protected static readonly CancellationTokenSource Cts = new(); - protected static readonly IMapper Mapper = MappingTestHelper.Mapper; - - protected const HttpStatusCode OkStatusCode = HttpStatusCode.OK; protected const HttpStatusCode ConflictStatusCode = HttpStatusCode.Conflict; protected const HttpStatusCode NotFoundStatusCode = HttpStatusCode.NotFound; protected const HttpStatusCode UnexpectedErrorStatusCode = HttpStatusCode.InternalServerError; + protected static readonly CancellationTokenSource Cts = new(); + protected static readonly IMapper Mapper = MappingTestHelper.Mapper; protected TestDbMigrator DbMigrator { get; private set; } = null!; @@ -31,7 +29,7 @@ public async Task SetupContainersAsync() DbMigrator = new TestDbMigrator(); await DbMigrator.StartDbAsync(); } - + [OneTimeTearDown] public async Task DisposeContainersAsync() { @@ -45,13 +43,9 @@ protected static async Task ClearTableContentsForAsync( where T : class { if (filter is not null) - { await db.Set().Where(filter).ExecuteDeleteAsync(Cts.Token); - } else - { await db.Set().ExecuteDeleteAsync(Cts.Token); - } await db.SaveChangesAsync(); } @@ -64,4 +58,4 @@ protected static async Task AddEntitiesToTable( await db.Set().AddRangeAsync(entities, Cts.Token); await db.SaveChangesAsync(Cts.Token); } -} \ No newline at end of file +} diff --git a/StudioManager.Tests.Common/StudioManager.Tests.Common.csproj b/StudioManager.Tests.Common/StudioManager.Tests.Common.csproj index 860d3a4..fe5c7a7 100644 --- a/StudioManager.Tests.Common/StudioManager.Tests.Common.csproj +++ b/StudioManager.Tests.Common/StudioManager.Tests.Common.csproj @@ -10,15 +10,16 @@ - - - - + + + + + - - + + diff --git a/StudioManager.sln b/StudioManager.sln index ff2442d..abd4656 100644 --- a/StudioManager.sln +++ b/StudioManager.sln @@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\workflows\build_test.yaml = .github\workflows\build_test.yaml docker-compose.yml = docker-compose.yml pg-init-scripts\create-multiple-postgresql-databases.sh = pg-init-scripts\create-multiple-postgresql-databases.sh + coverlet.runsettings = coverlet.runsettings EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StudioManager.Infrastructure", "StudioManager.Infrastructure\StudioManager.Infrastructure.csproj", "{08F62C62-D31E-4E64-929F-D5DBF0C78E10}" @@ -23,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StudioManager.Application.T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StudioManager.Tests.Common", "StudioManager.Tests.Common\StudioManager.Tests.Common.csproj", "{8F76B06F-B382-4ECD-9F87-B4F9180CB25C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StudioManager.Notifications", "StudioManager.Notifications\StudioManager.Notifications.csproj", "{E1B4CFCA-4EC4-42DD-A104-C47F6529ED02}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +60,10 @@ Global {8F76B06F-B382-4ECD-9F87-B4F9180CB25C}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F76B06F-B382-4ECD-9F87-B4F9180CB25C}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F76B06F-B382-4ECD-9F87-B4F9180CB25C}.Release|Any CPU.Build.0 = Release|Any CPU + {E1B4CFCA-4EC4-42DD-A104-C47F6529ED02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1B4CFCA-4EC4-42DD-A104-C47F6529ED02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1B4CFCA-4EC4-42DD-A104-C47F6529ED02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1B4CFCA-4EC4-42DD-A104-C47F6529ED02}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {F030658C-CEB0-47C1-95C5-E6BFC183D338} = {83545C4A-3E03-4336-8AE3-B06E8836DE35} diff --git a/coverlet.runsettings b/coverlet.runsettings new file mode 100644 index 0000000..1969895 --- /dev/null +++ b/coverlet.runsettings @@ -0,0 +1,17 @@ + + + + + + + [*]StudioManager.Infrastructure*,[*]StudioManager.Tests* + Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverage + true + false + true + false + + + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 25175f1..c9c6481 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,14 +19,14 @@ - ./.containers/studiomanager-db:/var/lib/postgresql/data - ./pg-init-scripts:/docker-entrypoint-initdb.d restart: unless-stopped - + #### KEYCLOAK #### studiomanager.keycloak: image: quay.io/keycloak/keycloak:latest container_name: studiomanager.keycloak - command: ["start-dev", "--import-realm"] + command: [ "start-dev", "--import-realm" ] environment: #KC_LOG_LEVEL: debug KC_DB_VENDOR: postgres @@ -48,7 +48,7 @@ volumes: - ./.containers/realm.json:/opt/keycloak/data/import/realm.json:r restart: unless-stopped - + #### API #### studiomanager.api: @@ -66,8 +66,8 @@ context: . dockerfile: StudioManager.API/Dockerfile ports: - - 5000:5000 - - 5001:5001 + - 5000:5000 + - 5001:5001 environment: - ASPNETCORE_URLS=http://+:5001 restart: unless-stopped