diff --git a/web/backend/API/API.Core/API.Core.csproj b/web/backend/API/API.Core/API.Core.csproj index 35541dc..fa52cc5 100644 --- a/web/backend/API/API.Core/API.Core.csproj +++ b/web/backend/API/API.Core/API.Core.csproj @@ -30,10 +30,11 @@ + - + diff --git a/web/backend/API/API.Core/Controllers/BreweryController.cs b/web/backend/API/API.Core/Controllers/BreweryController.cs deleted file mode 100644 index d95074f..0000000 --- a/web/backend/API/API.Core/Controllers/BreweryController.cs +++ /dev/null @@ -1,183 +0,0 @@ -using API.Core.Contracts.Breweries; -using Shared.Contracts; -using Domain.Entities; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Service.Breweries; - -namespace API.Core.Controllers; - -/// -/// Provides CRUD endpoints for managing brewery posts. -/// -/// -/// The controller is decorated with [Authorize(AuthenticationSchemes = "JWT")] by default; read endpoints -/// ( and ) opt out via [AllowAnonymous]. -/// -/// Service used to query and mutate brewery post data. -[ApiController] -[Route("api/[controller]")] -[Authorize(AuthenticationSchemes = "JWT")] -public class BreweryController(IBreweryService breweryService) : ControllerBase -{ - /// - /// Retrieves a single brewery post by its unique identifier. - /// - /// Allows anonymous access. - /// The unique identifier of the brewery post to retrieve. - /// - /// A 200 OK result wrapping a of if found; - /// otherwise a 404 Not Found result wrapping a error message. - /// - [AllowAnonymous] - [HttpGet("{id:guid}")] - public async Task>> GetById(Guid id) - { - var brewery = await breweryService.GetByIdAsync(id); - if (brewery is null) - return NotFound(new ResponseBody { Message = $"Brewery with ID {id} not found." }); - - return Ok(new ResponseBody - { - Message = "Brewery retrieved successfully.", - Payload = MapToDto(brewery), - }); - } - - /// - /// Retrieves a paginated list of brewery posts. - /// - /// Allows anonymous access. - /// The maximum number of brewery posts to return, or null for no limit. - /// The number of brewery posts to skip before returning results, or null for no offset. - /// A 200 OK result wrapping a of a collection of . - [AllowAnonymous] - [HttpGet] - public async Task>>> GetAll( - [FromQuery] int? limit, - [FromQuery] int? offset) - { - var breweries = await breweryService.GetAllAsync(limit, offset); - return Ok(new ResponseBody> - { - Message = "Breweries retrieved successfully.", - Payload = breweries.Select(MapToDto), - }); - } - - /// - /// Creates a new brewery post. - /// - /// The brewery details to create, including the posting user, name, description, and location. - /// - /// A 201 Created result wrapping a of the newly created - /// if creation succeeds; otherwise a 400 Bad Request result wrapping a error message. - /// - [HttpPost] - public async Task>> Create([FromBody] BreweryCreateDto dto) - { - var request = new BreweryCreateRequest( - dto.PostedById, - dto.BreweryName, - dto.Description, - new BreweryLocationCreateRequest( - dto.Location.CityId, - dto.Location.AddressLine1, - dto.Location.AddressLine2, - dto.Location.PostalCode, - dto.Location.Coordinates - ) - ); - - var result = await breweryService.CreateAsync(request); - if (!result.Success) - return BadRequest(new ResponseBody { Message = result.Message }); - - return Created($"/api/brewery/{result.Brewery.BreweryPostId}", new ResponseBody - { - Message = "Brewery created successfully.", - Payload = MapToDto(result.Brewery), - }); - } - - /// - /// Updates an existing brewery post. - /// - /// The unique identifier of the brewery post from the route, which must match 's ID. - /// The updated brewery details, including the posting user, name, description, and optional location. - /// - /// A 200 OK result wrapping a of the updated brewery (as a ) - /// if the update succeeds; otherwise a 400 Bad Request result wrapping a error message - /// (returned when the route ID does not match the payload ID, or when the update itself fails). - /// - [HttpPut("{id:guid}")] - public async Task>> Update(Guid id, [FromBody] BreweryDto dto) - { - if (dto.BreweryPostId != id) - return BadRequest(new ResponseBody { Message = "Route ID does not match payload ID." }); - - var request = new BreweryUpdateRequest( - dto.BreweryPostId, - dto.PostedById, - dto.BreweryName, - dto.Description, - dto.Location is null ? null : new BreweryLocationUpdateRequest( - dto.Location.BreweryPostLocationId, - dto.Location.CityId, - dto.Location.AddressLine1, - dto.Location.AddressLine2, - dto.Location.PostalCode, - dto.Location.Coordinates - ) - ); - - var result = await breweryService.UpdateAsync(request); - if (!result.Success) - return BadRequest(new ResponseBody { Message = result.Message }); - - return Ok(new ResponseBody - { - Message = "Brewery updated successfully.", - Payload = result.Brewery, - }); - } - - /// - /// Deletes a brewery post. - /// - /// The unique identifier of the brewery post to delete. - /// A 200 OK result wrapping a confirming the deletion. - [HttpDelete("{id:guid}")] - public async Task> Delete(Guid id) - { - await breweryService.DeleteAsync(id); - return Ok(new ResponseBody { Message = "Brewery deleted successfully." }); - } - - /// - /// Maps a domain entity to its representation, - /// including its location, if present. - /// - /// The brewery post entity to map. - /// The mapped . - private static BreweryDto MapToDto(BreweryPost b) => new() - { - BreweryPostId = b.BreweryPostId, - PostedById = b.PostedById, - BreweryName = b.BreweryName, - Description = b.Description, - CreatedAt = b.CreatedAt, - UpdatedAt = b.UpdatedAt, - Timer = b.Timer, - Location = b.Location is null ? null : new BreweryLocationDto - { - BreweryPostLocationId = b.Location.BreweryPostLocationId, - BreweryPostId = b.Location.BreweryPostId, - CityId = b.Location.CityId, - AddressLine1 = b.Location.AddressLine1, - AddressLine2 = b.Location.AddressLine2, - PostalCode = b.Location.PostalCode, - Coordinates = b.Location.Coordinates, - }, - }; -} diff --git a/web/backend/API/API.Core/Dockerfile b/web/backend/API/API.Core/Dockerfile index 03dd219..5926b1d 100644 --- a/web/backend/API/API.Core/Dockerfile +++ b/web/backend/API/API.Core/Dockerfile @@ -8,16 +8,35 @@ EXPOSE 8081 FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src + +# Copy the solution file and every project file first (preserving their real relative paths, since +# ProjectReference paths are resolved relative to the referencing .csproj's location), so the +# `dotnet restore` layer stays cached as long as no project file or package reference changes, +# independent of source-only edits. Restoring against Core.slnx avoids hand-maintaining a per-project +# dependency list here that silently drifts out of sync as ProjectReferences change. +COPY ["Core.slnx", "./"] COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"] -COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"] -COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"] -COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"] +COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"] +COPY ["Database/Database.Migrations/Database.Migrations.csproj", "Database/Database.Migrations/"] +COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"] +COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"] +COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain/Domain.Exceptions/"] +COPY ["Features/Features.Breweries/Features.Breweries.csproj", "Features/Features.Breweries/"] +COPY ["Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj", "Features/Features.Breweries.Tests/"] +COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"] +COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"] COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"] COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"] -COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"] +COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"] +COPY ["Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository.Tests/"] +COPY ["Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj", "Infrastructure/Infrastructure.Sql/"] COPY ["Service/Service.Auth/Service.Auth.csproj", "Service/Service.Auth/"] +COPY ["Service/Service.Auth.Tests/Service.Auth.Tests.csproj", "Service/Service.Auth.Tests/"] +COPY ["Service/Service.Emails/Service.Emails.csproj", "Service/Service.Emails/"] COPY ["Service/Service.UserManagement/Service.UserManagement.csproj", "Service/Service.UserManagement/"] -RUN dotnet restore "API/API.Core/API.Core.csproj" +COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"] +COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"] +RUN dotnet restore "Core.slnx" COPY . . WORKDIR "/src/API/API.Core" RUN dotnet build "./API.Core.csproj" -c $BUILD_CONFIGURATION -o /app/build diff --git a/web/backend/API/API.Core/Program.cs b/web/backend/API/API.Core/Program.cs index 0106454..811a9f3 100644 --- a/web/backend/API/API.Core/Program.cs +++ b/web/backend/API/API.Core/Program.cs @@ -1,5 +1,7 @@ using API.Core; using API.Core.Authentication; +using Features.Breweries.Controllers; +using Features.Breweries.DependencyInjection; using FluentValidation; using FluentValidation.AspNetCore; using Infrastructure.Email; @@ -7,11 +9,9 @@ using Infrastructure.Email.Templates.Rendering; using Infrastructure.Jwt; using Infrastructure.PasswordHashing; using Infrastructure.Repository.Auth; -using Infrastructure.Repository.Sql; using Infrastructure.Repository.UserAccount; -using Infrastructure.Repository.Breweries; +using Infrastructure.Sql; using Service.Auth; -using Service.Breweries; using Service.Emails; using Service.UserManagement.User; using Shared.Application.Behaviors; @@ -22,7 +22,8 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { options.Filters.Add(); -}); +}) + .AddApplicationPart(typeof(BreweryController).Assembly); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -30,6 +31,7 @@ builder.Services.AddOpenApi(); // Add FluentValidation builder.Services.AddValidatorsFromAssemblyContaining(); +builder.Services.AddValidatorsFromAssembly(typeof(BreweryController).Assembly); builder.Services.AddFluentValidationAutoValidation(); // Add MediatR. Each Features.* slice's assembly is registered here as it's introduced; @@ -37,6 +39,7 @@ builder.Services.AddFluentValidationAutoValidation(); builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblyContaining(); + cfg.RegisterServicesFromAssembly(typeof(BreweryController).Assembly); cfg.AddOpenBehavior(typeof(ValidationBehavior<,>)); }); @@ -60,8 +63,7 @@ builder.Services.AddSingleton< builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddFeaturesBreweries(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/web/backend/Core.slnx b/web/backend/Core.slnx index 860f7f7..7318055 100644 --- a/web/backend/Core.slnx +++ b/web/backend/Core.slnx @@ -21,13 +21,17 @@ + + + + + - diff --git a/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs new file mode 100644 index 0000000..a632d11 --- /dev/null +++ b/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs @@ -0,0 +1,68 @@ +using Domain.Entities; +using FluentAssertions; +using Features.Breweries.Commands.CreateBrewery; +using Features.Breweries.Repository; +using Moq; + +namespace Features.Breweries.Tests.Commands; + +public class CreateBreweryHandlerTests +{ + private readonly Mock _repoMock = new(); + private readonly CreateBreweryHandler _handler; + + public CreateBreweryHandlerTests() + { + _handler = new CreateBreweryHandler(_repoMock.Object); + } + + private static CreateBreweryLocation ValidLocation() => + new( + CityId: Guid.NewGuid(), + AddressLine1: "123 Main St", + AddressLine2: null, + PostalCode: "12345", + Coordinates: null + ); + + [Fact] + public async Task Handle_PersistsEntity_WithNewIdsAndCreatedAt() + { + var command = new CreateBreweryCommand( + PostedById: Guid.NewGuid(), + BreweryName: "MyBrew", + Description: "Desc", + Location: ValidLocation() + ); + + BreweryPost? persisted = null; + _repoMock + .Setup(r => r.CreateAsync(It.IsAny())) + .Callback(b => persisted = b) + .Returns(Task.CompletedTask); + + var before = DateTime.UtcNow; + var result = await _handler.Handle(command, CancellationToken.None); + var after = DateTime.UtcNow; + + persisted.Should().NotBeNull(); + persisted!.BreweryPostId.Should().NotBe(Guid.Empty); + persisted.PostedById.Should().Be(command.PostedById); + persisted.BreweryName.Should().Be("MyBrew"); + persisted.Description.Should().Be("Desc"); + persisted.CreatedAt.Should().BeOnOrAfter(before).And.BeOnOrBefore(after); + persisted.UpdatedAt.Should().BeNull(); + + persisted.Location.Should().NotBeNull(); + persisted.Location!.BreweryPostLocationId.Should().NotBe(Guid.Empty); + persisted.Location.BreweryPostLocationId.Should().NotBe(persisted.BreweryPostId); + persisted.Location.CityId.Should().Be(command.Location.CityId); + persisted.Location.AddressLine1.Should().Be(command.Location.AddressLine1); + persisted.Location.PostalCode.Should().Be(command.Location.PostalCode); + + _repoMock.Verify(r => r.CreateAsync(It.IsAny()), Times.Once); + + result.BreweryName.Should().Be("MyBrew"); + result.Location.Should().NotBeNull(); + } +} diff --git a/web/backend/Features/Features.Breweries.Tests/Commands/DeleteBreweryHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Commands/DeleteBreweryHandlerTests.cs new file mode 100644 index 0000000..1c7106c --- /dev/null +++ b/web/backend/Features/Features.Breweries.Tests/Commands/DeleteBreweryHandlerTests.cs @@ -0,0 +1,21 @@ +using Features.Breweries.Commands.DeleteBrewery; +using Features.Breweries.Repository; +using Moq; + +namespace Features.Breweries.Tests.Commands; + +public class DeleteBreweryHandlerTests +{ + [Fact] + public async Task Handle_DelegatesToRepository() + { + var repoMock = new Mock(); + var handler = new DeleteBreweryHandler(repoMock.Object); + var id = Guid.NewGuid(); + repoMock.Setup(r => r.DeleteAsync(id)).Returns(Task.CompletedTask); + + await handler.Handle(new DeleteBreweryCommand(id), CancellationToken.None); + + repoMock.Verify(r => r.DeleteAsync(id), Times.Once); + } +} diff --git a/web/backend/Features/Features.Breweries.Tests/Commands/UpdateBreweryHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Commands/UpdateBreweryHandlerTests.cs new file mode 100644 index 0000000..797c1a3 --- /dev/null +++ b/web/backend/Features/Features.Breweries.Tests/Commands/UpdateBreweryHandlerTests.cs @@ -0,0 +1,106 @@ +using Domain.Entities; +using FluentAssertions; +using Features.Breweries.Commands.UpdateBrewery; +using Features.Breweries.Repository; +using Moq; + +namespace Features.Breweries.Tests.Commands; + +public class UpdateBreweryHandlerTests +{ + private readonly Mock _repoMock = new(); + private readonly UpdateBreweryHandler _handler; + + public UpdateBreweryHandlerTests() + { + _handler = new UpdateBreweryHandler(_repoMock.Object); + } + + [Fact] + public async Task Handle_UpdatesNameDescription_AndSetsUpdatedAt() + { + var command = new UpdateBreweryCommand( + BreweryPostId: Guid.NewGuid(), + PostedById: Guid.NewGuid(), + BreweryName: "Renamed", + Description: "New description", + Location: null + ); + + BreweryPost? persisted = null; + _repoMock + .Setup(r => r.UpdateAsync(It.IsAny())) + .Callback(b => persisted = b) + .Returns(Task.CompletedTask); + + var before = DateTime.UtcNow; + await _handler.Handle(command, CancellationToken.None); + var after = DateTime.UtcNow; + + persisted.Should().NotBeNull(); + persisted!.BreweryPostId.Should().Be(command.BreweryPostId); + persisted.PostedById.Should().Be(command.PostedById); + persisted.BreweryName.Should().Be("Renamed"); + persisted.Description.Should().Be("New description"); + persisted.UpdatedAt.Should().NotBeNull(); + persisted.UpdatedAt!.Value.Should().BeOnOrAfter(before).And.BeOnOrBefore(after); + } + + [Fact] + public async Task Handle_ClearsLocation_WhenCommandLocationIsNull() + { + var command = new UpdateBreweryCommand( + BreweryPostId: Guid.NewGuid(), + PostedById: Guid.NewGuid(), + BreweryName: "Name", + Description: "Description", + Location: null + ); + + BreweryPost? persisted = null; + _repoMock + .Setup(r => r.UpdateAsync(It.IsAny())) + .Callback(b => persisted = b) + .Returns(Task.CompletedTask); + + await _handler.Handle(command, CancellationToken.None); + + persisted!.Location.Should().BeNull(); + } + + [Fact] + public async Task Handle_SetsLocation_WhenCommandLocationProvided() + { + var locationCommand = new UpdateBreweryLocation( + BreweryPostLocationId: Guid.NewGuid(), + CityId: Guid.NewGuid(), + AddressLine1: "456 Oak Ave", + AddressLine2: "Suite 2", + PostalCode: "54321", + Coordinates: null + ); + var command = new UpdateBreweryCommand( + BreweryPostId: Guid.NewGuid(), + PostedById: Guid.NewGuid(), + BreweryName: "Name", + Description: "Description", + Location: locationCommand + ); + + BreweryPost? persisted = null; + _repoMock + .Setup(r => r.UpdateAsync(It.IsAny())) + .Callback(b => persisted = b) + .Returns(Task.CompletedTask); + + await _handler.Handle(command, CancellationToken.None); + + persisted!.Location.Should().NotBeNull(); + persisted.Location!.BreweryPostLocationId.Should().Be(locationCommand.BreweryPostLocationId); + persisted.Location.BreweryPostId.Should().Be(command.BreweryPostId); + persisted.Location.CityId.Should().Be(locationCommand.CityId); + persisted.Location.AddressLine1.Should().Be(locationCommand.AddressLine1); + persisted.Location.AddressLine2.Should().Be(locationCommand.AddressLine2); + persisted.Location.PostalCode.Should().Be(locationCommand.PostalCode); + } +} diff --git a/web/backend/Service/Service.Breweries.Tests/Service.Breweries.Tests.csproj b/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj similarity index 63% rename from web/backend/Service/Service.Breweries.Tests/Service.Breweries.Tests.csproj rename to web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj index 5803530..debe2e0 100644 --- a/web/backend/Service/Service.Breweries.Tests/Service.Breweries.Tests.csproj +++ b/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj @@ -4,7 +4,7 @@ enable enable false - Service.Breweries.Tests + Features.Breweries.Tests @@ -13,6 +13,7 @@ + @@ -20,10 +21,6 @@ - - - - + diff --git a/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweriesHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweriesHandlerTests.cs new file mode 100644 index 0000000..9cd96fc --- /dev/null +++ b/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweriesHandlerTests.cs @@ -0,0 +1,46 @@ +using Domain.Entities; +using FluentAssertions; +using Features.Breweries.Queries.GetAllBreweries; +using Features.Breweries.Repository; +using Moq; + +namespace Features.Breweries.Tests.Queries; + +public class GetAllBreweriesHandlerTests +{ + private readonly Mock _repoMock = new(); + private readonly GetAllBreweriesHandler _handler; + + public GetAllBreweriesHandlerTests() + { + _handler = new GetAllBreweriesHandler(_repoMock.Object); + } + + [Fact] + public async Task Handle_PassesLimitAndOffset_ToRepository() + { + _repoMock.Setup(r => r.GetAllAsync(10, 5)) + .ReturnsAsync(Array.Empty()); + + var result = await _handler.Handle(new GetAllBreweriesQuery(10, 5), CancellationToken.None); + + result.Should().BeEmpty(); + _repoMock.Verify(r => r.GetAllAsync(10, 5), Times.Once); + } + + [Fact] + public async Task Handle_ReturnsAllBreweries_FromRepository() + { + var breweries = new[] + { + new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "A" }, + new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "B" }, + }; + _repoMock.Setup(r => r.GetAllAsync(null, null)) + .ReturnsAsync(breweries); + + var result = await _handler.Handle(new GetAllBreweriesQuery(null, null), CancellationToken.None); + + result.Select(b => b.BreweryPostId).Should().BeEquivalentTo(breweries.Select(b => b.BreweryPostId)); + } +} diff --git a/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryByIdHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryByIdHandlerTests.cs new file mode 100644 index 0000000..0c98295 --- /dev/null +++ b/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryByIdHandlerTests.cs @@ -0,0 +1,43 @@ +using Domain.Entities; +using FluentAssertions; +using Features.Breweries.Queries.GetBreweryById; +using Features.Breweries.Repository; +using Moq; + +namespace Features.Breweries.Tests.Queries; + +public class GetBreweryByIdHandlerTests +{ + private readonly Mock _repoMock = new(); + private readonly GetBreweryByIdHandler _handler; + + public GetBreweryByIdHandlerTests() + { + _handler = new GetBreweryByIdHandler(_repoMock.Object); + } + + [Fact] + public async Task Handle_ReturnsBrewery_WhenFound() + { + var brewery = new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "Test" }; + _repoMock.Setup(r => r.GetByIdAsync(brewery.BreweryPostId)) + .ReturnsAsync(brewery); + + var result = await _handler.Handle(new GetBreweryByIdQuery(brewery.BreweryPostId), CancellationToken.None); + + result.Should().NotBeNull(); + result!.BreweryPostId.Should().Be(brewery.BreweryPostId); + } + + [Fact] + public async Task Handle_ReturnsNull_WhenNotFound() + { + var id = Guid.NewGuid(); + _repoMock.Setup(r => r.GetByIdAsync(id)) + .ReturnsAsync((BreweryPost?)null); + + var result = await _handler.Handle(new GetBreweryByIdQuery(id), CancellationToken.None); + + result.Should().BeNull(); + } +} diff --git a/web/backend/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs b/web/backend/Features/Features.Breweries.Tests/Repository/BreweryRepositoryTests.cs similarity index 95% rename from web/backend/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs rename to web/backend/Features/Features.Breweries.Tests/Repository/BreweryRepositoryTests.cs index 6352124..aefb999 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs +++ b/web/backend/Features/Features.Breweries.Tests/Repository/BreweryRepositoryTests.cs @@ -1,12 +1,11 @@ using Apps72.Dev.Data.DbMocker; using FluentAssertions; -using Infrastructure.Repository.Breweries; -using Infrastructure.Repository.Tests.Database; +using Features.Breweries.Repository; using Domain.Entities; -namespace Infrastructure.Repository.Tests.Breweries; +namespace Features.Breweries.Tests.Repository; -public class BreweryRepositoryTest +public class BreweryRepositoryTests { private static BreweryRepository CreateRepo(MockDbConnection conn) => new(new TestConnectionFactory(conn)); @@ -100,7 +99,7 @@ public class BreweryRepositoryTest Coordinates = [0x00, 0x01] } }; - + // Should not throw var act = async () => await repo.CreateAsync(brewery); await act.Should().NotThrowAsync(); diff --git a/web/backend/Features/Features.Breweries.Tests/Repository/TestConnectionFactory.cs b/web/backend/Features/Features.Breweries.Tests/Repository/TestConnectionFactory.cs new file mode 100644 index 0000000..69c6c1a --- /dev/null +++ b/web/backend/Features/Features.Breweries.Tests/Repository/TestConnectionFactory.cs @@ -0,0 +1,11 @@ +using System.Data.Common; +using Infrastructure.Sql; + +namespace Features.Breweries.Tests.Repository; + +internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory +{ + private readonly DbConnection _conn = conn; + + public DbConnection CreateConnection() => _conn; +} diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs new file mode 100644 index 0000000..9a61928 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs @@ -0,0 +1,25 @@ +using Features.Breweries.Dtos; +using MediatR; + +namespace Features.Breweries.Commands.CreateBrewery; + +/// +/// Location data required to create a new brewery post, supplied as part of . +/// +public record CreateBreweryLocation( + Guid CityId, + string AddressLine1, + string? AddressLine2, + string PostalCode, + byte[]? Coordinates +); + +/// +/// Creates a new brewery post. Bound directly from the request body of POST /api/brewery. +/// +public record CreateBreweryCommand( + Guid PostedById, + string BreweryName, + string Description, + CreateBreweryLocation Location +) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs new file mode 100644 index 0000000..5eea5ec --- /dev/null +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs @@ -0,0 +1,44 @@ +using Domain.Entities; +using Features.Breweries.Dtos; +using Features.Breweries.Repository; +using MediatR; + +namespace Features.Breweries.Commands.CreateBrewery; + +/// +/// Handles by persisting a new brewery post and its location. +/// +/// Repository used to persist the new brewery post. +public class CreateBreweryHandler(IBreweryRepository repository) + : IRequestHandler +{ + /// + /// Creates a new brewery post, generating new identifiers for the post and its location. + /// + /// The details of the brewery post to create. + /// A token to observe for cancellation requests. + /// The newly created brewery post. + public async Task Handle(CreateBreweryCommand request, CancellationToken cancellationToken) + { + var entity = new BreweryPost + { + BreweryPostId = Guid.NewGuid(), + PostedById = request.PostedById, + BreweryName = request.BreweryName, + Description = request.Description, + CreatedAt = DateTime.UtcNow, + Location = new BreweryPostLocation + { + BreweryPostLocationId = Guid.NewGuid(), + CityId = request.Location.CityId, + AddressLine1 = request.Location.AddressLine1, + AddressLine2 = request.Location.AddressLine2, + PostalCode = request.Location.PostalCode, + Coordinates = request.Location.Coordinates, + }, + }; + + await repository.CreateAsync(entity); + return entity.ToDto(); + } +} diff --git a/web/backend/API/API.Core/Contracts/Breweries/BreweryCreateRequestValidator.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs similarity index 74% rename from web/backend/API/API.Core/Contracts/Breweries/BreweryCreateRequestValidator.cs rename to web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs index fa8d26f..bb1c2e6 100644 --- a/web/backend/API/API.Core/Contracts/Breweries/BreweryCreateRequestValidator.cs +++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs @@ -1,19 +1,19 @@ using FluentValidation; -namespace API.Core.Contracts.Breweries; +namespace Features.Breweries.Commands.CreateBrewery; /// -/// Validates instances before they are processed by the brewery creation endpoint. +/// Validates instances before they are processed. /// -public class BreweryCreateDtoValidator : AbstractValidator +public class CreateBreweryValidator : AbstractValidator { /// - /// Configures validation rules requiring , - /// , , and - /// to be present, with length limits on the name, description, + /// Configures validation rules requiring , + /// , , and + /// to be present, with length limits on the name, description, /// address line 1, and postal code fields. /// - public BreweryCreateDtoValidator() + public CreateBreweryValidator() { RuleFor(x => x.PostedById) .NotEmpty() diff --git a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs new file mode 100644 index 0000000..360de7c --- /dev/null +++ b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Features.Breweries.Commands.DeleteBrewery; + +/// +/// Deletes a brewery post by its unique identifier. +/// +public record DeleteBreweryCommand(Guid BreweryPostId) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs new file mode 100644 index 0000000..44675f5 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs @@ -0,0 +1,15 @@ +using Features.Breweries.Repository; +using MediatR; + +namespace Features.Breweries.Commands.DeleteBrewery; + +/// +/// Handles by deleting the matching brewery post. +/// +/// Repository used to delete the brewery post. +public class DeleteBreweryHandler(IBreweryRepository repository) + : IRequestHandler +{ + public Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken) => + repository.DeleteAsync(request.BreweryPostId); +} diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs new file mode 100644 index 0000000..b4126b2 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs @@ -0,0 +1,28 @@ +using Features.Breweries.Dtos; +using MediatR; + +namespace Features.Breweries.Commands.UpdateBrewery; + +/// +/// Location data for an existing brewery post, supplied as part of . +/// +public record UpdateBreweryLocation( + Guid BreweryPostLocationId, + Guid CityId, + string AddressLine1, + string? AddressLine2, + string PostalCode, + byte[]? Coordinates +); + +/// +/// Updates an existing brewery post. Bound directly from the request body of PUT /api/brewery/{id}. +/// A null clears the brewery's location. +/// +public record UpdateBreweryCommand( + Guid BreweryPostId, + Guid PostedById, + string BreweryName, + string Description, + UpdateBreweryLocation? Location +) : IRequest; diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs new file mode 100644 index 0000000..8cac769 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs @@ -0,0 +1,46 @@ +using Domain.Entities; +using Features.Breweries.Dtos; +using Features.Breweries.Repository; +using MediatR; + +namespace Features.Breweries.Commands.UpdateBrewery; + +/// +/// Handles by persisting changes to an existing brewery post. +/// +/// Repository used to persist the updated brewery post. +public class UpdateBreweryHandler(IBreweryRepository repository) + : IRequestHandler +{ + /// + /// Updates an existing brewery post. If has no Location, + /// the brewery's location is cleared. + /// + /// The updated details of the brewery post. + /// A token to observe for cancellation requests. + /// The updated brewery post. + public async Task Handle(UpdateBreweryCommand request, CancellationToken cancellationToken) + { + var entity = new BreweryPost + { + BreweryPostId = request.BreweryPostId, + PostedById = request.PostedById, + BreweryName = request.BreweryName, + Description = request.Description, + UpdatedAt = DateTime.UtcNow, + Location = request.Location is null ? null : new BreweryPostLocation + { + BreweryPostLocationId = request.Location.BreweryPostLocationId, + BreweryPostId = request.BreweryPostId, + CityId = request.Location.CityId, + AddressLine1 = request.Location.AddressLine1, + AddressLine2 = request.Location.AddressLine2, + PostalCode = request.Location.PostalCode, + Coordinates = request.Location.Coordinates, + }, + }; + + await repository.UpdateAsync(entity); + return entity.ToDto(); + } +} diff --git a/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs b/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs new file mode 100644 index 0000000..db5ecdc --- /dev/null +++ b/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs @@ -0,0 +1,123 @@ +using Features.Breweries.Commands.CreateBrewery; +using Features.Breweries.Commands.DeleteBrewery; +using Features.Breweries.Commands.UpdateBrewery; +using Features.Breweries.Dtos; +using Features.Breweries.Queries.GetAllBreweries; +using Features.Breweries.Queries.GetBreweryById; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Shared.Contracts; + +namespace Features.Breweries.Controllers; + +/// +/// Provides CRUD endpoints for managing brewery posts. +/// +/// +/// The controller is decorated with [Authorize(AuthenticationSchemes = "JWT")] by default; read endpoints +/// ( and ) opt out via [AllowAnonymous]. +/// +/// Used to dispatch brewery commands and queries to their handlers. +[ApiController] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "JWT")] +public class BreweryController(IMediator mediator) : ControllerBase +{ + /// + /// Retrieves a single brewery post by its unique identifier. + /// + /// Allows anonymous access. + /// The unique identifier of the brewery post to retrieve. + /// + /// A 200 OK result wrapping a of if found; + /// otherwise a 404 Not Found result wrapping a error message. + /// + [AllowAnonymous] + [HttpGet("{id:guid}")] + public async Task>> GetById(Guid id) + { + var brewery = await mediator.Send(new GetBreweryByIdQuery(id)); + if (brewery is null) + return NotFound(new ResponseBody { Message = $"Brewery with ID {id} not found." }); + + return Ok(new ResponseBody + { + Message = "Brewery retrieved successfully.", + Payload = brewery, + }); + } + + /// + /// Retrieves a paginated list of brewery posts. + /// + /// Allows anonymous access. + /// The maximum number of brewery posts to return, or null for no limit. + /// The number of brewery posts to skip before returning results, or null for no offset. + /// A 200 OK result wrapping a of a collection of . + [AllowAnonymous] + [HttpGet] + public async Task>>> GetAll( + [FromQuery] int? limit, + [FromQuery] int? offset) + { + var breweries = await mediator.Send(new GetAllBreweriesQuery(limit, offset)); + return Ok(new ResponseBody> + { + Message = "Breweries retrieved successfully.", + Payload = breweries, + }); + } + + /// + /// Creates a new brewery post. + /// + /// The brewery details to create, including the posting user, name, description, and location. + /// A 201 Created result wrapping a of the newly created . + [HttpPost] + public async Task>> Create([FromBody] CreateBreweryCommand command) + { + var brewery = await mediator.Send(command); + return Created($"/api/brewery/{brewery.BreweryPostId}", new ResponseBody + { + Message = "Brewery created successfully.", + Payload = brewery, + }); + } + + /// + /// Updates an existing brewery post. + /// + /// The unique identifier of the brewery post from the route, which must match 's ID. + /// The updated brewery details, including the posting user, name, description, and optional location. + /// + /// A 200 OK result wrapping a of the updated if the + /// update succeeds; otherwise a 400 Bad Request result wrapping a error message + /// when the route ID does not match the payload ID. + /// + [HttpPut("{id:guid}")] + public async Task>> Update(Guid id, [FromBody] UpdateBreweryCommand command) + { + if (command.BreweryPostId != id) + return BadRequest(new ResponseBody { Message = "Route ID does not match payload ID." }); + + var brewery = await mediator.Send(command); + return Ok(new ResponseBody + { + Message = "Brewery updated successfully.", + Payload = brewery, + }); + } + + /// + /// Deletes a brewery post. + /// + /// The unique identifier of the brewery post to delete. + /// A 200 OK result wrapping a confirming the deletion. + [HttpDelete("{id:guid}")] + public async Task> Delete(Guid id) + { + await mediator.Send(new DeleteBreweryCommand(id)); + return Ok(new ResponseBody { Message = "Brewery deleted successfully." }); + } +} diff --git a/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs b/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs new file mode 100644 index 0000000..6fc01db --- /dev/null +++ b/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using Features.Breweries.Repository; +using Microsoft.Extensions.DependencyInjection; + +namespace Features.Breweries.DependencyInjection; + +/// +/// Registers the services owned by the Breweries feature slice. +/// +public static class FeaturesBreweriesServiceCollectionExtensions +{ + public static IServiceCollection AddFeaturesBreweries(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +} diff --git a/web/backend/API/API.Core/Contracts/Breweries/BreweryDto.cs b/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs similarity index 56% rename from web/backend/API/API.Core/Contracts/Breweries/BreweryDto.cs rename to web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs index eda26e3..c05c48d 100644 --- a/web/backend/API/API.Core/Contracts/Breweries/BreweryDto.cs +++ b/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs @@ -1,36 +1,4 @@ -namespace API.Core.Contracts.Breweries; - -/// -/// Request payload describing the location of a brewery to be created, supplied as part of -/// . -/// -public class BreweryLocationCreateDto -{ - /// - /// The unique identifier of the city in which the brewery is located. - /// - public Guid CityId { get; set; } - - /// - /// The primary street address line of the brewery. - /// - public string AddressLine1 { get; set; } = string.Empty; - - /// - /// An optional secondary address line (e.g. suite or unit number). - /// - public string? AddressLine2 { get; set; } - - /// - /// The postal/ZIP code of the brewery's address. - /// - public string PostalCode { get; set; } = string.Empty; - - /// - /// The optional geographic coordinates of the brewery, in a raw binary representation. - /// - public byte[]? Coordinates { get; set; } -} +namespace Features.Breweries.Dtos; /// /// Represents the location details of an existing brewery, as returned in . @@ -74,34 +42,8 @@ public class BreweryLocationDto } /// -/// Request body used by to create a new brewery post. -/// -public class BreweryCreateDto -{ - /// - /// The unique identifier of the user account that is creating the brewery post. - /// - public Guid PostedById { get; set; } - - /// - /// The name of the brewery; required and limited to 256 characters. - /// - public string BreweryName { get; set; } = string.Empty; - - /// - /// A description of the brewery; required and limited to 512 characters. - /// - public string Description { get; set; } = string.Empty; - - /// - /// The location details of the brewery being created. - /// - public BreweryLocationCreateDto Location { get; set; } = null!; -} - -/// -/// Represents a brewery post as returned by, or submitted to, the brewery endpoints, including its -/// metadata and optional location. +/// Represents a brewery post as returned by the brewery endpoints, including its metadata and +/// optional location. /// public class BreweryDto { diff --git a/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs b/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs new file mode 100644 index 0000000..90db7a3 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs @@ -0,0 +1,36 @@ +using Domain.Entities; + +namespace Features.Breweries.Dtos; + +/// +/// Maps domain entities to their wire representation. +/// +public static class BreweryDtoMapper +{ + /// + /// Maps a domain entity to its representation, + /// including its location, if present. + /// + /// The brewery post entity to map. + /// The mapped . + public static BreweryDto ToDto(this BreweryPost brewery) => new() + { + BreweryPostId = brewery.BreweryPostId, + PostedById = brewery.PostedById, + BreweryName = brewery.BreweryName, + Description = brewery.Description, + CreatedAt = brewery.CreatedAt, + UpdatedAt = brewery.UpdatedAt, + Timer = brewery.Timer, + Location = brewery.Location is null ? null : new BreweryLocationDto + { + BreweryPostLocationId = brewery.Location.BreweryPostLocationId, + BreweryPostId = brewery.Location.BreweryPostId, + CityId = brewery.Location.CityId, + AddressLine1 = brewery.Location.AddressLine1, + AddressLine2 = brewery.Location.AddressLine2, + PostalCode = brewery.Location.PostalCode, + Coordinates = brewery.Location.Coordinates, + }, + }; +} diff --git a/web/backend/Features/Features.Breweries/Features.Breweries.csproj b/web/backend/Features/Features.Breweries/Features.Breweries.csproj new file mode 100644 index 0000000..0c5faed --- /dev/null +++ b/web/backend/Features/Features.Breweries/Features.Breweries.csproj @@ -0,0 +1,19 @@ + + + net10.0 + enable + enable + Features.Breweries + + + + + + + + + + + + + diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs new file mode 100644 index 0000000..e758625 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs @@ -0,0 +1,19 @@ +using Features.Breweries.Dtos; +using Features.Breweries.Repository; +using MediatR; + +namespace Features.Breweries.Queries.GetAllBreweries; + +/// +/// Handles by retrieving a paginated list of brewery posts. +/// +/// Repository used to query brewery post data. +public class GetAllBreweriesHandler(IBreweryRepository repository) + : IRequestHandler> +{ + public async Task> Handle(GetAllBreweriesQuery request, CancellationToken cancellationToken) + { + var breweries = await repository.GetAllAsync(request.Limit, request.Offset); + return breweries.Select(b => b.ToDto()); + } +} diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs new file mode 100644 index 0000000..d8caea2 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs @@ -0,0 +1,9 @@ +using Features.Breweries.Dtos; +using MediatR; + +namespace Features.Breweries.Queries.GetAllBreweries; + +/// +/// Retrieves a paginated list of brewery posts. +/// +public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest>; diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs new file mode 100644 index 0000000..efbd3c5 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs @@ -0,0 +1,19 @@ +using Features.Breweries.Dtos; +using Features.Breweries.Repository; +using MediatR; + +namespace Features.Breweries.Queries.GetBreweryById; + +/// +/// Handles by looking up the matching brewery post. +/// +/// Repository used to query brewery post data. +public class GetBreweryByIdHandler(IBreweryRepository repository) + : IRequestHandler +{ + public async Task Handle(GetBreweryByIdQuery request, CancellationToken cancellationToken) + { + var brewery = await repository.GetByIdAsync(request.BreweryPostId); + return brewery?.ToDto(); + } +} diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs new file mode 100644 index 0000000..c753991 --- /dev/null +++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs @@ -0,0 +1,9 @@ +using Features.Breweries.Dtos; +using MediatR; + +namespace Features.Breweries.Queries.GetBreweryById; + +/// +/// Retrieves a single brewery post by its unique identifier. +/// +public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest; diff --git a/web/backend/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs b/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs similarity index 98% rename from web/backend/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs rename to web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs index a974e86..e8753e3 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs +++ b/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs @@ -1,8 +1,8 @@ using System.Data.Common; using Domain.Entities; -using Infrastructure.Repository.Sql; +using Infrastructure.Sql; -namespace Infrastructure.Repository.Breweries; +namespace Features.Breweries.Repository; /// /// ADO.NET-based implementation of backed by SQL Server stored @@ -12,8 +12,6 @@ namespace Infrastructure.Repository.Breweries; public class BreweryRepository(ISqlConnectionFactory connectionFactory) : Repository(connectionFactory), IBreweryRepository { - private readonly ISqlConnectionFactory _connectionFactory = connectionFactory; - /// /// Retrieves a brewery post by ID using the USP_GetBreweryById stored procedure. /// diff --git a/web/backend/Infrastructure/Infrastructure.Repository/Breweries/IBreweryRepository.cs b/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs similarity index 97% rename from web/backend/Infrastructure/Infrastructure.Repository/Breweries/IBreweryRepository.cs rename to web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs index 9598bfe..8563190 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/Breweries/IBreweryRepository.cs +++ b/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs @@ -1,6 +1,6 @@ using Domain.Entities; -namespace Infrastructure.Repository.Breweries; +namespace Features.Breweries.Repository; /// /// Repository for CRUD operations on brewery post records. @@ -40,4 +40,4 @@ public interface IBreweryRepository /// The brewery post to create. Must have a non-null Location. /// Thrown when has no Location. Task CreateAsync(BreweryPost brewery); -} \ No newline at end of file +} diff --git a/web/backend/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs b/web/backend/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs index c25f0e1..d43dcda 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs +++ b/web/backend/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs @@ -1,5 +1,5 @@ using System.Data.Common; -using Infrastructure.Repository.Sql; +using Infrastructure.Sql; namespace Infrastructure.Repository.Tests.Database; diff --git a/web/backend/Infrastructure/Infrastructure.Repository.Tests/Dockerfile b/web/backend/Infrastructure/Infrastructure.Repository.Tests/Dockerfile index a995f50..435afb5 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository.Tests/Dockerfile +++ b/web/backend/Infrastructure/Infrastructure.Repository.Tests/Dockerfile @@ -1,9 +1,10 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"] -COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"] +COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"] +COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain/Domain.Exceptions/"] COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"] +COPY ["Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj", "Infrastructure/Infrastructure.Sql/"] COPY ["Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository.Tests/"] RUN dotnet restore "Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj" COPY . . diff --git a/web/backend/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj b/web/backend/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj index 55a49a2..ad54e15 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj +++ b/web/backend/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj @@ -22,5 +22,6 @@ + diff --git a/web/backend/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs b/web/backend/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs index a993162..92ce775 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs +++ b/web/backend/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs @@ -1,7 +1,7 @@ using System.Data; using System.Data.Common; using Domain.Entities; -using Infrastructure.Repository.Sql; +using Infrastructure.Sql; using Microsoft.Data.SqlClient; namespace Infrastructure.Repository.Auth; diff --git a/web/backend/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj b/web/backend/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj index 3b418d7..df21703 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj +++ b/web/backend/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj @@ -19,5 +19,6 @@ + diff --git a/web/backend/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs b/web/backend/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs index 0bb76cc..943042d 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs +++ b/web/backend/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs @@ -1,6 +1,6 @@ using System.Data; using System.Data.Common; -using Infrastructure.Repository.Sql; +using Infrastructure.Sql; namespace Infrastructure.Repository.UserAccount; diff --git a/web/backend/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs b/web/backend/Infrastructure/Infrastructure.Sql/DefaultSqlConnectionFactory.cs similarity index 98% rename from web/backend/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs rename to web/backend/Infrastructure/Infrastructure.Sql/DefaultSqlConnectionFactory.cs index f802e50..503ab8a 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/DefaultSqlConnectionFactory.cs @@ -2,7 +2,7 @@ using System.Data.Common; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; -namespace Infrastructure.Repository.Sql; +namespace Infrastructure.Sql; /// /// Default implementation that creates SQL Server connections, diff --git a/web/backend/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs b/web/backend/Infrastructure/Infrastructure.Sql/ISqlConnectionFactory.cs similarity index 89% rename from web/backend/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs rename to web/backend/Infrastructure/Infrastructure.Sql/ISqlConnectionFactory.cs index 1799305..fc2d112 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/ISqlConnectionFactory.cs @@ -1,6 +1,6 @@ using System.Data.Common; -namespace Infrastructure.Repository.Sql; +namespace Infrastructure.Sql; /// /// Factory for creating database connections used by repositories. diff --git a/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj b/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj new file mode 100644 index 0000000..78ce87b --- /dev/null +++ b/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj @@ -0,0 +1,20 @@ + + + net10.0 + enable + enable + Infrastructure.Sql + + + + + + + + diff --git a/web/backend/Infrastructure/Infrastructure.Repository/Repository.cs b/web/backend/Infrastructure/Infrastructure.Sql/Repository.cs similarity index 94% rename from web/backend/Infrastructure/Infrastructure.Repository/Repository.cs rename to web/backend/Infrastructure/Infrastructure.Sql/Repository.cs index e4eba31..872dcd1 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/Repository.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/Repository.cs @@ -1,7 +1,6 @@ using System.Data.Common; -using Infrastructure.Repository.Sql; -namespace Infrastructure.Repository; +namespace Infrastructure.Sql; /// /// Base class for ADO.NET-based repositories, providing shared connection creation and diff --git a/web/backend/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs b/web/backend/Infrastructure/Infrastructure.Sql/SqlConnectionStringHelper.cs similarity index 98% rename from web/backend/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs rename to web/backend/Infrastructure/Infrastructure.Sql/SqlConnectionStringHelper.cs index c8b304d..a38e401 100644 --- a/web/backend/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs +++ b/web/backend/Infrastructure/Infrastructure.Sql/SqlConnectionStringHelper.cs @@ -1,6 +1,6 @@ using Microsoft.Data.SqlClient; -namespace Infrastructure.Repository.Sql; +namespace Infrastructure.Sql; /// /// Helper for building SQL Server connection strings from environment variables. diff --git a/web/backend/Service/Service.Breweries.Tests/BreweryService.test.cs b/web/backend/Service/Service.Breweries.Tests/BreweryService.test.cs deleted file mode 100644 index 4b371db..0000000 --- a/web/backend/Service/Service.Breweries.Tests/BreweryService.test.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Domain.Entities; -using FluentAssertions; -using Infrastructure.Repository.Breweries; -using Moq; - -namespace Service.Breweries.Tests; - -public class BreweryServiceTests -{ - private readonly Mock _repoMock; - private readonly BreweryService _service; - - public BreweryServiceTests() - { - _repoMock = new Mock(); - _service = new BreweryService(_repoMock.Object); - } - - private static BreweryLocationCreateRequest ValidLocationCreateRequest() => - new( - CityId: Guid.NewGuid(), - AddressLine1: "123 Main St", - AddressLine2: null, - PostalCode: "12345", - Coordinates: null - ); - - [Fact] - public async Task GetByIdAsync_ReturnsBrewery_WhenFound() - { - var brewery = new BreweryPost { BreweryPostId = Guid.NewGuid() }; - _repoMock.Setup(r => r.GetByIdAsync(brewery.BreweryPostId)) - .ReturnsAsync(brewery); - - var result = await _service.GetByIdAsync(brewery.BreweryPostId); - - result.Should().Be(brewery); - } - - [Fact] - public async Task GetByIdAsync_ReturnsNull_WhenNotFound() - { - var id = Guid.NewGuid(); - _repoMock.Setup(r => r.GetByIdAsync(id)) - .ReturnsAsync((BreweryPost?)null); - - var result = await _service.GetByIdAsync(id); - - result.Should().BeNull(); - } - - [Fact] - public async Task GetAllAsync_PassesLimitAndOffset_ToRepository() - { - _repoMock.Setup(r => r.GetAllAsync(10, 5)) - .ReturnsAsync(Array.Empty()); - - var result = await _service.GetAllAsync(10, 5); - - result.Should().BeEmpty(); - _repoMock.Verify(r => r.GetAllAsync(10, 5), Times.Once); - } - - [Fact] - public async Task GetAllAsync_ReturnsAllBreweries_FromRepository() - { - var breweries = new[] - { - new BreweryPost { BreweryPostId = Guid.NewGuid() }, - new BreweryPost { BreweryPostId = Guid.NewGuid() }, - }; - _repoMock.Setup(r => r.GetAllAsync(null, null)) - .ReturnsAsync(breweries); - - var result = await _service.GetAllAsync(); - - result.Should().BeEquivalentTo(breweries); - } - - [Fact] - public async Task CreateAsync_PersistsEntity_WithNewIdsAndCreatedAt() - { - var request = new BreweryCreateRequest( - PostedById: Guid.NewGuid(), - BreweryName: "MyBrew", - Description: "Desc", - Location: ValidLocationCreateRequest() - ); - - BreweryPost? persisted = null; - _repoMock - .Setup(r => r.CreateAsync(It.IsAny())) - .Callback(b => persisted = b) - .Returns(Task.CompletedTask); - - var before = DateTime.UtcNow; - var result = await _service.CreateAsync(request); - var after = DateTime.UtcNow; - - result.Success.Should().BeTrue(); - persisted.Should().NotBeNull(); - persisted!.BreweryPostId.Should().NotBe(Guid.Empty); - persisted.PostedById.Should().Be(request.PostedById); - persisted.BreweryName.Should().Be("MyBrew"); - persisted.Description.Should().Be("Desc"); - persisted.CreatedAt.Should().BeOnOrAfter(before).And.BeOnOrBefore(after); - persisted.UpdatedAt.Should().BeNull(); - - persisted.Location.Should().NotBeNull(); - persisted.Location!.BreweryPostLocationId.Should().NotBe(Guid.Empty); - persisted.Location.BreweryPostLocationId.Should().NotBe(persisted.BreweryPostId); - persisted.Location.CityId.Should().Be(request.Location.CityId); - persisted.Location.AddressLine1.Should().Be(request.Location.AddressLine1); - persisted.Location.PostalCode.Should().Be(request.Location.PostalCode); - - _repoMock.Verify(r => r.CreateAsync(It.IsAny()), Times.Once); - } - - [Fact] - public async Task CreateAsync_ReturnsServiceReturn_WrappingTheCreatedEntity() - { - var request = new BreweryCreateRequest( - PostedById: Guid.NewGuid(), - BreweryName: "MyBrew", - Description: "Desc", - Location: ValidLocationCreateRequest() - ); - - _repoMock - .Setup(r => r.CreateAsync(It.IsAny())) - .Returns(Task.CompletedTask); - - var result = await _service.CreateAsync(request); - - result.Success.Should().BeTrue(); - result.Brewery.BreweryName.Should().Be("MyBrew"); - result.Brewery.Location.Should().NotBeNull(); - } - - [Fact] - public async Task UpdateAsync_UpdatesNameDescription_AndSetsUpdatedAt() - { - var request = new BreweryUpdateRequest( - BreweryPostId: Guid.NewGuid(), - PostedById: Guid.NewGuid(), - BreweryName: "Renamed", - Description: "New description", - Location: null - ); - - BreweryPost? persisted = null; - _repoMock - .Setup(r => r.UpdateAsync(It.IsAny())) - .Callback(b => persisted = b) - .Returns(Task.CompletedTask); - - var before = DateTime.UtcNow; - var result = await _service.UpdateAsync(request); - var after = DateTime.UtcNow; - - result.Success.Should().BeTrue(); - persisted.Should().NotBeNull(); - persisted!.BreweryPostId.Should().Be(request.BreweryPostId); - persisted.PostedById.Should().Be(request.PostedById); - persisted.BreweryName.Should().Be("Renamed"); - persisted.Description.Should().Be("New description"); - persisted.UpdatedAt.Should().NotBeNull(); - persisted.UpdatedAt!.Value.Should().BeOnOrAfter(before).And.BeOnOrBefore(after); - } - - [Fact] - public async Task UpdateAsync_ClearsLocation_WhenRequestLocationIsNull() - { - var request = new BreweryUpdateRequest( - BreweryPostId: Guid.NewGuid(), - PostedById: Guid.NewGuid(), - BreweryName: "Name", - Description: "Description", - Location: null - ); - - BreweryPost? persisted = null; - _repoMock - .Setup(r => r.UpdateAsync(It.IsAny())) - .Callback(b => persisted = b) - .Returns(Task.CompletedTask); - - await _service.UpdateAsync(request); - - persisted!.Location.Should().BeNull(); - } - - [Fact] - public async Task UpdateAsync_SetsLocation_WhenRequestLocationProvided() - { - var locationRequest = new BreweryLocationUpdateRequest( - BreweryPostLocationId: Guid.NewGuid(), - CityId: Guid.NewGuid(), - AddressLine1: "456 Oak Ave", - AddressLine2: "Suite 2", - PostalCode: "54321", - Coordinates: null - ); - var request = new BreweryUpdateRequest( - BreweryPostId: Guid.NewGuid(), - PostedById: Guid.NewGuid(), - BreweryName: "Name", - Description: "Description", - Location: locationRequest - ); - - BreweryPost? persisted = null; - _repoMock - .Setup(r => r.UpdateAsync(It.IsAny())) - .Callback(b => persisted = b) - .Returns(Task.CompletedTask); - - await _service.UpdateAsync(request); - - persisted!.Location.Should().NotBeNull(); - persisted.Location!.BreweryPostLocationId.Should().Be(locationRequest.BreweryPostLocationId); - persisted.Location.BreweryPostId.Should().Be(request.BreweryPostId); - persisted.Location.CityId.Should().Be(locationRequest.CityId); - persisted.Location.AddressLine1.Should().Be(locationRequest.AddressLine1); - persisted.Location.AddressLine2.Should().Be(locationRequest.AddressLine2); - persisted.Location.PostalCode.Should().Be(locationRequest.PostalCode); - } - - [Fact] - public async Task DeleteAsync_DelegatesToRepository() - { - var id = Guid.NewGuid(); - _repoMock.Setup(r => r.DeleteAsync(id)).Returns(Task.CompletedTask); - - await _service.DeleteAsync(id); - - _repoMock.Verify(r => r.DeleteAsync(id), Times.Once); - } -} diff --git a/web/backend/Service/Service.Breweries/BreweryService.cs b/web/backend/Service/Service.Breweries/BreweryService.cs deleted file mode 100644 index dd1b750..0000000 --- a/web/backend/Service/Service.Breweries/BreweryService.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Domain.Entities; -using Infrastructure.Repository.Breweries; - -namespace Service.Breweries; - -/// -/// Handles retrieval, creation, update, and deletion of brewery posts. -/// -/// Repository used to persist and query brewery post data. -public class BreweryService(IBreweryRepository repository) : IBreweryService -{ - /// - /// Retrieves a brewery post by its unique identifier. - /// - /// The unique identifier of the brewery post. - /// The matching , or null if no such post exists. - public Task GetByIdAsync(Guid id) => - repository.GetByIdAsync(id); - - /// - /// Retrieves all brewery posts, optionally paginated. - /// - /// The maximum number of results to return, or null for no limit. - /// The number of results to skip, or null to start from the beginning. - /// A collection of entities. - public Task> GetAllAsync(int? limit = null, int? offset = null) => - repository.GetAllAsync(limit, offset); - - /// - /// Creates a new brewery post, generating new identifiers for the post and its location. - /// - /// The details of the brewery post to create. - /// A successful wrapping the newly created brewery post. - public async Task CreateAsync(BreweryCreateRequest request) - { - var entity = new BreweryPost - { - BreweryPostId = Guid.NewGuid(), - PostedById = request.PostedById, - BreweryName = request.BreweryName, - Description = request.Description, - CreatedAt = DateTime.UtcNow, - Location = new BreweryPostLocation - { - BreweryPostLocationId = Guid.NewGuid(), - CityId = request.Location.CityId, - AddressLine1 = request.Location.AddressLine1, - AddressLine2 = request.Location.AddressLine2, - PostalCode = request.Location.PostalCode, - Coordinates = request.Location.Coordinates, - }, - }; - - await repository.CreateAsync(entity); - return new BreweryServiceReturn(entity); - } - - /// - /// Updates an existing brewery post. If has no Location, - /// the brewery's location is cleared. - /// - /// The updated details of the brewery post. - /// A successful wrapping the updated brewery post. - public async Task UpdateAsync(BreweryUpdateRequest request) - { - var entity = new BreweryPost - { - BreweryPostId = request.BreweryPostId, - PostedById = request.PostedById, - BreweryName = request.BreweryName, - Description = request.Description, - UpdatedAt = DateTime.UtcNow, - Location = request.Location is null ? null : new BreweryPostLocation - { - BreweryPostLocationId = request.Location.BreweryPostLocationId, - BreweryPostId = request.BreweryPostId, - CityId = request.Location.CityId, - AddressLine1 = request.Location.AddressLine1, - AddressLine2 = request.Location.AddressLine2, - PostalCode = request.Location.PostalCode, - Coordinates = request.Location.Coordinates, - }, - }; - - await repository.UpdateAsync(entity); - return new BreweryServiceReturn(entity); - } - - /// - /// Deletes a brewery post by its unique identifier. - /// - /// The unique identifier of the brewery post to delete. - /// A task that completes once the deletion has finished. - public Task DeleteAsync(Guid id) => - repository.DeleteAsync(id); -} diff --git a/web/backend/Service/Service.Breweries/IBreweryService.cs b/web/backend/Service/Service.Breweries/IBreweryService.cs deleted file mode 100644 index ef97a9e..0000000 --- a/web/backend/Service/Service.Breweries/IBreweryService.cs +++ /dev/null @@ -1,151 +0,0 @@ -using Domain.Entities; - -namespace Service.Breweries; - -/// -/// Represents the data required to create a new brewery post. -/// -/// The unique identifier of the user creating the post. -/// The name of the brewery. -/// A description of the brewery. -/// The location details for the new brewery post. -public record BreweryCreateRequest( - Guid PostedById, - string BreweryName, - string Description, - BreweryLocationCreateRequest Location -); - -/// -/// Represents the location data required to create a new brewery post. -/// -/// The unique identifier of the city the brewery is located in. -/// The primary address line. -/// An optional secondary address line. -/// The postal code of the brewery's address. -/// Optional geographic coordinates for the brewery's location. -public record BreweryLocationCreateRequest( - Guid CityId, - string AddressLine1, - string? AddressLine2, - string PostalCode, - byte[]? Coordinates -); - -/// -/// Represents the data required to update an existing brewery post. -/// -/// The unique identifier of the brewery post to update. -/// The unique identifier of the user who owns/posted the brewery. -/// The updated name of the brewery. -/// The updated description of the brewery. -/// The updated location details, or null to clear the location. -public record BreweryUpdateRequest( - Guid BreweryPostId, - Guid PostedById, - string BreweryName, - string Description, - BreweryLocationUpdateRequest? Location -); - -/// -/// Represents the location data required to update an existing brewery post. -/// -/// The unique identifier of the existing brewery location record. -/// The unique identifier of the city the brewery is located in. -/// The primary address line. -/// An optional secondary address line. -/// The postal code of the brewery's address. -/// Optional geographic coordinates for the brewery's location. -public record BreweryLocationUpdateRequest( - Guid BreweryPostLocationId, - Guid CityId, - string AddressLine1, - string? AddressLine2, - string PostalCode, - byte[]? Coordinates -); - -/// -/// Represents the result of a create or update operation against a brewery post. -/// -public record BreweryServiceReturn -{ - /// - /// Gets a value indicating whether the operation succeeded. - /// - public bool Success { get; init; } - - /// - /// Gets the resulting brewery post when the operation succeeded; otherwise a default/unset value. - /// - public BreweryPost Brewery { get; init; } - - /// - /// Gets a message describing the failure reason, empty when is true. - /// - public string Message { get; init; } = string.Empty; - - /// - /// Initializes a new successful result wrapping the given brewery post. - /// - /// The brewery post resulting from the operation. - public BreweryServiceReturn(BreweryPost brewery) - { - Success = true; - Brewery = brewery; - } - - /// - /// Initializes a new failed result with the given error message. - /// - /// A message describing why the operation failed. - public BreweryServiceReturn(string message) - { - Success = false; - Brewery = default!; - Message = message; - } -} - -/// -/// Defines operations for retrieving, creating, updating, and deleting brewery posts. -/// -public interface IBreweryService -{ - /// - /// Retrieves a brewery post by its unique identifier. - /// - /// The unique identifier of the brewery post. - /// The matching , or null if no such post exists. - Task GetByIdAsync(Guid id); - - /// - /// Retrieves all brewery posts, optionally paginated. - /// - /// The maximum number of results to return, or null for no limit. - /// The number of results to skip, or null to start from the beginning. - /// A collection of entities. - Task> GetAllAsync(int? limit = null, int? offset = null); - - /// - /// Creates a new brewery post. - /// - /// The details of the brewery post to create. - /// A describing the outcome of the creation. - Task CreateAsync(BreweryCreateRequest request); - - /// - /// Updates an existing brewery post. - /// - /// The updated details of the brewery post. - /// A describing the outcome of the update. - Task UpdateAsync(BreweryUpdateRequest request); - - /// - /// Deletes a brewery post by its unique identifier. - /// - /// The unique identifier of the brewery post to delete. - /// A task that completes once the deletion has finished. - Task DeleteAsync(Guid id); -} diff --git a/web/backend/Service/Service.Breweries/Service.Breweries.csproj b/web/backend/Service/Service.Breweries/Service.Breweries.csproj deleted file mode 100644 index 578b547..0000000 --- a/web/backend/Service/Service.Breweries/Service.Breweries.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - net10.0 - enable - enable - - - - - - -