diff --git a/src/Common/SharedKernel/SharedKernel.csproj b/src/Common/Common.SharedKernel/Common.SharedKernel.csproj similarity index 100% rename from src/Common/SharedKernel/SharedKernel.csproj rename to src/Common/Common.SharedKernel/Common.SharedKernel.csproj diff --git a/src/Common/SharedKernel/Domain/Base/AggregateRoot.cs b/src/Common/Common.SharedKernel/Domain/Base/AggregateRoot.cs similarity index 85% rename from src/Common/SharedKernel/Domain/Base/AggregateRoot.cs rename to src/Common/Common.SharedKernel/Domain/Base/AggregateRoot.cs index 15273e3..bc43c18 100644 --- a/src/Common/SharedKernel/Domain/Base/AggregateRoot.cs +++ b/src/Common/Common.SharedKernel/Domain/Base/AggregateRoot.cs @@ -1,7 +1,7 @@ -using SharedKernel.Domain.Interfaces; +using Common.SharedKernel.Domain.Interfaces; using System.ComponentModel.DataAnnotations.Schema; -namespace SharedKernel.Domain.Base; +namespace Common.SharedKernel.Domain.Base; public abstract class AggregateRoot : Entity, IDomainEvents { diff --git a/src/Common/SharedKernel/Domain/Base/DomainEvent.cs b/src/Common/Common.SharedKernel/Domain/Base/DomainEvent.cs similarity index 62% rename from src/Common/SharedKernel/Domain/Base/DomainEvent.cs rename to src/Common/Common.SharedKernel/Domain/Base/DomainEvent.cs index 7654eef..7f2bf3e 100644 --- a/src/Common/SharedKernel/Domain/Base/DomainEvent.cs +++ b/src/Common/Common.SharedKernel/Domain/Base/DomainEvent.cs @@ -1,4 +1,4 @@ -namespace SharedKernel.Domain.Base; +namespace Common.SharedKernel.Domain.Base; //public record DomainEvent : INotification { } diff --git a/src/Common/SharedKernel/Domain/Base/Entity.cs b/src/Common/Common.SharedKernel/Domain/Base/Entity.cs similarity index 86% rename from src/Common/SharedKernel/Domain/Base/Entity.cs rename to src/Common/Common.SharedKernel/Domain/Base/Entity.cs index 4326da7..2650d43 100644 --- a/src/Common/SharedKernel/Domain/Base/Entity.cs +++ b/src/Common/Common.SharedKernel/Domain/Base/Entity.cs @@ -1,6 +1,6 @@ -using SharedKernel.Domain.Interfaces; +using Common.SharedKernel.Domain.Interfaces; -namespace SharedKernel.Domain.Base; +namespace Common.SharedKernel.Domain.Base; public abstract class Entity : IAuditableEntity { diff --git a/src/Common/Common.SharedKernel/Domain/Base/ValueObject.cs b/src/Common/Common.SharedKernel/Domain/Base/ValueObject.cs new file mode 100644 index 0000000..9f5dfb6 --- /dev/null +++ b/src/Common/Common.SharedKernel/Domain/Base/ValueObject.cs @@ -0,0 +1,3 @@ +namespace Common.SharedKernel.Domain.Base; + +public record ValueObject { } \ No newline at end of file diff --git a/src/Common/SharedKernel/Domain/Entities/Currency.cs b/src/Common/Common.SharedKernel/Domain/Entities/Currency.cs similarity index 89% rename from src/Common/SharedKernel/Domain/Entities/Currency.cs rename to src/Common/Common.SharedKernel/Domain/Entities/Currency.cs index ee5dbce..5b183d6 100644 --- a/src/Common/SharedKernel/Domain/Entities/Currency.cs +++ b/src/Common/Common.SharedKernel/Domain/Entities/Currency.cs @@ -1,6 +1,6 @@ -using SharedKernel.Domain.Exceptions; +using Common.SharedKernel.Domain.Exceptions; -namespace SharedKernel.Domain.Entities; +namespace Common.SharedKernel.Domain.Entities; public record Currency { diff --git a/src/Common/SharedKernel/Domain/Entities/Money.cs b/src/Common/Common.SharedKernel/Domain/Entities/Money.cs similarity index 93% rename from src/Common/SharedKernel/Domain/Entities/Money.cs rename to src/Common/Common.SharedKernel/Domain/Entities/Money.cs index a02322d..d143962 100644 --- a/src/Common/SharedKernel/Domain/Entities/Money.cs +++ b/src/Common/Common.SharedKernel/Domain/Entities/Money.cs @@ -1,4 +1,4 @@ -namespace SharedKernel.Domain.Entities; +namespace Common.SharedKernel.Domain.Entities; public record Money(Currency Currency, decimal Amount) { diff --git a/src/Common/SharedKernel/Domain/Exceptions/DomainException.cs b/src/Common/Common.SharedKernel/Domain/Exceptions/DomainException.cs similarity index 85% rename from src/Common/SharedKernel/Domain/Exceptions/DomainException.cs rename to src/Common/Common.SharedKernel/Domain/Exceptions/DomainException.cs index 70ae171..44abf17 100644 --- a/src/Common/SharedKernel/Domain/Exceptions/DomainException.cs +++ b/src/Common/Common.SharedKernel/Domain/Exceptions/DomainException.cs @@ -1,4 +1,4 @@ -namespace SharedKernel.Domain.Exceptions; +namespace Common.SharedKernel.Domain.Exceptions; public class DomainException : Exception { diff --git a/src/Common/SharedKernel/Domain/Exceptions/GuardClausesExt.cs b/src/Common/Common.SharedKernel/Domain/Exceptions/GuardClausesExt.cs similarity index 83% rename from src/Common/SharedKernel/Domain/Exceptions/GuardClausesExt.cs rename to src/Common/Common.SharedKernel/Domain/Exceptions/GuardClausesExt.cs index bde25c3..71c077c 100644 --- a/src/Common/SharedKernel/Domain/Exceptions/GuardClausesExt.cs +++ b/src/Common/Common.SharedKernel/Domain/Exceptions/GuardClausesExt.cs @@ -1,6 +1,7 @@ -using System.Runtime.CompilerServices; +using Ardalis.GuardClauses; +using System.Runtime.CompilerServices; -namespace Ardalis.GuardClauses; +namespace Common.SharedKernel.Domain.Exceptions; public static class FooGuard { diff --git a/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs b/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs new file mode 100644 index 0000000..d8dd8e1 --- /dev/null +++ b/src/Common/Common.SharedKernel/Domain/Identifiers/ProductId.cs @@ -0,0 +1,3 @@ +namespace Common.SharedKernel.Domain.Identifiers; + +public record ProductId(Guid Value); \ No newline at end of file diff --git a/src/Common/SharedKernel/Domain/Interfaces/IAuditableEntity.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IAuditableEntity.cs similarity index 74% rename from src/Common/SharedKernel/Domain/Interfaces/IAuditableEntity.cs rename to src/Common/Common.SharedKernel/Domain/Interfaces/IAuditableEntity.cs index 8b253d1..49e9340 100644 --- a/src/Common/SharedKernel/Domain/Interfaces/IAuditableEntity.cs +++ b/src/Common/Common.SharedKernel/Domain/Interfaces/IAuditableEntity.cs @@ -1,4 +1,4 @@ -namespace SharedKernel.Domain.Interfaces; +namespace Common.SharedKernel.Domain.Interfaces; public interface IAuditableEntity { diff --git a/src/Common/SharedKernel/Domain/Interfaces/IDomainEvents.cs b/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvents.cs similarity index 71% rename from src/Common/SharedKernel/Domain/Interfaces/IDomainEvents.cs rename to src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvents.cs index ee37648..974c2c3 100644 --- a/src/Common/SharedKernel/Domain/Interfaces/IDomainEvents.cs +++ b/src/Common/Common.SharedKernel/Domain/Interfaces/IDomainEvents.cs @@ -1,6 +1,6 @@ -using SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; -namespace SharedKernel.Domain.Interfaces; +namespace Common.SharedKernel.Domain.Interfaces; public interface IDomainEvents { diff --git a/src/Common/SharedKernel/Domain/Base/ValueObject.cs b/src/Common/SharedKernel/Domain/Base/ValueObject.cs deleted file mode 100644 index 2cead53..0000000 --- a/src/Common/SharedKernel/Domain/Base/ValueObject.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SharedKernel.Domain.Base; - -public record ValueObject { } \ No newline at end of file diff --git a/src/Common/SharedKernel/Domain/Identifiers/ProductId.cs b/src/Common/SharedKernel/Domain/Identifiers/ProductId.cs deleted file mode 100644 index ece9f4f..0000000 --- a/src/Common/SharedKernel/Domain/Identifiers/ProductId.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SharedKernel.Domain.Identifiers; - -public record ProductId(Guid Value); \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Domain/Customers/Customer.cs b/src/Modules/Orders/Modules.Orders.Domain/Customers/Customer.cs index f569b1d..1c6bf34 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Customers/Customer.cs +++ b/src/Modules/Orders/Modules.Orders.Domain/Customers/Customer.cs @@ -1,5 +1,5 @@ using Ardalis.GuardClauses; -using SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; namespace Modules.Orders.Domain.Customers; diff --git a/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerCreatedEvent.cs b/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerCreatedEvent.cs index d2b75af..d62abed 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders.Domain/Customers/CustomerCreatedEvent.cs @@ -1,4 +1,4 @@ -using SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; namespace Modules.Orders.Domain.Customers; diff --git a/src/Modules/Orders/Modules.Orders.Domain/Modules.Orders.Domain.csproj b/src/Modules/Orders/Modules.Orders.Domain/Modules.Orders.Domain.csproj index 204fb5c..383d33d 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Modules.Orders.Domain.csproj +++ b/src/Modules/Orders/Modules.Orders.Domain/Modules.Orders.Domain.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItem.cs b/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItem.cs index ec3225f..59e4e14 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItem.cs +++ b/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItem.cs @@ -1,7 +1,8 @@ using Ardalis.GuardClauses; -using SharedKernel.Domain.Base; -using SharedKernel.Domain.Entities; -using SharedKernel.Domain.Identifiers; +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Entities; +using Common.SharedKernel.Domain.Exceptions; +using Common.SharedKernel.Domain.Identifiers; namespace Modules.Orders.Domain.Orders; diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemCreatedEvent.cs b/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemCreatedEvent.cs index 97921ba..b5cb39f 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders.Domain/Orders/LineItemCreatedEvent.cs @@ -1,4 +1,4 @@ -using SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; namespace Modules.Orders.Domain.Orders; @@ -7,4 +7,4 @@ public record LineItemCreatedEvent(LineItemId LineItemId, OrderId Order) : Domai public LineItemCreatedEvent(LineItem lineItem) : this(lineItem.Id, lineItem.OrderId) { } public static LineItemCreatedEvent Create(LineItem lineItem) => new(lineItem.Id, lineItem.OrderId); -} \ No newline at end of file +} diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/Order.cs b/src/Modules/Orders/Modules.Orders.Domain/Orders/Order.cs index a2c490b..aa4c484 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/Order.cs +++ b/src/Modules/Orders/Modules.Orders.Domain/Orders/Order.cs @@ -1,9 +1,9 @@ using Ardalis.GuardClauses; +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Entities; +using Common.SharedKernel.Domain.Exceptions; +using Common.SharedKernel.Domain.Identifiers; using Modules.Orders.Domain.Customers; -using SharedKernel.Domain.Base; -using SharedKernel.Domain.Entities; -using SharedKernel.Domain.Exceptions; -using SharedKernel.Domain.Identifiers; namespace Modules.Orders.Domain.Orders; diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderCreatedEvent.cs b/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderCreatedEvent.cs index 6f05edb..04734d2 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderCreatedEvent.cs +++ b/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderCreatedEvent.cs @@ -1,5 +1,5 @@ -using Modules.Orders.Domain.Customers; -using SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; +using Modules.Orders.Domain.Customers; namespace Modules.Orders.Domain.Orders; diff --git a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderReadyForShippingEvent.cs b/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderReadyForShippingEvent.cs index 5077e60..b00dd2b 100644 --- a/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderReadyForShippingEvent.cs +++ b/src/Modules/Orders/Modules.Orders.Domain/Orders/OrderReadyForShippingEvent.cs @@ -1,4 +1,4 @@ -using SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; namespace Modules.Orders.Domain.Orders; diff --git a/src/Modules/Orders/Modules.Orders.Domain/Products/Product.cs b/src/Modules/Orders/Modules.Orders.Domain/Products/Product.cs deleted file mode 100644 index 32728bd..0000000 --- a/src/Modules/Orders/Modules.Orders.Domain/Products/Product.cs +++ /dev/null @@ -1,63 +0,0 @@ -// using Ardalis.GuardClauses; -// -// using SharedKernel.Domain.Base; -// using SharedKernel.Domain.Entities; -// -// namespace Modules.Orders.Domain.Products; -// -// public class Product : AggregateRoot -// { -// //public CategoryId CategoryId { get; set; } = null!; -// -// //public Category Category { get; set; } = null!; -// -// public string Name { get; private set; } = null!; -// -// public Money Price { get; private set; } = null!; -// -// public Sku Sku { get; private set; } = null!; -// -// private Product() { } -// -// // NOTE: Need to use a factory, as EF does not let owned entities (i.e Money & Sku) be passed via the constructor -// public static Product Create(string name, Money price, Sku sku/*, CategoryId categoryId*/) -// { -// Guard.Against.NullOrWhiteSpace(name); -// Guard.Against.Null(sku); -// Guard.Against.Null(price); -// Guard.Against.ZeroOrNegative(price.Amount); -// //Guard.Against.Null(categoryId); -// -// var product = new Product -// { -// Id = new ProductId(Guid.NewGuid()), -// // CategoryId = categoryId, -// Name = name, -// Price = price, -// Sku = sku -// }; -// -// product.AddDomainEvent(ProductCreatedEvent.Create(product)); -// -// return product; -// } -// -// public void UpdateName(string name) -// { -// Guard.Against.NullOrWhiteSpace(name); -// Name = name; -// } -// -// public void UpdatePrice(Money price) -// { -// Guard.Against.Null(price); -// Guard.Against.ZeroOrNegative(price.Amount); -// Price = price; -// } -// -// public void UpdateSku(Sku sku) -// { -// Guard.Against.Null(sku); -// Sku = sku; -// } -// } \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Domain/Products/ProductByIdSpec.cs b/src/Modules/Orders/Modules.Orders.Domain/Products/ProductByIdSpec.cs deleted file mode 100644 index 6d18fef..0000000 --- a/src/Modules/Orders/Modules.Orders.Domain/Products/ProductByIdSpec.cs +++ /dev/null @@ -1,11 +0,0 @@ -// using Ardalis.Specification; -// -// namespace Modules.Orders.Domain.Products; -// -// public class ProductByIdSpec : Specification, ISingleResultSpecification -// { -// public ProductByIdSpec(ProductId id) : base() -// { -// Query.Where(i => i.Id == id); -// } -// } \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Domain/Products/ProductCreatedEvent.cs b/src/Modules/Orders/Modules.Orders.Domain/Products/ProductCreatedEvent.cs deleted file mode 100644 index e82fc13..0000000 --- a/src/Modules/Orders/Modules.Orders.Domain/Products/ProductCreatedEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -// using SharedKernel.Domain.Base; -// -// namespace Modules.Orders.Domain.Products; -// -// public record ProductCreatedEvent(ProductId Product, string ProductName) : DomainEvent -// { -// public static ProductCreatedEvent Create(Product product) => new(product.Id, product.Name); -// } \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Domain/Products/ProductId.cs b/src/Modules/Orders/Modules.Orders.Domain/Products/ProductId.cs deleted file mode 100644 index ead3449..0000000 --- a/src/Modules/Orders/Modules.Orders.Domain/Products/ProductId.cs +++ /dev/null @@ -1,3 +0,0 @@ -// namespace Modules.Orders.Domain.Products; -// -// public record ProductId(Guid Value); \ No newline at end of file diff --git a/src/Modules/Orders/Modules.Orders.Domain/Products/Sku.cs b/src/Modules/Orders/Modules.Orders.Domain/Products/Sku.cs deleted file mode 100644 index 74b9add..0000000 --- a/src/Modules/Orders/Modules.Orders.Domain/Products/Sku.cs +++ /dev/null @@ -1,21 +0,0 @@ -// namespace Modules.Orders.Domain.Products; -// -// public record Sku -// { -// private const int DefaultLength = 8; -// -// public string Value { get; } -// -// private Sku(string value) => Value = value; -// -// public static Sku? Create(string value) -// { -// if (string.IsNullOrWhiteSpace(value)) -// return null; -// -// if (value.Length != DefaultLength) -// return null; -// -// return new Sku(value); -// } -// } \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Categories/CategoryService.cs b/src/Modules/Warehouse/Modules.Warehouse.Application/Categories/CategoryService.cs new file mode 100644 index 0000000..bc70f3d --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Application/Categories/CategoryService.cs @@ -0,0 +1,19 @@ +using Modules.Warehouse.Application.Common.Interfaces; +using Modules.Warehouse.Domain.Categories; + +namespace Modules.Warehouse.Application.Categories; + +public class CategoryRepository : ICategoryRepository +{ + private readonly IWarehouseDbContext _dbContext; + + public CategoryRepository(IWarehouseDbContext dbContext) + { + _dbContext = dbContext; + } + + public bool CategoryExists(string categoryName) + { + return _dbContext.Categories.Any(c => c.Name == categoryName); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Interfaces/IWarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Interfaces/IWarehouseDbContext.cs new file mode 100644 index 0000000..bd326eb --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Application/Common/Interfaces/IWarehouseDbContext.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Domain.Categories; +using Modules.Warehouse.Domain.Products; + +namespace Modules.Warehouse.Application.Common.Interfaces; + +public interface IWarehouseDbContext +{ + public DbSet Categories { get; } + + public DbSet Products { get; } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Modules.Warehouse.Application.csproj b/src/Modules/Warehouse/Modules.Warehouse.Application/Modules.Warehouse.Application.csproj index bf7d303..a5e661e 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Application/Modules.Warehouse.Application.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Application/Modules.Warehouse.Application.csproj @@ -6,6 +6,15 @@ enable + + + + + + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Application/Products/Queries/GetProducts/GetProductsQuery.cs b/src/Modules/Warehouse/Modules.Warehouse.Application/Products/Queries/GetProducts/GetProductsQuery.cs new file mode 100644 index 0000000..66cd1c9 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Application/Products/Queries/GetProducts/GetProductsQuery.cs @@ -0,0 +1,26 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Application.Common.Interfaces; + +namespace Modules.Warehouse.Application.Products.Queries.GetProducts; + +public record GetProductsQuery : IRequest>; + +public record ProductDto(Guid Id, string Sku, string Name, decimal Price); + +public class GetProductsQueryHandler : IRequestHandler> +{ + private readonly IWarehouseDbContext _dbContext; + + public GetProductsQueryHandler(IWarehouseDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Handle(GetProductsQuery request, CancellationToken cancellationToken) + { + return await _dbContext.Products + .Select(p => new ProductDto(p.Id.Value, p.Sku.Value, p.Name, p.Price.Amount)) + .ToListAsync(cancellationToken: cancellationToken); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/Category.cs b/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/Category.cs index 12108e5..256b997 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/Category.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/Category.cs @@ -1,6 +1,6 @@ using Ardalis.GuardClauses; -using SharedKernel.Domain.Base; -using SharedKernel.Domain.Exceptions; +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Exceptions; namespace Modules.Warehouse.Domain.Categories; @@ -11,27 +11,27 @@ public class Category : AggregateRoot private Category() { } // NOTE: Need to use a factory, as EF does not let owned entities (i.e Money & Sku) be passed via the constructor - public static Category Create(string name, ICategoryService categoryService) + public static Category Create(string name, ICategoryRepository categoryRepository) { var category = new Category { Id = new CategoryId(Guid.NewGuid()), }; - category.UpdateName(name, categoryService); + category.UpdateName(name, categoryRepository); category.AddDomainEvent(new CategoryCreatedEvent(category.Id, category.Name)); return category; } - public void UpdateName(string name, ICategoryService categoryService) + public void UpdateName(string name, ICategoryRepository categoryRepository) { Guard.Against.NullOrWhiteSpace(name); - if (categoryService.CategoryExists(name)) + if (categoryRepository.CategoryExists(name)) throw new DomainException($"Category {name} already exists"); Name = name; } -} \ No newline at end of file +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryCreatedEvent.cs index cfbe648..ff8b930 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/CategoryCreatedEvent.cs @@ -1,4 +1,4 @@ -using SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Base; namespace Modules.Warehouse.Domain.Categories; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/ICategoryService.cs b/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/ICategoryService.cs index 8269d6f..172d046 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/ICategoryService.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Domain/Categories/ICategoryService.cs @@ -1,6 +1,6 @@ namespace Modules.Warehouse.Domain.Categories; -public interface ICategoryService +public interface ICategoryRepository { bool CategoryExists(string categoryName); } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Modules.Warehouse.Domain.csproj b/src/Modules/Warehouse/Modules.Warehouse.Domain/Modules.Warehouse.Domain.csproj index 21c4e65..422e651 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Modules.Warehouse.Domain.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Domain/Modules.Warehouse.Domain.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/LowStockEvent.cs b/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/LowStockEvent.cs new file mode 100644 index 0000000..66bf314 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/LowStockEvent.cs @@ -0,0 +1,6 @@ +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Identifiers; + +namespace Modules.Warehouse.Domain.Products; + +public record LowStockEvent(ProductId ProductId) : DomainEvent; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Product.cs b/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Product.cs index a1e4723..f04ba6d 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Product.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/Product.cs @@ -1,13 +1,16 @@ using Ardalis.GuardClauses; +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Entities; +using Common.SharedKernel.Domain.Exceptions; +using Common.SharedKernel.Domain.Identifiers; using Modules.Warehouse.Domain.Categories; -using SharedKernel.Domain.Base; -using SharedKernel.Domain.Entities; -using SharedKernel.Domain.Identifiers; namespace Modules.Warehouse.Domain.Products; public class Product : AggregateRoot { + private const int LowStockThreshold = 5; + public CategoryId CategoryId { get; set; } = null!; public Category Category { get; set; } = null!; @@ -18,7 +21,11 @@ public class Product : AggregateRoot public Sku Sku { get; private set; } = null!; - private Product() { } + public int StockOnHand { get; private set; } + + private Product() + { + } // NOTE: Need to use a factory, as EF does not let owned entities (i.e Money & Sku) be passed via the constructor public static Product Create(string name, Money price, Sku sku, CategoryId categoryId) @@ -35,7 +42,8 @@ public static Product Create(string name, Money price, Sku sku, CategoryId categ CategoryId = categoryId, Name = name, Price = price, - Sku = sku + Sku = sku, + StockOnHand = 0 }; product.AddDomainEvent(ProductCreatedEvent.Create(product)); @@ -61,4 +69,23 @@ public void UpdateSku(Sku sku) Guard.Against.Null(sku); Sku = sku; } + + public void RemoveStock(int quantity) + { + Guard.Against.NegativeOrZero(quantity); + + if (StockOnHand - quantity < 0) + throw new DomainException("Cannot adjust stock below zero"); + + StockOnHand -= quantity; + + if (StockOnHand <= LowStockThreshold) + AddDomainEvent(new LowStockEvent(Id)); + } + + public void AddStock(int quantity) + { + Guard.Against.NegativeOrZero(quantity); + StockOnHand += quantity; + } } diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductByIdSpec.cs b/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductByIdSpec.cs index ce73a33..920d3b0 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductByIdSpec.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductByIdSpec.cs @@ -1,5 +1,5 @@ using Ardalis.Specification; -using SharedKernel.Domain.Identifiers; +using Common.SharedKernel.Domain.Identifiers; namespace Modules.Warehouse.Domain.Products; diff --git a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductCreatedEvent.cs b/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductCreatedEvent.cs index def3d3f..f3f767b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductCreatedEvent.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Domain/Products/ProductCreatedEvent.cs @@ -1,9 +1,9 @@ -using SharedKernel.Domain.Base; -using SharedKernel.Domain.Identifiers; +using Common.SharedKernel.Domain.Base; +using Common.SharedKernel.Domain.Identifiers; namespace Modules.Warehouse.Domain.Products; public record ProductCreatedEvent(ProductId Product, string ProductName) : DomainEvent { public static ProductCreatedEvent Create(Product product) => new(product.Id, product.Name); -} \ No newline at end of file +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Extensions/EndpointRouteBuilderExt.cs b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Extensions/EndpointRouteBuilderExt.cs new file mode 100644 index 0000000..3e34c0a --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Extensions/EndpointRouteBuilderExt.cs @@ -0,0 +1,22 @@ +namespace Modules.Warehouse.Endpoints.Extensions; + +public static class EndpointRouteBuilderExt +{ + public static RouteHandlerBuilder ProducesGet(this RouteHandlerBuilder builder) => builder + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status500InternalServerError); + + public static RouteHandlerBuilder ProducesPost(this RouteHandlerBuilder builder) => builder + .Produces(StatusCodes.Status201Created) + .ProducesValidationProblem() + .ProducesProblem(StatusCodes.Status500InternalServerError); + + public static RouteHandlerBuilder ProducesPut(this RouteHandlerBuilder builder) => builder + .Produces(StatusCodes.Status204NoContent) + .ProducesValidationProblem() + .ProducesProblem(StatusCodes.Status500InternalServerError); + + public static RouteHandlerBuilder ProducesDelete(this RouteHandlerBuilder builder) => builder + .Produces(StatusCodes.Status204NoContent) + .ProducesProblem(StatusCodes.Status500InternalServerError); +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Modules.Warehouse.Endpoints.csproj b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Modules.Warehouse.Endpoints.csproj index 051954f..09a8133 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Modules.Warehouse.Endpoints.csproj +++ b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/Modules.Warehouse.Endpoints.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/ProductEndpoints.cs b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/ProductEndpoints.cs new file mode 100644 index 0000000..6954fe8 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/ProductEndpoints.cs @@ -0,0 +1,26 @@ +using MediatR; +using Modules.Warehouse.Application.Products.Queries.GetProducts; +using Modules.Warehouse.Endpoints.Extensions; + +namespace Modules.Warehouse.Endpoints; + +public static class ProductEndpoints +{ + public static void MapProductEndpoints(this WebApplication app) + { + var group = app + .MapGroup("api/products") + .WithTags("Warehouse") + .WithOpenApi(); + + group + .MapGet("/", async (ISender sender, CancellationToken ct) => await sender.Send(new GetProductsQuery(), ct)) + .WithName("GetProducts") + .ProducesGet(); + + // group + // .MapPost("/", async (ISender sender, CreateProductCommand command, CancellationToken ct) => await sender.Send(command, ct)) + // .WithName("CreateProduct") + // .ProducesPost(); + } +} \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/WarehouseModule.cs b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/WarehouseModule.cs index 512e679..efab61b 100644 --- a/src/Modules/Warehouse/Modules.Warehouse.Endpoints/WarehouseModule.cs +++ b/src/Modules/Warehouse/Modules.Warehouse.Endpoints/WarehouseModule.cs @@ -1,29 +1,26 @@ -namespace Modules.Warehouse.Endpoints; +using Modules.Warehouse.Infrastructure; +using Modules.Warehouse.Infrastructure.Persistence; -public static class WarhouseModule +namespace Modules.Warehouse.Endpoints; + +public static class WarehouseModule { - public static void AddWarehouseServices(this IServiceCollection services) + public static void AddWarehouseServices(this IServiceCollection services, IConfiguration configuration) { + services.AddInfrastructure(configuration); } - public static void UseWarehouseModule(this WebApplication app) + public static async Task UseWarehouseModule(this WebApplication app) { - app.MapGet("/api/products", () => - { - var products = Enumerable.Range(1, 5).Select(index => new ProductDto - ( - $"Product {index}", - $"Product {index} description", - index * 10.0m, - index * 10 - )); + if (app.Environment.IsDevelopment()) + { + // Initialise and seed database + using var scope = app.Services.CreateScope(); + var initializer = scope.ServiceProvider.GetRequiredService(); + await initializer.InitializeAsync(); + await initializer.SeedAsync(); + } - return products; - }) - .WithName("GetProducts") - .WithTags("Warehouse") - .WithOpenApi(); + app.MapProductEndpoints(); } } - -record ProductDto(string Name, string Description, decimal Price, int Quantity); \ No newline at end of file diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/DependencyInjection.cs b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..f7e42ad --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/DependencyInjection.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Modules.Warehouse.Application.Common.Interfaces; +using Modules.Warehouse.Infrastructure.Persistence; + +namespace Modules.Warehouse.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config) + { + var connectionString = config.GetConnectionString("DefaultConnection"); + services.AddDbContext(options => + options.UseSqlServer(connectionString, builder => + { + builder.MigrationsAssembly(typeof(DependencyInjection).Assembly.FullName); + builder.EnableRetryOnFailure(); + })); + + //services.AddSingleton(); + services.AddScoped(); + // services.AddScoped(); + // services.AddScoped(); + // services.AddScoped(); + + return services; + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Modules.Warehouse.Infrastructure.csproj b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Modules.Warehouse.Infrastructure.csproj new file mode 100644 index 0000000..c2477f7 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Modules.Warehouse.Infrastructure.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/CategoryConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/CategoryConfiguration.cs new file mode 100644 index 0000000..30757ff --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/CategoryConfiguration.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Warehouse.Domain.Categories; + +namespace Modules.Warehouse.Infrastructure.Persistence.Configurations; + +internal class CategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(p => p.Id); + + builder.Property(p => p.Id) + .HasConversion(categoryId => categoryId.Value, value => new CategoryId(value)); + + builder.Property(p => p.Name) + .HasMaxLength(50); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/MoneyConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/MoneyConfiguration.cs new file mode 100644 index 0000000..ead652f --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/MoneyConfiguration.cs @@ -0,0 +1,16 @@ +using Common.SharedKernel.Domain.Entities; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Modules.Warehouse.Infrastructure.Persistence.Configurations; + +internal static class MoneyConfiguration +{ + internal static void BuildAction(ComplexPropertyBuilder priceBuilder) + { + priceBuilder.Property(m => m.Currency) + .HasConversion(currency => currency.Symbol, value => new Currency(value)) + .HasMaxLength(3); + + priceBuilder.Property(m => m.Amount).HasPrecision(18, 2); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/ProductConfiguration.cs b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/ProductConfiguration.cs new file mode 100644 index 0000000..dd9a734 --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/Configurations/ProductConfiguration.cs @@ -0,0 +1,31 @@ +using Common.SharedKernel.Domain.Entities; +using Common.SharedKernel.Domain.Identifiers; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modules.Warehouse.Domain.Products; + +namespace Modules.Warehouse.Infrastructure.Persistence.Configurations; + +internal class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(p => p.Id); + + builder.Property(p => p.Id) + .HasConversion(productId => productId.Value, value => new ProductId(value)); + + builder.Property(p => p.Sku) + .HasConversion(sku => sku.Value, value => Sku.Create(value)!) + .HasMaxLength(50); + + builder.ComplexProperty(p => p.Price, MoneyConfiguration.BuildAction); + + //builder.ComplexProperty(p => p.Price, () => MoneyConfiguration.BuildAction) + + builder.HasOne(p => p.Category) + .WithMany() + .HasForeignKey(o => o.CategoryId) + .IsRequired(); + } +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContext.cs b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContext.cs new file mode 100644 index 0000000..62e1e0e --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContext.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using Modules.Warehouse.Application.Common.Interfaces; +using Modules.Warehouse.Domain.Categories; +using Modules.Warehouse.Domain.Products; + +namespace Modules.Warehouse.Infrastructure.Persistence; + +public class WarehouseDbContext : DbContext, IWarehouseDbContext +{ + // private readonly EntitySaveChangesInterceptor _saveChangesInterceptor; + // private readonly OutboxInterceptor _outboxInterceptor; + // + // public DbSet Products { get; set; } = default!; + // + // public DbSet Customers { get; set; } = default!; + // + // public DbSet Orders { get; set; } = default!; + // + // public DbSet OutboxMessages { get; set; } = default!; + // + + public DbSet Products => Set(); + public DbSet Categories => Set(); + + public WarehouseDbContext(DbContextOptions options /*EntitySaveChangesInterceptor saveChangesInterceptor, OutboxInterceptor outboxInterceptor*/) : base(options) + { + // _saveChangesInterceptor = saveChangesInterceptor; + // _outboxInterceptor = outboxInterceptor; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(typeof(WarehouseDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // optionsBuilder.AddInterceptors( + // _saveChangesInterceptor, + // _outboxInterceptor); + } + +} diff --git a/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContextInitializer.cs b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContextInitializer.cs new file mode 100644 index 0000000..c79dadc --- /dev/null +++ b/src/Modules/Warehouse/Modules.Warehouse.Infrastructure/Persistence/WarehouseDbContextInitializer.cs @@ -0,0 +1,114 @@ +using Bogus; +using Common.SharedKernel.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Modules.Warehouse.Application.Categories; +using Modules.Warehouse.Domain.Categories; +using Modules.Warehouse.Domain.Products; + +namespace Modules.Warehouse.Infrastructure.Persistence; + +public class WarehouseDbContextInitializer +{ + private readonly ILogger _logger; + private readonly WarehouseDbContext _dbContext; + + private const int NumProducts = 20; + private const int NumCategories = 5; + // private const int NumCustomers = 20; + // private const int NumOrders = 20; + + public WarehouseDbContextInitializer(ILogger logger, WarehouseDbContext dbContext) + { + _logger = logger; + _dbContext = dbContext; + } + + public async Task InitializeAsync() + { + try + { + if (_dbContext.Database.IsSqlServer()) + { + //await _dbContext.Database.EnsureDeletedAsync(); + await _dbContext.Database.EnsureCreatedAsync(); + } + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while migrating or initializing the database"); + throw; + } + } + + public async Task SeedAsync() + { + await SeedCategoriesAsync(); + await SeedProductsAsync(); + // await SeedCustomersAsync(); + // await SeedOrdersAsync(); + } + + // private async Task SeedCustomersAsync() + // { + // if (await _dbContext.Customers.AnyAsync()) + // return; + // + // var customerFaker = new Faker() + // .CustomInstantiator(f => Customer.Create(f.Person.Email, f.Person.FirstName, f.Person.LastName)); + // + // var customers = customerFaker.Generate(NumCustomers); + // _dbContext.Customers.AddRange(customers); + // await _dbContext.SaveChangesAsync(); + // } + + private async Task SeedProductsAsync() + { + if (await _dbContext.Products.AnyAsync()) + return; + + var categories = await _dbContext.Categories.ToListAsync(); + + var moneyFaker = new Faker() + .CustomInstantiator(f => new Money(f.PickRandom(Currency.Currencies), f.Finance.Amount())); + + var skuFaker = new Faker() + .CustomInstantiator(f => Sku.Create(f.Commerce.Ean8())!); + + var faker = new Faker() + .CustomInstantiator(f => Product.Create(f.Commerce.ProductName(), moneyFaker.Generate(), + skuFaker.Generate(), f.PickRandom(categories).Id)); + + var products = faker.Generate(NumProducts); + _dbContext.Products.AddRange(products); + await _dbContext.SaveChangesAsync(); + } + + // private async Task SeedOrdersAsync() + // { + // if (await _dbContext.Orders.AnyAsync()) + // return; + // + // var customerIds = _dbContext.Customers.Select(c => c.Id).ToList(); + // + // var orderFaker = new Faker() + // .CustomInstantiator(f => Order.Create(f.PickRandom(customerIds))); + // + // var orders = orderFaker.Generate(NumOrders); + // _dbContext.Orders.AddRange(orders); + // await _dbContext.SaveChangesAsync(); + // } + + private async Task SeedCategoriesAsync() + { + if (await _dbContext.Categories.AnyAsync()) + return; + + var categoryFaker = new Faker() + .CustomInstantiator(f => Category.Create(f.Commerce.Categories(1)[0], new CategoryRepository(_dbContext))); + + var categories = categoryFaker.Generate(NumCategories); + _dbContext.Categories.AddRange(categories); + await _dbContext.SaveChangesAsync(); + } +} diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 8f8f3d3..e351a2b 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -9,7 +9,7 @@ builder.Services.AddSwaggerGen(); builder.Services.AddOrdersServices(); -builder.Services.AddWarehouseServices(); +builder.Services.AddWarehouseServices(builder.Configuration); var app = builder.Build(); @@ -23,6 +23,6 @@ app.UseHttpsRedirection(); app.UseOrdersModule(); -app.UseWarehouseModule(); +await app.UseWarehouseModule(); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index 6a52436..aace12e 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -4,8 +4,9 @@ net8.0 enable enable - true + Web + 992be0e9-0906-4010-8fca-76a03c001d19