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
-
-
-
-
-
-
-