From 1b467ac4f1d0c55307c7654df93908c0442dddae Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Sun, 29 Mar 2026 20:08:21 -0400 Subject: [PATCH] Implement CRUD operations for Brewery, including service and repository layers --- .../Contracts/Breweries/BreweryCreateDto.cs | 11 +++ .../Contracts/Breweries/BreweryDto.cs | 15 ++++ .../Breweries/BreweryLocationCreateDto.cs | 12 ++++ .../Contracts/Breweries/BreweryLocationDto.cs | 14 ++++ .../Breweries/BreweryRepository.cs | 20 +----- .../Breweries/IBreweryRepository.cs | 12 ++++ .../BreweryService.test.cs | 69 ++++++++++++++++++ .../Service.Breweries.Tests.csproj | 28 ++++++++ .../Service.Breweries/BreweryService.cs | 72 +++++++++++++++++++ .../BreweryServiceCollectionExtensions.cs | 12 ++++ .../Service.Breweries/IBreweryService.cs | 33 +++++++++ .../Service.Breweries.csproj | 15 ++++ 12 files changed, 296 insertions(+), 17 deletions(-) create mode 100644 src/Core/API/API.Core/Contracts/Breweries/BreweryCreateDto.cs create mode 100644 src/Core/API/API.Core/Contracts/Breweries/BreweryDto.cs create mode 100644 src/Core/API/API.Core/Contracts/Breweries/BreweryLocationCreateDto.cs create mode 100644 src/Core/API/API.Core/Contracts/Breweries/BreweryLocationDto.cs create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Breweries/IBreweryRepository.cs create mode 100644 src/Core/Service/Service.Breweries.Tests/BreweryService.test.cs create mode 100644 src/Core/Service/Service.Breweries.Tests/Service.Breweries.Tests.csproj create mode 100644 src/Core/Service/Service.Breweries/BreweryService.cs create mode 100644 src/Core/Service/Service.Breweries/DependencyInjection/BreweryServiceCollectionExtensions.cs create mode 100644 src/Core/Service/Service.Breweries/IBreweryService.cs create mode 100644 src/Core/Service/Service.Breweries/Service.Breweries.csproj diff --git a/src/Core/API/API.Core/Contracts/Breweries/BreweryCreateDto.cs b/src/Core/API/API.Core/Contracts/Breweries/BreweryCreateDto.cs new file mode 100644 index 0000000..342d125 --- /dev/null +++ b/src/Core/API/API.Core/Contracts/Breweries/BreweryCreateDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace API.Core.Contracts.Breweries; + +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; } = new BreweryLocationCreateDto(); +} 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..3bd7c06 --- /dev/null +++ b/src/Core/API/API.Core/Contracts/Breweries/BreweryDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace API.Core.Contracts.Breweries; + +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/Contracts/Breweries/BreweryLocationCreateDto.cs b/src/Core/API/API.Core/Contracts/Breweries/BreweryLocationCreateDto.cs new file mode 100644 index 0000000..8844981 --- /dev/null +++ b/src/Core/API/API.Core/Contracts/Breweries/BreweryLocationCreateDto.cs @@ -0,0 +1,12 @@ +using System; + +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; } +} diff --git a/src/Core/API/API.Core/Contracts/Breweries/BreweryLocationDto.cs b/src/Core/API/API.Core/Contracts/Breweries/BreweryLocationDto.cs new file mode 100644 index 0000000..0c6173c --- /dev/null +++ b/src/Core/API/API.Core/Contracts/Breweries/BreweryLocationDto.cs @@ -0,0 +1,14 @@ +using System; + +namespace API.Core.Contracts.Breweries; + +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; } +} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs index 82177fc..08332c4 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Breweries/BreweryRepository.cs @@ -4,24 +4,10 @@ using Infrastructure.Repository.Sql; namespace Infrastructure.Repository.Breweries; -public interface IBreweryRepository +public class BreweryRepository(ISqlConnectionFactory connectionFactory) + : Repository(connectionFactory), IBreweryRepository { - Task GetByIdAsync(Guid id); - Task> GetAllAsync(int? limit, int? offset); - Task UpdateAsync(BreweryPost brewery); - Task DeleteAsync(Guid id); - Task CreateAsync(BreweryPost brewery); -} - -public class BreweryRepository : Repository, IBreweryRepository -{ - private readonly ISqlConnectionFactory _connectionFactory; - - public BreweryRepository(ISqlConnectionFactory connectionFactory) - : base(connectionFactory) - { - _connectionFactory = connectionFactory; - } + private readonly ISqlConnectionFactory _connectionFactory = connectionFactory; public async Task GetByIdAsync(Guid id) { 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..2338e3c --- /dev/null +++ b/src/Core/Service/Service.Breweries/BreweryService.cs @@ -0,0 +1,72 @@ +using Domain.Entities; +using Infrastructure.Repository.Breweries; +using API.Core.Contracts.Breweries; + +namespace Service.Breweries; + +public class BreweryService(IBreweryRepository repository) : IBreweryService +{ + private readonly IBreweryRepository _repository = repository; + + 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(BreweryCreateDto brewery) + { + if (brewery.Location is null) + return new BreweryServiceReturn("Location must be provided"); + + var entity = new BreweryPost + { + BreweryPostId = Guid.NewGuid(), + PostedById = brewery.PostedById, + BreweryName = brewery.BreweryName, + Description = brewery.Description, + CreatedAt = DateTime.UtcNow, + Location = new BreweryPostLocation + { + BreweryPostLocationId = Guid.NewGuid(), + CityId = brewery.Location.CityId, + AddressLine1 = brewery.Location.AddressLine1, + AddressLine2 = brewery.Location.AddressLine2, + PostalCode = brewery.Location.PostalCode, + Coordinates = brewery.Location.Coordinates + } + }; + + await _repository.CreateAsync(entity); + return new BreweryServiceReturn(entity); + } + + public async Task UpdateAsync(BreweryDto brewery) + { + if (brewery is null) return new BreweryServiceReturn("Brewery payload is null"); + + var entity = new BreweryPost + { + 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 BreweryPostLocation + { + BreweryPostLocationId = brewery.Location.BreweryPostLocationId, + BreweryPostId = brewery.BreweryPostId, + CityId = brewery.Location.CityId, + AddressLine1 = brewery.Location.AddressLine1, + AddressLine2 = brewery.Location.AddressLine2, + PostalCode = brewery.Location.PostalCode, + Coordinates = brewery.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/DependencyInjection/BreweryServiceCollectionExtensions.cs b/src/Core/Service/Service.Breweries/DependencyInjection/BreweryServiceCollectionExtensions.cs new file mode 100644 index 0000000..6884bbf --- /dev/null +++ b/src/Core/Service/Service.Breweries/DependencyInjection/BreweryServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Service.Breweries.DependencyInjection; + +public static class BreweryServiceCollectionExtensions +{ + public static IServiceCollection AddBreweryServices(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +} diff --git a/src/Core/Service/Service.Breweries/IBreweryService.cs b/src/Core/Service/Service.Breweries/IBreweryService.cs new file mode 100644 index 0000000..b2ec445 --- /dev/null +++ b/src/Core/Service/Service.Breweries/IBreweryService.cs @@ -0,0 +1,33 @@ +using API.Core.Contracts.Breweries; +using Domain.Entities; + +namespace Service.Breweries; + +public interface IBreweryService +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(int? limit = null, int? offset = null); + Task CreateAsync(BreweryCreateDto brewery); + Task UpdateAsync(BreweryDto brewery); + Task DeleteAsync(Guid id); +} + +public record BreweryServiceReturn +{ + public bool Success { get; init; } = false; + 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; + } +} 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..3ea9cbb --- /dev/null +++ b/src/Core/Service/Service.Breweries/Service.Breweries.csproj @@ -0,0 +1,15 @@ + + + net10.0 + enable + enable + + + + + + + + +