diff --git a/src/Core/API/API.Core/API.Core.csproj b/src/Core/API/API.Core/API.Core.csproj
index 61c6ba5..aab193e 100644
--- a/src/Core/API/API.Core/API.Core.csproj
+++ b/src/Core/API/API.Core/API.Core.csproj
@@ -31,6 +31,7 @@
+
diff --git a/src/Core/API/API.Core/Contracts/Breweries/BreweryCreateRequestValidator.cs b/src/Core/API/API.Core/Contracts/Breweries/BreweryCreateRequestValidator.cs
new file mode 100644
index 0000000..b181a74
--- /dev/null
+++ b/src/Core/API/API.Core/Contracts/Breweries/BreweryCreateRequestValidator.cs
@@ -0,0 +1,50 @@
+using FluentValidation;
+
+namespace API.Core.Contracts.Breweries;
+
+public class BreweryCreateDtoValidator : AbstractValidator
+{
+ public BreweryCreateDtoValidator()
+ {
+ RuleFor(x => x.PostedById)
+ .NotEmpty()
+ .WithMessage("PostedById is required.");
+
+ RuleFor(x => x.BreweryName)
+ .NotEmpty()
+ .WithMessage("Brewery name is required.")
+ .MaximumLength(256)
+ .WithMessage("Brewery name cannot exceed 256 characters.");
+
+ RuleFor(x => x.Description)
+ .NotEmpty()
+ .WithMessage("Description is required.")
+ .MaximumLength(512)
+ .WithMessage("Description cannot exceed 512 characters.");
+
+ RuleFor(x => x.Location)
+ .NotNull()
+ .WithMessage("Location is required.");
+
+ RuleFor(x => x.Location.CityId)
+ .NotEmpty()
+ .When(x => x.Location is not null)
+ .WithMessage("CityId is required.");
+
+ RuleFor(x => x.Location.AddressLine1)
+ .NotEmpty()
+ .When(x => x.Location is not null)
+ .WithMessage("Address line 1 is required.")
+ .MaximumLength(256)
+ .When(x => x.Location is not null)
+ .WithMessage("Address line 1 cannot exceed 256 characters.");
+
+ RuleFor(x => x.Location.PostalCode)
+ .NotEmpty()
+ .When(x => x.Location is not null)
+ .WithMessage("Postal code is required.")
+ .MaximumLength(20)
+ .When(x => x.Location is not null)
+ .WithMessage("Postal code cannot exceed 20 characters.");
+ }
+}
diff --git a/src/Core/API/API.Core/Contracts/Breweries/BreweryDto.cs b/src/Core/API/API.Core/Contracts/Breweries/BreweryDto.cs
new file mode 100644
index 0000000..c6ff6bf
--- /dev/null
+++ b/src/Core/API/API.Core/Contracts/Breweries/BreweryDto.cs
@@ -0,0 +1,41 @@
+namespace API.Core.Contracts.Breweries;
+
+public class BreweryLocationCreateDto
+{
+ public Guid CityId { get; set; }
+ public string AddressLine1 { get; set; } = string.Empty;
+ public string? AddressLine2 { get; set; }
+ public string PostalCode { get; set; } = string.Empty;
+ public byte[]? Coordinates { get; set; }
+}
+
+public class BreweryLocationDto
+{
+ public Guid BreweryPostLocationId { get; set; }
+ public Guid BreweryPostId { get; set; }
+ public Guid CityId { get; set; }
+ public string AddressLine1 { get; set; } = string.Empty;
+ public string? AddressLine2 { get; set; }
+ public string PostalCode { get; set; } = string.Empty;
+ public byte[]? Coordinates { get; set; }
+}
+
+public class BreweryCreateDto
+{
+ public Guid PostedById { get; set; }
+ public string BreweryName { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public BreweryLocationCreateDto Location { get; set; } = null!;
+}
+
+public class BreweryDto
+{
+ public Guid BreweryPostId { get; set; }
+ public Guid PostedById { get; set; }
+ public string BreweryName { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+ public DateTime? UpdatedAt { get; set; }
+ public byte[]? Timer { get; set; }
+ public BreweryLocationDto? Location { get; set; }
+}
diff --git a/src/Core/API/API.Core/Controllers/AuthController.cs b/src/Core/API/API.Core/Controllers/AuthController.cs
index 7b2dcc4..a6cc9af 100644
--- a/src/Core/API/API.Core/Controllers/AuthController.cs
+++ b/src/Core/API/API.Core/Controllers/AuthController.cs
@@ -86,6 +86,13 @@ namespace API.Core.Controllers
);
}
+ [HttpPost("confirm/resend")]
+ public async Task ResendConfirmation([FromQuery] Guid userId)
+ {
+ await confirmationService.ResendConfirmationEmailAsync(userId);
+ return Ok(new ResponseBody { Message = "confirmation email has been resent" });
+ }
+
[AllowAnonymous]
[HttpPost("refresh")]
public async Task Refresh(
diff --git a/src/Core/API/API.Core/Controllers/BreweryController.cs b/src/Core/API/API.Core/Controllers/BreweryController.cs
new file mode 100644
index 0000000..d0fb8d4
--- /dev/null
+++ b/src/Core/API/API.Core/Controllers/BreweryController.cs
@@ -0,0 +1,129 @@
+using API.Core.Contracts.Breweries;
+using API.Core.Contracts.Common;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Service.Breweries;
+
+namespace API.Core.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+[Authorize(AuthenticationSchemes = "JWT")]
+public class BreweryController(IBreweryService breweryService) : ControllerBase
+{
+ [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),
+ });
+ }
+
+ [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),
+ });
+ }
+
+ [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),
+ });
+ }
+
+ [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 = MapToDto(result.Brewery),
+ });
+ }
+
+ [HttpDelete("{id:guid}")]
+ public async Task> Delete(Guid id)
+ {
+ await breweryService.DeleteAsync(id);
+ return Ok(new ResponseBody { Message = "Brewery deleted successfully." });
+ }
+
+ private static BreweryDto MapToDto(Domain.Entities.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/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs
index e186dba..f6c7aee 100644
--- a/src/Core/API/API.Core/Program.cs
+++ b/src/Core/API/API.Core/Program.cs
@@ -1,20 +1,15 @@
using API.Core;
using API.Core.Authentication;
-using API.Core.Contracts.Common;
-using Domain.Exceptions;
using FluentValidation;
using FluentValidation.AspNetCore;
using Infrastructure.Email;
-using Infrastructure.Email.Templates;
using Infrastructure.Email.Templates.Rendering;
using Infrastructure.Jwt;
using Infrastructure.PasswordHashing;
using Infrastructure.Repository.Auth;
using Infrastructure.Repository.Sql;
using Infrastructure.Repository.UserAccount;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Filters;
+using Infrastructure.Repository.Breweries;
using Service.Auth;
using Service.Emails;
using Service.UserManagement.User;
@@ -55,6 +50,7 @@ builder.Services.AddSingleton<
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
diff --git a/src/Core/Core.slnx b/src/Core/Core.slnx
index b1ff5d5..6add115 100644
--- a/src/Core/Core.slnx
+++ b/src/Core/Core.slnx
@@ -26,6 +26,7 @@
-
+
+
diff --git a/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql b/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql
index 38cdf51..3683dc7 100644
--- a/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql
+++ b/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql
@@ -12,7 +12,7 @@ AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
-
+
IF @BreweryName IS NULL
THROW 50001, 'Brewery name cannot be null.', 1;
@@ -30,6 +30,7 @@ BEGIN
THROW 50404, 'City not found.', 1;
DECLARE @NewBreweryID UNIQUEIDENTIFIER = NEWID();
+ DECLARE @NewBrewerLocationID UNIQUEIDENTIFIER = NEWID();
BEGIN TRANSACTION;
@@ -37,9 +38,13 @@ BEGIN
(BreweryPostID, BreweryName, Description, PostedByID)
VALUES (@NewBreweryID, @BreweryName, @Description, @PostedByID);
- INSERT INTO dbo.BreweryPostLocation
- (@NewBreweryID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates)
- VALUES (@NewBreweryID, @CityID, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates);
+ INSERT INTO dbo.BreweryPostLocation
+ (BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates)
+ VALUES (@NewBrewerLocationID, @NewBreweryID, @CityID, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates);
COMMIT TRANSACTION;
+
+ SELECT @NewBreweryID AS BreweryPostID,
+ @NewBrewerLocationID AS BreweryPostLocationID;
+
END
diff --git a/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql b/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql
new file mode 100644
index 0000000..25425ef
--- /dev/null
+++ b/src/Core/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql
@@ -0,0 +1,9 @@
+CREATE OR ALTER PROCEDURE dbo.USP_GetBreweryById @BreweryPostID UNIQUEIDENTIFIER
+AS
+BEGIN
+ SELECT *
+ FROM BreweryPost bp
+ INNER JOIN BreweryPostLocation bpl
+ ON bp.BreweryPostID = bpl.BreweryPostID
+ WHERE bp.BreweryPostID = @BreweryPostID;
+END
\ No newline at end of file
diff --git a/src/Core/Domain/Domain.Entities/Entities/BreweryPost.cs b/src/Core/Domain/Domain.Entities/Entities/BreweryPost.cs
new file mode 100644
index 0000000..07a7b78
--- /dev/null
+++ b/src/Core/Domain/Domain.Entities/Entities/BreweryPost.cs
@@ -0,0 +1,13 @@
+namespace Domain.Entities;
+
+public class BreweryPost
+{
+ public Guid BreweryPostId { get; set; }
+ public Guid PostedById { get; set; }
+ public string BreweryName { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+ public DateTime? UpdatedAt { get; set; }
+ public byte[]? Timer { get; set; }
+ public BreweryPostLocation? Location { get; set; }
+}
diff --git a/src/Core/Domain/Domain.Entities/Entities/BreweryPostLocation.cs b/src/Core/Domain/Domain.Entities/Entities/BreweryPostLocation.cs
new file mode 100644
index 0000000..460e30a
--- /dev/null
+++ b/src/Core/Domain/Domain.Entities/Entities/BreweryPostLocation.cs
@@ -0,0 +1,13 @@
+namespace Domain.Entities;
+
+public class BreweryPostLocation
+{
+ public Guid BreweryPostLocationId { get; set; }
+ public Guid BreweryPostId { get; set; }
+ public string AddressLine1 { get; set; } = string.Empty;
+ public string? AddressLine2 { get; set; }
+ public string PostalCode { get; set; } = string.Empty;
+ public Guid CityId { get; set; }
+ public byte[]? Coordinates { get; set; }
+ public byte[]? Timer { get; set; }
+}
diff --git a/src/Core/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj b/src/Core/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj
index b02eae8..717486f 100644
--- a/src/Core/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj
+++ b/src/Core/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj
@@ -7,6 +7,6 @@
-
+
diff --git a/src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs
index e30ceee..e067b4f 100644
--- a/src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs
+++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs
@@ -17,10 +17,34 @@ public class AuthRepositoryTest
var conn = new MockDbConnection();
conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
+ .ReturnsScalar(expectedUserId);
+
+ // Mock the subsequent read for the newly created user by id
+ conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById")
.ReturnsTable(
MockTable
- .WithColumns(("UserAccountId", typeof(Guid)))
- .AddRow(expectedUserId)
+ .WithColumns(
+ ("UserAccountId", typeof(Guid)),
+ ("Username", typeof(string)),
+ ("FirstName", typeof(string)),
+ ("LastName", typeof(string)),
+ ("Email", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("UpdatedAt", typeof(DateTime?)),
+ ("DateOfBirth", typeof(DateTime)),
+ ("Timer", typeof(byte[]))
+ )
+ .AddRow(
+ expectedUserId,
+ "testuser",
+ "Test",
+ "User",
+ "test@example.com",
+ DateTime.UtcNow,
+ null,
+ new DateTime(1990, 1, 1),
+ null
+ )
);
var repo = CreateRepo(conn);
diff --git a/src/Core/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs
new file mode 100644
index 0000000..6352124
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Breweries/BreweryRepository.test.cs
@@ -0,0 +1,108 @@
+using Apps72.Dev.Data.DbMocker;
+using FluentAssertions;
+using Infrastructure.Repository.Breweries;
+using Infrastructure.Repository.Tests.Database;
+using Domain.Entities;
+
+namespace Infrastructure.Repository.Tests.Breweries;
+
+public class BreweryRepositoryTest
+{
+ private static BreweryRepository CreateRepo(MockDbConnection conn) =>
+ new(new TestConnectionFactory(conn));
+
+ [Fact]
+ public async Task GetByIdAsync_ReturnsBrewery_WhenExists()
+ {
+ var breweryId = Guid.NewGuid();
+ var conn = new MockDbConnection();
+
+ // Repository calls the stored procedure
+ const string getByIdSql = "USP_GetBreweryById";
+
+ var locationId = Guid.NewGuid();
+
+ conn.Mocks.When(cmd => cmd.CommandText == getByIdSql)
+ .ReturnsTable(
+ MockTable
+ .WithColumns(
+ ("BreweryPostId", typeof(Guid)),
+ ("PostedById", typeof(Guid)),
+ ("BreweryName", typeof(string)),
+ ("Description", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("UpdatedAt", typeof(DateTime?)),
+ ("Timer", typeof(byte[])),
+ ("BreweryPostLocationId", typeof(Guid)),
+ ("CityId", typeof(Guid)),
+ ("AddressLine1", typeof(string)),
+ ("AddressLine2", typeof(string)),
+ ("PostalCode", typeof(string)),
+ ("Coordinates", typeof(byte[]))
+ )
+ .AddRow(
+ breweryId,
+ Guid.NewGuid(),
+ "Test Brewery",
+ "A test brewery description",
+ DateTime.UtcNow,
+ null,
+ null,
+ locationId,
+ Guid.NewGuid(),
+ "123 Main St",
+ null,
+ "12345",
+ null
+ )
+ );
+
+ var repo = CreateRepo(conn);
+ var result = await repo.GetByIdAsync(breweryId);
+ result.Should().NotBeNull();
+ result!.BreweryPostId.Should().Be(breweryId);
+ result.Location.Should().NotBeNull();
+ result.Location!.BreweryPostLocationId.Should().Be(locationId);
+ }
+
+ [Fact]
+ public async Task GetByIdAsync_ReturnsNull_WhenNotExists()
+ {
+ var conn = new MockDbConnection();
+ conn.Mocks.When(cmd => cmd.CommandText == "USP_GetBreweryById")
+ .ReturnsTable(MockTable.Empty());
+ var repo = CreateRepo(conn);
+ var result = await repo.GetByIdAsync(Guid.NewGuid());
+ result.Should().BeNull();
+
+ }
+
+ [Fact]
+ public async Task CreateAsync_ExecutesSuccessfully()
+ {
+ var conn = new MockDbConnection();
+ conn.Mocks.When(cmd => cmd.CommandText == "USP_CreateBrewery")
+ .ReturnsScalar(1);
+ var repo = CreateRepo(conn);
+ var brewery = new BreweryPost
+ {
+ BreweryPostId = Guid.NewGuid(),
+ PostedById = Guid.NewGuid(),
+ BreweryName = "Test Brewery",
+ Description = "A test brewery description",
+ CreatedAt = DateTime.UtcNow,
+ Location = new BreweryPostLocation
+ {
+ BreweryPostLocationId = Guid.NewGuid(),
+ CityId = Guid.NewGuid(),
+ AddressLine1 = "123 Main St",
+ PostalCode = "12345",
+ Coordinates = [0x00, 0x01]
+ }
+ };
+
+ // Should not throw
+ var act = async () => await repo.CreateAsync(brewery);
+ await act.Should().NotThrowAsync();
+ }
+}
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs
index fbff2b3..bb701b3 100644
--- a/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs
@@ -33,18 +33,39 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
AddParameter(command, "@Hash", passwordHash);
var result = await command.ExecuteScalarAsync();
- var userAccountId = result != null ? (Guid)result : Guid.Empty;
- return new Domain.Entities.UserAccount
+ Guid userAccountId = Guid.Empty;
+ if (result != null && result != DBNull.Value)
{
- UserAccountId = userAccountId,
- Username = username,
- FirstName = firstName,
- LastName = lastName,
- Email = email,
- DateOfBirth = dateOfBirth,
- CreatedAt = DateTime.UtcNow,
- };
+ if (result is Guid g)
+ {
+ userAccountId = g;
+ }
+ else if (result is string s && Guid.TryParse(s, out var parsed))
+ {
+ userAccountId = parsed;
+ }
+ else if (result is byte[] bytes && bytes.Length == 16)
+ {
+ userAccountId = new Guid(bytes);
+ }
+ else
+ {
+ // Fallback: try to convert and parse string representation
+ try
+ {
+ var str = result.ToString();
+ if (!string.IsNullOrEmpty(str) && Guid.TryParse(str, out var p))
+ userAccountId = p;
+ }
+ catch
+ {
+ userAccountId = Guid.Empty;
+ }
+ }
+ }
+
+ return await GetUserByIdAsync(userAccountId) ?? throw new Exception("Failed to retrieve newly registered user.");
}
public async Task GetUserByEmailAsync(
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs
new file mode 100644
index 0000000..08332c4
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs
@@ -0,0 +1,147 @@
+using System.Data.Common;
+using Domain.Entities;
+using Infrastructure.Repository.Sql;
+
+namespace Infrastructure.Repository.Breweries;
+
+public class BreweryRepository(ISqlConnectionFactory connectionFactory)
+ : Repository(connectionFactory), IBreweryRepository
+{
+ private readonly ISqlConnectionFactory _connectionFactory = connectionFactory;
+
+ public async Task GetByIdAsync(Guid id)
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandType = System.Data.CommandType.StoredProcedure;
+
+ command.CommandText = "USP_GetBreweryById";
+ AddParameter(command, "@BreweryPostID", id);
+
+ await using var reader = await command.ExecuteReaderAsync();
+ if (await reader.ReadAsync())
+ {
+ return MapToEntity(reader);
+ }
+ return null;
+ }
+
+ public Task> GetAllAsync(int? limit, int? offset)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdateAsync(BreweryPost brewery)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task DeleteAsync(Guid id)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task CreateAsync(BreweryPost brewery)
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+
+ command.CommandText = "USP_CreateBrewery";
+ command.CommandType = System.Data.CommandType.StoredProcedure;
+
+ if (brewery.Location is null)
+ {
+ throw new ArgumentException("Location must be provided when creating a brewery.");
+ }
+
+ AddParameter(command, "@BreweryName", brewery.BreweryName);
+ AddParameter(command, "@Description", brewery.Description);
+ AddParameter(command, "@PostedByID", brewery.PostedById);
+ AddParameter(command, "@CityID", brewery.Location?.CityId);
+ AddParameter(command, "@AddressLine1", brewery.Location?.AddressLine1);
+ AddParameter(command, "@AddressLine2", brewery.Location?.AddressLine2);
+ AddParameter(command, "@PostalCode", brewery.Location?.PostalCode);
+ AddParameter(command, "@Coordinates", brewery.Location?.Coordinates);
+ await command.ExecuteNonQueryAsync();
+
+ }
+
+ protected override BreweryPost MapToEntity(DbDataReader reader)
+ {
+ var brewery = new BreweryPost();
+
+ var ordBreweryPostId = reader.GetOrdinal("BreweryPostId");
+ var ordPostedById = reader.GetOrdinal("PostedById");
+ var ordBreweryName = reader.GetOrdinal("BreweryName");
+ var ordDescription = reader.GetOrdinal("Description");
+ var ordCreatedAt = reader.GetOrdinal("CreatedAt");
+ var ordUpdatedAt = reader.GetOrdinal("UpdatedAt");
+ var ordTimer = reader.GetOrdinal("Timer");
+
+ brewery.BreweryPostId = reader.GetGuid(ordBreweryPostId);
+ brewery.PostedById = reader.GetGuid(ordPostedById);
+ brewery.BreweryName = reader.GetString(ordBreweryName);
+ brewery.Description = reader.GetString(ordDescription);
+ brewery.CreatedAt = reader.GetDateTime(ordCreatedAt);
+
+ brewery.UpdatedAt = reader.IsDBNull(ordUpdatedAt) ? null : reader.GetDateTime(ordUpdatedAt);
+
+ // Read timer (varbinary/rowversion) robustly
+ if (reader.IsDBNull(ordTimer))
+ {
+ brewery.Timer = null;
+ }
+ else
+ {
+ try
+ {
+ brewery.Timer = reader.GetFieldValue(ordTimer);
+ }
+ catch
+ {
+ var length = reader.GetBytes(ordTimer, 0, null, 0, 0);
+ var buffer = new byte[length];
+ reader.GetBytes(ordTimer, 0, buffer, 0, (int)length);
+ brewery.Timer = buffer;
+ }
+ }
+
+ // Map BreweryPostLocation if columns are present
+ try
+ {
+ var ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
+ if (!reader.IsDBNull(ordLocationId))
+ {
+ var location = new BreweryPostLocation
+ {
+ BreweryPostLocationId = reader.GetGuid(ordLocationId),
+ BreweryPostId = reader.GetGuid(reader.GetOrdinal("BreweryPostId")),
+ CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
+ AddressLine1 = reader.GetString(reader.GetOrdinal("AddressLine1")),
+ AddressLine2 = reader.IsDBNull(reader.GetOrdinal("AddressLine2")) ? null : reader.GetString(reader.GetOrdinal("AddressLine2")),
+ PostalCode = reader.GetString(reader.GetOrdinal("PostalCode")),
+ Coordinates = reader.IsDBNull(reader.GetOrdinal("Coordinates")) ? null : reader.GetFieldValue(reader.GetOrdinal("Coordinates"))
+ };
+ brewery.Location = location;
+ }
+ }
+ catch (IndexOutOfRangeException)
+ {
+ // Location columns not present, skip mapping location
+ }
+
+ return brewery;
+ }
+
+ private static void AddParameter(
+ DbCommand command,
+ string name,
+ object? value
+ )
+ {
+ var p = command.CreateParameter();
+ p.ParameterName = name;
+ p.Value = value ?? DBNull.Value;
+ command.Parameters.Add(p);
+ }
+}
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Breweries/IBreweryRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Breweries/IBreweryRepository.cs
new file mode 100644
index 0000000..2d394cb
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Breweries/IBreweryRepository.cs
@@ -0,0 +1,12 @@
+using Domain.Entities;
+
+namespace Infrastructure.Repository.Breweries;
+
+public interface IBreweryRepository
+{
+ Task GetByIdAsync(Guid id);
+ Task> GetAllAsync(int? limit, int? offset);
+ Task UpdateAsync(BreweryPost brewery);
+ Task DeleteAsync(Guid id);
+ Task CreateAsync(BreweryPost brewery);
+}
\ No newline at end of file
diff --git a/src/Core/Service/Service.Breweries.Tests/BreweryService.test.cs b/src/Core/Service/Service.Breweries.Tests/BreweryService.test.cs
new file mode 100644
index 0000000..a6b49cb
--- /dev/null
+++ b/src/Core/Service/Service.Breweries.Tests/BreweryService.test.cs
@@ -0,0 +1,69 @@
+using FluentAssertions;
+using Xunit;
+using Service.Breweries;
+using API.Core.Contracts.Breweries;
+using Domain.Entities;
+
+namespace Service.Breweries.Tests;
+
+public class BreweryServiceTests
+{
+ private class FakeRepo : IBreweryRepository
+ {
+ public BreweryPost? Created;
+
+ public Task GetByIdAsync(Guid id) => Task.FromResult(null);
+ public Task> GetAllAsync(int? limit, int? offset) => Task.FromResult>(Array.Empty());
+ public Task UpdateAsync(BreweryPost brewery) { Created = brewery; return Task.CompletedTask; }
+ public Task DeleteAsync(Guid id) => Task.CompletedTask;
+ public Task CreateAsync(BreweryPost brewery) { Created = brewery; return Task.CompletedTask; }
+ }
+
+ [Fact]
+ public async Task CreateAsync_ReturnsFailure_WhenLocationMissing()
+ {
+ var repo = new FakeRepo();
+ var svc = new BreweryService(repo);
+
+ var dto = new BreweryCreateDto
+ {
+ PostedById = Guid.NewGuid(),
+ BreweryName = "X",
+ Description = "Y",
+ Location = null!
+ };
+
+ var result = await svc.CreateAsync(dto);
+ result.Success.Should().BeFalse();
+ result.Message.Should().Contain("Location");
+ }
+
+ [Fact]
+ public async Task CreateAsync_ReturnsSuccess_AndPersistsEntity()
+ {
+ var repo = new FakeRepo();
+ var svc = new BreweryService(repo);
+
+ var loc = new BreweryLocationCreateDto
+ {
+ CityId = Guid.NewGuid(),
+ AddressLine1 = "123 Main",
+ PostalCode = "12345"
+ };
+
+ var dto = new BreweryCreateDto
+ {
+ PostedById = Guid.NewGuid(),
+ BreweryName = "MyBrew",
+ Description = "Desc",
+ Location = loc
+ };
+
+ var result = await svc.CreateAsync(dto);
+
+ result.Success.Should().BeTrue();
+ repo.Created.Should().NotBeNull();
+ repo.Created!.BreweryName.Should().Be("MyBrew");
+ result.Brewery.BreweryName.Should().Be("MyBrew");
+ }
+}
diff --git a/src/Core/Service/Service.Breweries.Tests/Service.Breweries.Tests.csproj b/src/Core/Service/Service.Breweries.Tests/Service.Breweries.Tests.csproj
new file mode 100644
index 0000000..bd1c5c8
--- /dev/null
+++ b/src/Core/Service/Service.Breweries.Tests/Service.Breweries.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+ Service.Breweries.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Core/Service/Service.Breweries/BreweryService.cs b/src/Core/Service/Service.Breweries/BreweryService.cs
new file mode 100644
index 0000000..5966f6c
--- /dev/null
+++ b/src/Core/Service/Service.Breweries/BreweryService.cs
@@ -0,0 +1,65 @@
+using Domain.Entities;
+using Infrastructure.Repository.Breweries;
+
+namespace Service.Breweries;
+
+public class BreweryService(IBreweryRepository repository) : IBreweryService
+{
+ public Task GetByIdAsync(Guid id) =>
+ repository.GetByIdAsync(id);
+
+ public Task> GetAllAsync(int? limit = null, int? offset = null) =>
+ repository.GetAllAsync(limit, offset);
+
+ 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);
+ }
+
+ 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);
+ }
+
+ public Task DeleteAsync(Guid id) =>
+ repository.DeleteAsync(id);
+}
diff --git a/src/Core/Service/Service.Breweries/IBreweryService.cs b/src/Core/Service/Service.Breweries/IBreweryService.cs
new file mode 100644
index 0000000..5331634
--- /dev/null
+++ b/src/Core/Service/Service.Breweries/IBreweryService.cs
@@ -0,0 +1,64 @@
+using Domain.Entities;
+
+namespace Service.Breweries;
+
+public record BreweryCreateRequest(
+ Guid PostedById,
+ string BreweryName,
+ string Description,
+ BreweryLocationCreateRequest Location
+);
+
+public record BreweryLocationCreateRequest(
+ Guid CityId,
+ string AddressLine1,
+ string? AddressLine2,
+ string PostalCode,
+ byte[]? Coordinates
+);
+
+public record BreweryUpdateRequest(
+ Guid BreweryPostId,
+ Guid PostedById,
+ string BreweryName,
+ string Description,
+ BreweryLocationUpdateRequest? Location
+);
+
+public record BreweryLocationUpdateRequest(
+ Guid BreweryPostLocationId,
+ Guid CityId,
+ string AddressLine1,
+ string? AddressLine2,
+ string PostalCode,
+ byte[]? Coordinates
+);
+
+public record BreweryServiceReturn
+{
+ public bool Success { get; init; }
+ public BreweryPost Brewery { get; init; }
+ public string Message { get; init; } = string.Empty;
+
+ public BreweryServiceReturn(BreweryPost brewery)
+ {
+ Success = true;
+ Brewery = brewery;
+ }
+
+ public BreweryServiceReturn(string message)
+ {
+ Success = false;
+ Brewery = default!;
+ Message = message;
+ }
+}
+
+public interface IBreweryService
+{
+ Task GetByIdAsync(Guid id);
+ Task> GetAllAsync(int? limit = null, int? offset = null);
+ Task CreateAsync(BreweryCreateRequest request);
+ Task UpdateAsync(BreweryUpdateRequest request);
+ Task DeleteAsync(Guid id);
+}
diff --git a/src/Core/Service/Service.Breweries/Service.Breweries.csproj b/src/Core/Service/Service.Breweries/Service.Breweries.csproj
new file mode 100644
index 0000000..578b547
--- /dev/null
+++ b/src/Core/Service/Service.Breweries/Service.Breweries.csproj
@@ -0,0 +1,12 @@
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+