diff --git a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs new file mode 100644 index 0000000..1a0826a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs @@ -0,0 +1,81 @@ +using LiteCharms.Features.MidrandBooks.Authors; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + private readonly AuthorService authorService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateAuthorAsync_ShouldReturn_ResultWithAuthorId() + { + var request = new CreateAuthor + { + Name = "John", + LastName = "Doe", + Company = "Solo Publishers", + Email = "solo@publishers.co.za", + PublisherType = PublisherTypes.Independent, + ImageUrl = "" + }; + + var result = await authorService.CreateAuthorAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task UpdateAuthorAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateAuthor + { + Name = "Jane", + LastName = "Doe", + Company = "Solo Publishers", + Email = "solo@publishers.co.za", + PublisherType = PublisherTypes.Independent, + ImageUrl = "" + }; + + var result = await authorService.UpdateAuthorAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetAuthors_ShouldReturn_ResultWithAuthorList() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await authorService.GetAuthorsAsync(range, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetAuthorAsync_ShouldReturn_ResultWithAuthor() + { + var result = await authorService.GetAuthorAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task UpdateAuthorStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await authorService.UpdateAuthorStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs new file mode 100644 index 0000000..d65e0d6 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs @@ -0,0 +1,52 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class BooksServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly BooksService bookService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateBookAsync_ShouldReturn_ResultWithBookId() + { + var result = await bookService.CreateBookAsync(1, 2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetBookAsync_ShouldReturn_ResultWithBook() + { + var result = await bookService.GetBookAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetBooksByAuthorAsync_ShouldReturn_ResultWithAuthorBooks() + { + var result = await bookService.GetBooksByAuthorAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetPublishedBooksAsync_ShouldReturn_ResultWithBublishedBooks() + { + var result = await bookService.GetPublishedBooksAsync(0, 1000, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task UpdateBookStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await bookService.UpdateBookStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs similarity index 78% rename from LiteCharms.Features.MidrandBooks.Tests/Fixture.cs rename to LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs index 38c8bd7..6948d48 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs @@ -1,7 +1,8 @@ using LiteCharms.Features.Extensions; +using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; -namespace LiteCharms.Features.MidrandBooks.Tests; +namespace LiteCharms.Features.MidrandBooks.Tests.Common; public class Fixture : IDisposable { @@ -11,6 +12,10 @@ public class Fixture : IDisposable public IMediator Mediator { get; set; } + private readonly CancellationTokenSource cancellationTokenSource = new(); + + public CancellationToken CancellationToken => cancellationTokenSource.Token; + public Fixture() { Configuration = new ConfigurationBuilder() @@ -23,12 +28,12 @@ public class Fixture : IDisposable Services = new ServiceCollection() .AddMediator() .AddLogging() - //.AddMidrandShopServices() .AddEmailServiceBus() .AddGarageS3(Configuration) .AddMidrandShopDatabase(Configuration) .AddEmailServices(Configuration) .AddSingleton(Configuration) + .AddShopServices() .BuildServiceProvider(); Mediator = Services.GetRequiredService(); diff --git a/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs new file mode 100644 index 0000000..304f78e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.MidrandBooks.Tests.Common; + +public class IntegrationFactAttribute : FactAttribute +{ + public IntegrationFactAttribute() + { + if(!Debugger.IsAttached) + Skip = "This test requires the debugger to be attached."; + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs new file mode 100644 index 0000000..5edea77 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs @@ -0,0 +1,201 @@ +using LiteCharms.Features.MidrandBooks.Customers; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class CustomerServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly CustomerService customerService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateCustomerAsync_ShouldReturn_ResultWithCustomerId() + { + var request = new CreateCustomer + { + Company = "Book Lovers", + Email = "hank@booklovers.com", + Phone = "555 1245 8577", + Website = "https://www.booklovers.com" + }; + + var result = await customerService.CreateCustomerAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CreateCustomerContactAsync_ShouldReturn_ResultWithCustomerContactId() + { + var request = new CreateCustomerContact + { + Name = "Sipho", + LastName = "Madlanga", + Phone = "0710857365", + Email = "sipho@madlanga.africa", + Type = ContactTypes.Business + }; + + var result = await customerService.CreateCustomerContactAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CreateCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddressId() + { + var request = new CreateCustomerAddress + { + Name = "Business", + BuildingType = AddressBuildingTypes.MixedUse, + Type = AddressType.Shipping, + Street = "123 Building 4, XYZ Suburb, Some Region", + City = "Johannesburg", + State = "Gauteng", + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = "12345" + }; + + var result = await customerService.CreateCustomerAddressAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task UpdateCustomerAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomer + { + Company = "Book Lovers", + Email = "hank@booklovers.com", + Phone = "555 1245 8578", + Website = "https://www.booklovers.com" + }; + + var result = await customerService.UpdateCustomerAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerContactAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomerContact + { + Name = "Sipho", + LastName = "Madlanga", + Phone = "0710857366", + Email = "sipho@madlanga.africa", + Type = ContactTypes.Business + }; + + var result = await customerService.UpdateCustomerContactAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerAddressAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomerAddress + { + Name = "Business", + BuildingType = AddressBuildingTypes.MixedUse, + Type = AddressType.Shipping, + Street = "123 Building 4, XYZ Suburb, Some Region", + City = "Johannesburg", + State = "Gauteng", + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = "12346" + }; + + var result = await customerService.UpdateCustomerAddressAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerContactStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerContactStatusAsync(1, true, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerAddressStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerAddressStatusAsync(1, true, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetCustomersAsync_ShouldReturn_ResultWithCustomerList() + { + var result = await customerService.GetCustomersAsync(fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerContactsAsync_ShouldReturn_ResultWithCustomerContactList() + { + var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAddressesAsync_ShouldReturn_ResultWithCustomerAddressList() + { + var result = await customerService.GetCustomerAddressesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAsync_ShouldReturn_ResultWithCustomer() + { + var result = await customerService.GetCustomerAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerContactAsync_ShouldReturn_ResultWithCustomerContact() + { + var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddress() + { + var result = await customerService.GetCustomerAddressAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj index 824c4ff..3b0e796 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -39,6 +39,8 @@ + + diff --git a/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs new file mode 100644 index 0000000..ef09044 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs @@ -0,0 +1,196 @@ +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Orders.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class OrderServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly OrderService orderService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateOrderAsync_ShouldReturn_ResultWithOrderId() + { + var request = new CreateOrder(250, "At the intercomm, dial 1 then option 2"); + + var result = await orderService.CreateOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AddItemToOrderAsync_ShouldReturn_ResultWithOrderItemId() + { + var request = new CreateOrderItem(1, 1, 2); + + var result = await orderService.AddItemToOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AddItemsToOrderAsync_ShouldReturn_ResultWithSuccess() + { + var requests = new List + { + new(1, 1, 1), + new(1, 1, 3) + }; + + var result = await orderService.AddItemsToOrderAsync(1, [.. requests], fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task RemoveItemFromOrderAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.RemoveItemFromOrderAsync(1, 5, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task ClearOrderItemsAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.ClearOrderItemsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CancelOrderAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.CancelOrderAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetOrderAsync_ShouldReturn_ResultWithOrder() + { + var result = await orderService.GetOrderAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetOrdersByCustomerAsync_ShouldReturn_ResultWithOrderList() + { + var result = await orderService.GetOrdersByCustomerAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetOrdersAsync_ShouldReturn_ResultWithOrderList() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await orderService.GetOrdersAsync(range, 0, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task UpdateOrderStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.UpdateOrderStatusAsync(1, OrderStatus.Pending, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task AddShippingToOrderAsync_ShouldReturn_ResultWithSuccess() + { + var request = new CreateShipping(1, 2); + + var result = await orderService.AddShippingToOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task UpdateShippingStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.UpdateShippingStatusAsync(1, ShippingStatuses.Shipped, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetShippingByOrderIdAsync_ShouldReturn_ResultWithShipping() + { + var result = await orderService.GetShippingByOrderIdAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task RemoveShippingFromOrderAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.RemoveShippingFromOrderAsync(1, 1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateShippingTrackingNumberAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.UpdateShippingTrackingNumberAsync(1, 2, "NA0009969397"); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithShippingProviderId() + { + var request = new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel"); + + var result = await orderService.CreateShippingProviderAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task GetShippingProvidersAsync_ShouldReturn_ResultWithShippingProviderList() + { + var result = await orderService.GetShippingProvidersAsync(true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetShippingProviderAsync_ShouldReturn_ResultWithShippingProvider() + { + var result = await orderService.GetShippingProviderAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task UpdateShippingProviderAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateShippingProvider(2,true, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel"); + + var result = await orderService.UpdateShippingProviderAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs new file mode 100644 index 0000000..a98c368 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs @@ -0,0 +1,63 @@ +using LiteCharms.Features.MidrandBooks.Pages; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class PageServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PageService pageService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreatePageAsync_ShouldReturn_ResultWithPageId() + { + + } + + [IntegrationFact] + public async Task GetPagesAsync_ByBookId_ShouldReturn_ResultWithPageList() + { + + } + + [IntegrationFact] + public async Task GetPageAsync_ShouldReturn_ResultWithPage() + { + + } + + [IntegrationFact] + public async Task GetPageByNumberAsync_ById_And_BookPageNumber_ShouldReturn_ResultWithPage() + { + + } + + [IntegrationFact] + public async Task UpdatePageAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeletePageAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeleteByPageTypeAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeleteAllAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task UpdatePageStatusAsync_ShouldReturn_ResultWithSuccess() + { + + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs new file mode 100644 index 0000000..6dc7e88 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -0,0 +1,143 @@ +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + private readonly ProductService productService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task GetProductPriceAsync_ShouldReturn_RetultOneProductPrice() + { + var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task GetProductPricesAsync_ShouldReturn_RetultProductPriceList() + { + var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task SearchProductsAsync_ShouldReturn_RetultMatchingProducts() + { + var filter = new ProductFilter + { + Name = "system", + Manufacturer = "techwave", + SerialNumber = "2024", + MinPrice = 10, + MaxPrice = 30 + }; + + var result = await productService.SearchProductsAsync(filter, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task GetProductAsync_ShouldReturn_RetultOneProduct() + { + var result = await productService.GetProductAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task GetProductsAsync_ShouldReturn_RetultProducts() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await productService.GetProductsAsync(0, range, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue() + { + var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateProductPriceStatusAsync_ShouldReturn_ResultTrue() + { + var result = await productService.UpdateProductPriceStatusAsync(2, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateProductPriceAsync_Should_Return_NewProductPriceId() + { + var request = new CreateProductPrice + { + Amount = 29.99m, + Discount = 0.00m + }; + + var result = await productService.CreateProductPriceAsync(2, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess, "Product price creation should be successful."); + Assert.True(result.Value > 0, "New ProductPriceId should be greater than 0."); + + output.WriteLine($"Created ProductPriceId: {result.Value}"); + } + + [IntegrationFact] + public async Task CreateProductAsync_Result_Returns_ProductId() + { + var request = new CreateProduct + { + Name = "Systems Rewired", + Description = "[Design], , AND /CHAORS/ IN ***SYNC***", + Summary = "A comprehensive guide to systems thinking and design.", + ImageUrl = "https://bookshop.cdn.khongisa.co.za/design/2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp", + Type = ProductTypes.Book, + Categories = ["Systems Thinking", "Design", "Programming"], + Metadata = new ProductMetadata + { + CopyrightInfo = "© 2024 John Doe. All rights reserved.", + ManufactureDate = "2024-06-01", + Manufacturer = "TechWave Publishing", + SerialNumber = "SR-2024-0001" + } + }; + + var result = await productService.CreateProductAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess, "Product creation should be successful."); + Assert.True(result.Value > 0, "ProductId should be greater than 0."); + + output.WriteLine($"Created ProductId: {result.Value}"); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json index 1066af9..7f9a6b8 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json @@ -5,12 +5,6 @@ "BucketName": "bookshop", "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" }, - "BookshopQuotesS3Settings": { - "ServiceUrl": "http://192.168.1.177:30900", - "Region": "garage", - "BucketName": "bookshop.quotes", - "CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za" - }, "Email": { "Credentials": { "Username": "shop@litecharms.co.za" diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 81ff39f..4375d8e 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -7,23 +7,21 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks; public sealed class BooksService(IDbContextFactory contextFactory) : IService { - public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) + public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var book = await context.Books.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); + var rowsUpdated = await context.Books + .Where(b => b.Id == bookId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(b => b.Enabled, isEnabled) + .SetProperty(b => b.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (book is null) - return Result.Fail(new Error($"Book with ID {bookId} not found")); - - book.UpdatedAt = DateTime.UtcNow; - book.Enabled = isEnabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error($"Failed to change status of book with ID {bookId}")); + : Result.Fail(new Error($"Book with ID {bookId} not found")); } catch (Exception ex) { @@ -45,8 +43,9 @@ public sealed class BooksService(IDbContextFactory contex var book = context.Books.Add(new Entities.AuthorBook { + CreatedAt = DateTime.UtcNow, AuthorId = authorId, - ProductId = productId, + ProductId = productId }); return await context.SaveChangesAsync(cancellationToken) > 0 @@ -68,7 +67,8 @@ public sealed class BooksService(IDbContextFactory contex var book = await context.Books .AsNoTracking() .Include(b => b.Author) - .Include(b => b.Product!.Price) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) .Include(b => b.Pages) .FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); @@ -86,7 +86,7 @@ public sealed class BooksService(IDbContextFactory contex { try { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken)) return Result.Fail(new Error($"Author with ID {authorId} not found")); @@ -94,7 +94,8 @@ public sealed class BooksService(IDbContextFactory contex var books = await context.Books .AsNoTracking() .Include(b => b.Author) - .Include(b => b.Product!.Price) + .Include(b => b.Product) + .ThenInclude(b => b.Prices) .OrderByDescending(b => b.CreatedAt) .Where(b => b.AuthorId == authorId) .ToListAsync(cancellationToken); @@ -118,9 +119,10 @@ public sealed class BooksService(IDbContextFactory contex var books = await context.Books .AsNoTracking() .Include(b => b.Author) - .Include(b => b.Product!.Price) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) .Include(b => b.Pages) - .Where(b => b.Enabled && b.Product!.Enabled && b.Author.Enabled) + .Where(b => b.Enabled && b.Product!.Enabled && b.Author!.Enabled) .OrderByDescending(b => b.Ranking) .ThenByDescending(b => b.Ranking) .ThenByDescending(b => b.CreatedAt) diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs index c1282e8..2b6b906 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs @@ -6,7 +6,7 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; public class AuthorBook : Models.AuthorBook { - public virtual Author Author { get; set; } = new(); + public virtual Author? Author { get; set; } public new virtual Product? Product { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index 2f71d02..c1aa6b3 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -1,5 +1,4 @@ using LiteCharms.Features.MidrandBooks.Abstractions; -using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; @@ -9,53 +8,21 @@ namespace LiteCharms.Features.MidrandBooks.Authors; public sealed class AuthorService(IDbContextFactory contextFactory) : IService { - public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) + public async ValueTask UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + var rowsUpdated = await context.Authors + .Where(a => a.Id == authorId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Enabled, isEnabled) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (author is null) - return Result.Fail(new Error($"Author with ID {authorId} not found")); - - var books = await context.Books - .AsNoTracking() - .Include(b => b.Author) - .Include(b => b.Product!.Price) - .OrderByDescending(b => b.CreatedAt) - .Where(p => p.AuthorId == authorId) - .AsSplitQuery() - .ToArrayAsync(cancellationToken); - - return books?.Length > 0 - ? Result.Ok(books.Select(b => b.ToModel()).ToArray()) - : Result.Fail(new Error($"No books found for author with ID {authorId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } - - public async ValueTask UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken) - { - try - { - await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); - - if (author is null) - return Result.Fail(new Error($"Author with ID {authorId} not found")); - - author.UpdatedAt = DateTime.UtcNow; - author.Enabled = isEnabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error($"Failed to change status of author with ID {authorId}")); + : Result.Fail(new Error($"Author with ID {authorId} not found")); } catch (Exception ex) { @@ -81,12 +48,12 @@ public sealed class AuthorService(IDbContextFactory conte } } - public async ValueTask> GetAuthors(DateRange range, CancellationToken cancellationToken) + public async ValueTask> GetAuthorsAsync(DateRange range, CancellationToken cancellationToken = default) { try { - var fromDate = range.From.ToDateTime(TimeOnly.MinValue); - var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -107,18 +74,12 @@ public sealed class AuthorService(IDbContextFactory conte } } - public async ValueTask UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken) + public async ValueTask UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) - return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists")); - - if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) - return Result.Fail(new Error($"An author with the email {request.Email} already exists")); - var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); if (author is null) @@ -151,12 +112,12 @@ public sealed class AuthorService(IDbContextFactory conte { try { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if(await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) + if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists")); - if(await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) + if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) return Result.Fail(new Error($"An author with the email {request.Email} already exists")); var newAuthor = context.Authors.Add(new Entities.Author diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs index 0c0af10..8a7de14 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs @@ -18,7 +18,8 @@ public sealed class AuthorConfiguration : IEntityTypeConfiguration builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024); builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048); builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048); - builder.Property(f => f.SocialMedia).IsRequired(false).HasColumnType("jsonb"); builder.Property(f => f.Enabled).HasDefaultValue(true); + + builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); } } diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs index 0546a7b..c6080f1 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs @@ -30,7 +30,7 @@ public class Author public string? ThumbnailImageUrl { get; set; } - public SocialMedia[]? SocialMedia { get; set; } + public ICollection? SocialMedia { get; set; } public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs index d73292d..03f34c0 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -13,7 +13,7 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if (await context.Customers.AnyAsync(c => c.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) + if (await context.Customers.AnyAsync(c => EF.Functions.ILike(c.Email!, $"%{request.Email}%"), cancellationToken)) return Result.Fail(new Error($"Customer with email '{request.Email}' already exists.")); var customer = context.Customers.Add(new Entities.Customer @@ -46,7 +46,7 @@ public sealed class CustomerService(IDbContextFactory con if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); - if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && cc.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) + if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && EF.Functions.ILike(cc.Email!, $"%{request.Email}%"), cancellationToken)) return Result.Fail(new Error($"Contact with email '{request.Email}' already exists for this customer.")); var contact = context.Contacts.Add(new Entities.Contact @@ -139,21 +139,19 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + var rowsUpdated = await context.Contacts + .Where(cc => cc.Id == contactId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(cc => cc.Name, request.Name) + .SetProperty(cc => cc.LastName, request.LastName) + .SetProperty(cc => cc.Email, request.Email) + .SetProperty(cc => cc.Phone, request.Phone) + .SetProperty(cc => cc.Type, request.Type) + .SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (contact is null) - return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); - - contact.UpdatedAt = DateTime.UtcNow; - contact.Name = request.Name; - contact.LastName = request.LastName; - contact.Email = request.Email; - contact.Phone = request.Phone; - contact.Type = request.Type; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer contact.")); + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); } catch (Exception ex) { @@ -167,25 +165,23 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + var rowsUpdated = await context.Addresses + .Where(a => a.Id == addressId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Street, request.Street) + .SetProperty(a => a.City, request.City) + .SetProperty(a => a.State, request.State) + .SetProperty(a => a.PostalCode, request.PostalCode) + .SetProperty(a => a.Country, request.Country) + .SetProperty(a => a.Type, request.Type) + .SetProperty(a => a.BuildingType, request.BuildingType) + .SetProperty(a => a.IsPrimary, request.IsPrimary) + .SetProperty(a => a.Name, request.Name) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (address is null) - return Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); - - address.UpdatedAt = DateTime.UtcNow; - address.Street = request.Street; - address.City = request.City; - address.State = request.State; - address.PostalCode = request.PostalCode; - address.Country = request.Country; - address.Type = request.Type; - address.BuildingType = request.BuildingType; - address.IsPrimary = request.IsPrimary; - address.Name = request.Name; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer address.")); + : Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); } catch (Exception ex) { @@ -199,17 +195,15 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + var rowsUpdated = await context.Customers + .Where(c => c.Id == customerId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.Enabled, enabled) + .SetProperty(c => c.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (customer is null) - return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); - - customer.Enabled = enabled; - customer.UpdatedAt = DateTime.UtcNow; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer status.")); + : Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); } catch (Exception ex) { @@ -223,18 +217,16 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + var rowsUpdated = await context.Contacts + .Where(cc => cc.Id == contactId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(cc => cc.Enabled, enabled) + .SetProperty(cc => cc.IsPrimary, isPrimary) + .SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (contact is null) - return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); - - contact.Enabled = enabled; - contact.IsPrimary = isPrimary; - contact.UpdatedAt = DateTime.UtcNow; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer contact status.")); + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); } catch (Exception ex) { @@ -248,18 +240,16 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + var rowsUpdated = await context.Addresses + .Where(a => a.Id == addressId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Enabled, enabled) + .SetProperty(a => a.IsPrimary, isPrimary) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (address is null) - return Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); - - address.Enabled = enabled; - address.IsPrimary = isPrimary; - address.UpdatedAt = DateTime.UtcNow; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer address status.")); + : Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); } catch (Exception ex) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs index f5015a7..b68c086 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs @@ -14,7 +14,8 @@ public sealed class CustomerConfiguration : IEntityTypeConfiguration builder.Property(c => c.Email).IsRequired(); builder.Property(c => c.Phone).IsRequired(); builder.Property(c => c.Website).IsRequired(); - builder.Property(c => c.SocialMedia).IsRequired(false).HasColumnType("jsonb"); builder.Property(c => c.Enabled).HasDefaultValue(true); + + builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); } } diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs index 13204ce..9cf8f7e 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs @@ -20,7 +20,7 @@ public class Customer public string? Phone { get; set; } - public SocialMedia[]? SocialMedia { get; set; } + public ICollection? SocialMedia { get; set; } public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs index 10c8dae..0383187 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs @@ -14,7 +14,7 @@ public record CreateCustomer public string? Phone { get; set; } - public SocialMedia[]? SocialMedia { get; set; } + public ICollection? SocialMedia { get; set; } } public sealed record UpdateCustomer : CreateCustomer; diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 72e57d5..753bcc7 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -17,7 +17,8 @@ public static class Mappers Name = entity.Name, Type = entity.Type, Price = entity.Price, - Enabled = entity.Enabled + Enabled = entity.Enabled, + TrackingUrl = entity.TrackingUrl, }; public static Shipping ToModel(this Orders.Entities.Shipping entity) => new() diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs index 228634d..54307dc 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs @@ -8,8 +8,18 @@ public static class Postgres public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) { + var connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); + + dataSourceBuilder.ConfigureTypeLoading(options => { options.EnableTypeLoading(false); }); + + var dataSource = dataSourceBuilder.Build(); + + services.AddSingleton(dataSource); + services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName))); + options.UseNpgsql(dataSource)); return services; } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index 43e407e..f407c5b 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -4,19 +4,15 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Shop { - public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime) + public static IServiceCollection AddShopServices(this IServiceCollection services) { var serviceType = typeof(IService); - var implementations = assembly.GetTypes() + var implementations = Assembly.GetExecutingAssembly().GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); - foreach (var implementation in implementations) - { - var descriptor = new ServiceDescriptor(serviceType, implementation, serviceLifetime); - - services.Add(descriptor); - } + foreach (var implementation in implementations) + services.AddScoped(implementation); return services; } diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 17db6dd..82d3a1b 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -162,5 +162,8 @@ + + + diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs index c7ae590..629b030 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs @@ -11,7 +11,7 @@ public sealed class OrderConfiguration : IEntityTypeConfiguration builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); builder.Property(o => o.Status).IsRequired(); - builder.Property(o => o.Total).IsRequired().HasColumnType("decimal(18,2)"); + builder.Property(o => o.Total).IsRequired().HasPrecision(18, 2); builder.Property(o => o.Notes).HasMaxLength(1000); } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs index f5453bc..6ffccb4 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -1,10 +1,10 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Models; -public sealed record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); +public sealed record CreateOrder(decimal TotalPrice, string? Notes); public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); -public sealed record CreateShipping(long OrderId, long AddressId, long ShippingProviderId, string? TrackingNumber); +public sealed record CreateShipping(long AddressId, long ShippingProviderId, string? TrackingNumber = null); public sealed record CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl); diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index c15b2bf..290908b 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -49,6 +49,17 @@ public sealed class OrderService(IDbContextFactory contex if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken)) return Result.Fail("Product price not found."); + var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == request.ProductPriceId && i.OrderId == orderId, cancellationToken); + + if(existingItem is not null) + { + existingItem.Quantity += request.Quantity; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(existingItem.Id) + : Result.Fail("Update existing order item."); + } + var orderItem = context.OrderItems.Add(new Entities.OrderItem { OrderId = orderId, @@ -78,9 +89,6 @@ public sealed class OrderService(IDbContextFactory contex if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) return Result.Fail("Order not found."); - - var existingItems = context.OrderItems.Where(oi => oi.OrderId == orderId); - context.OrderItems.RemoveRange(existingItems); foreach (var item in items) { @@ -90,18 +98,23 @@ public sealed class OrderService(IDbContextFactory contex if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken)) return Result.Fail($"Product price with ID {item.ProductPriceId} not found."); - context.OrderItems.Add(new Entities.OrderItem - { - OrderId = orderId, - AuthorBookId = item.AuthorBookId, - ProductPriceId = item.ProductPriceId, - Quantity = item.Quantity - }); + var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == item.ProductPriceId && i.OrderId == orderId, cancellationToken); + + if (existingItem is not null) + existingItem.Quantity += item.Quantity; + else + context.OrderItems.Add(new Entities.OrderItem + { + OrderId = orderId, + AuthorBookId = item.AuthorBookId, + ProductPriceId = item.ProductPriceId, + Quantity = item.Quantity + }); + + await context.SaveChangesAsync(cancellationToken); } - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail("Failed to add items to order."); + return Result.Ok(); } catch (Exception ex) { @@ -115,16 +128,13 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var orderItem = await context.OrderItems.FirstOrDefaultAsync(oi => oi.Id == orderItemId && oi.OrderId == orderId, cancellationToken); + var rowsDeleted = await context.OrderItems + .Where(oi => oi.Id == orderItemId && oi.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); - if (orderItem is null) - return Result.Fail("Order item not found."); - - context.OrderItems.Remove(orderItem); - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsDeleted > 0 ? Result.Ok() - : Result.Fail("Failed to remove item from order."); + : Result.Fail("Order item not found or failed to remove."); } catch (Exception ex) { @@ -132,15 +142,14 @@ public sealed class OrderService(IDbContextFactory contex } } - public async ValueTask ClearOrderItemasAsync(long orderId, CancellationToken cancellationToken = default) + public async ValueTask ClearOrderItemsAsync(long orderId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var orderItems = context.OrderItems.Where(oi => oi.OrderId == orderId); - - context.OrderItems.RemoveRange(orderItems); + var deletedItems = await context.OrderItems.Where(oi => oi.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() @@ -199,12 +208,15 @@ public sealed class OrderService(IDbContextFactory contex { try { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var orders = await context.Orders .AsNoTracking() - .Where(o => o.CreatedAt >= range.From.ToDateTime(TimeOnly.MinValue) && o.CreatedAt <= range.To.ToDateTime(TimeOnly.MaxValue)) - .Skip(index * range.MaxRecords) + .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) + .Skip(index).Take(range.MaxRecords) .ToListAsync(cancellationToken); return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); @@ -221,17 +233,15 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + var rowsUpdated = await context.Orders + .Where(o => o.Id == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(o => o.Status, newStatus) + .SetProperty(o => o.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (order is null) - return Result.Fail("Order not found."); - - order.UpdatedAt = DateTime.UtcNow; - order.Status = newStatus; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update order status."); + : Result.Fail("Order not found or status update failed."); } catch (Exception ex) { @@ -239,7 +249,7 @@ public sealed class OrderService(IDbContextFactory contex } } - public async ValueTask AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) + public async ValueTask> AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) { try { @@ -282,17 +292,16 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken); + var rowsUpdated = await context.Shippings + .Where(s => s.OrderId == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(s => s.Status, newStatus) + .SetProperty(s => s.UpdatedAt, DateTime.UtcNow), + cancellationToken); - if (shipping is null) - return Result.Fail("Shipping not found for this order."); - - shipping.UpdatedAt = DateTime.UtcNow; - shipping.Status = newStatus; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update shipping status."); + : Result.Fail("Shipping not found for this order or status update failed."); } catch (Exception ex) { @@ -325,21 +334,14 @@ public sealed class OrderService(IDbContextFactory contex try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.Shippings + .Where(s => s.Id == shippingId && s.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); - if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) - return Result.Fail("Order not found."); - - var shipping = await context.Shippings.AsNoTracking() - .FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken); - - if (shipping is null) - return Result.Fail("Shipping not found for this order."); - - context.Shippings.Remove(shipping); - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsDeleted > 0 ? Result.Ok() - : Result.Fail("Failed to remove shipping from order."); + : Result.Fail("Shipping record not found for this order."); } catch (Exception ex) { @@ -353,17 +355,15 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken); + var rowsUpdated = await context.Shippings + .Where(s => s.Id == shippingId && s.OrderId == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(s => s.TrackingNumber, trackingNumber) + .SetProperty(s => s.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (shipping is null) - return Result.Fail("Shipping not found for this order."); - - shipping.UpdatedAt = DateTime.UtcNow; - shipping.TrackingNumber = trackingNumber; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update shipping tracking number."); + : Result.Fail("Shipping record not found for this order."); } catch (Exception ex) { @@ -371,7 +371,7 @@ public sealed class OrderService(IDbContextFactory contex } } - public async ValueTask CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) + public async ValueTask> CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) { try { @@ -382,6 +382,7 @@ public sealed class OrderService(IDbContextFactory contex var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider { + CreatedAt = DateTime.UtcNow, Name = request.Name, Type = request.Type, Price = request.Price, @@ -389,7 +390,7 @@ public sealed class OrderService(IDbContextFactory contex }); return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() + ? Result.Ok(shippingProvider.Entity.Id) : Result.Fail("Failed to create shipping provider."); } catch (Exception ex) @@ -440,24 +441,22 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var provider = await context.ShippingProviders.FirstOrDefaultAsync(sp => sp.Id == request.ProviderId, cancellationToken); + var rowsUpdated = await context.ShippingProviders + .Where(sp => sp.Id == request.ProviderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(sp => sp.Name, request.Name) + .SetProperty(sp => sp.Price, request.Price) + .SetProperty(sp => sp.TrackingUrl, request.TrackingUrl) + .SetProperty(sp => sp.Enabled, request.Enabled) + .SetProperty(sp => sp.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (provider is null) - return Result.Fail("Shipping provider not found."); - - provider.UpdatedAt = DateTime.UtcNow; - provider.Enabled = request.Enabled; - provider.Name = request.Name; - provider.Price = request.Price; - provider.TrackingUrl = request.TrackingUrl; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update shipping provider status."); + : Result.Fail("Shipping provider not found."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } - } + } } diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs index d6c9fed..eb33f55 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs @@ -13,10 +13,11 @@ public sealed class BookPageConfiguration : IEntityTypeConfiguration builder.Property(bp => bp.AuthorBookId).IsRequired(); builder.Property(bp => bp.Content).IsRequired(); builder.Property(bp => bp.Type).IsRequired(); - builder.Property(bp => bp.ContentType).IsRequired(); - builder.Property(bp => bp.Notes).IsRequired(false).HasColumnType("jsonb"); - builder.Property(bp => bp.References).IsRequired(false).HasColumnType("jsonb"); + builder.Property(bp => bp.ContentType).IsRequired(); builder.Property(bp => bp.Enabled).HasDefaultValue(true); + builder.Property(bp => bp.Notes).IsRequired(false); + + builder.OwnsMany(f => f.References, b => { b.ToJson(); }); builder.HasOne(f =>f.Book) .WithMany(b => b.Pages) diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs index d95cfa1..c9d082d 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs @@ -22,7 +22,7 @@ public class BookPage public string[]? Notes { get; set; } - public PageReference[]? References { get; set; } + public ICollection? References { get; set; } public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs similarity index 74% rename from LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs rename to LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs index d4af2e7..5af145a 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs @@ -14,5 +14,7 @@ public class CreateBookPage public string[]? Notes { get; set; } - public PageReference[]? References { get; set; } + public ICollection? References { get; set; } } + +public sealed class UpdateBookPage : CreateBookPage; \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs deleted file mode 100644 index 7016067..0000000 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Pages.Models; - -public sealed class UpdateBookPage : CreateBookPage; diff --git a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs index 80f3a5d..26569bc 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -13,19 +13,13 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) - return Result.Fail("Book not found"); + var rowsDeleted = await context.Pages + .Where(p => p.AuthorBookId == authorBookId) + .ExecuteDeleteAsync(cancellationToken); - var pages = await context.Pages.Where(p => p.AuthorBookId == authorBookId).ToListAsync(cancellationToken); - - if (pages.Count == 0) - return Result.Fail("No pages found for the specified book"); - - context.Pages.RemoveRange(pages); - - await context.SaveChangesAsync(cancellationToken); - - return Result.Ok(); + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No pages found for the specified book"); } catch (Exception ex) { @@ -39,16 +33,13 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var page = await context.Pages.FirstOrDefaultAsync(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType, cancellationToken); + var rowsDeleted = await context.Pages + .Where(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType) + .ExecuteDeleteAsync(cancellationToken); - if (page is null) - return Result.Fail("Page not found"); - - context.Pages.Remove(page); - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsDeleted > 0 ? Result.Ok() - : Result.Fail("Failed to delete page"); + : Result.Fail("Page not found"); } catch (Exception ex) { @@ -62,17 +53,15 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + var rowsUpdated = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, enabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (page is null) - return Result.Fail("Page not found"); - - page.UpdatedAt = DateTime.UtcNow; - page.Enabled = enabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update page status"); + : Result.Fail("Page not found"); } catch (Exception ex) { @@ -86,16 +75,13 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + var rowsDeleted = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteDeleteAsync(cancellationToken); - if (page is null) - return Result.Fail("Page not found"); - - context.Pages.Remove(page); - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsDeleted > 0 ? Result.Ok() - : Result.Fail("Failed to delete page"); + : Result.Fail("Page not found"); } catch (Exception ex) { @@ -109,22 +95,20 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + var rowsUpdated = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Type, request.Type) + .SetProperty(p => p.ContentType, request.ContentType) + .SetProperty(p => p.Number, request.Number) + .SetProperty(p => p.Content, request.Content) + .SetProperty(p => p.Notes, request.Notes) + .SetProperty(p => p.References, request.References) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (page is null) - return Result.Fail("Page not found"); - - page.UpdatedAt = DateTime.UtcNow; - page.Type = request.Type; - page.ContentType = request.ContentType; - page.Number = request.Number; - page.Content = request.Content; - page.Notes = request.Notes; - page.References = request.References; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update page"); + : Result.Fail("Page not found"); } catch (Exception ex) { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs similarity index 87% rename from LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs index 856fd68..ed24e21 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs @@ -1,7 +1,6 @@ // using System; using LiteCharms.Features.MidrandBooks.Postgres; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -13,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations { [DbContext(typeof(MidrandBooksDbContext))] - [Migration("20260527070840_Init")] + [Migration("20260528052014_Init")] partial class Init { /// @@ -112,9 +111,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Property("PublisherType") .HasColumnType("integer"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("ThumbnailImageUrl") .HasMaxLength(2048) .HasColumnType("character varying(2048)"); @@ -291,9 +287,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("UpdatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -338,7 +331,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("integer"); b.Property("Total") - .HasColumnType("decimal(18,2)"); + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); b.Property("UpdatedAt") .ValueGeneratedOnAdd() @@ -494,17 +488,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("boolean") .HasDefaultValue(true); - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); b.Property("Number") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasDefaultValue(0); - b.Property("References") - .HasColumnType("jsonb"); - b.Property("Type") .HasColumnType("integer"); @@ -570,8 +561,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.PrimitiveCollection("Categories") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -591,9 +582,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(1024) .HasColumnType("character varying(1024)"); - b.Property("Metadata") - .HasColumnType("jsonb"); - b.Property("Name") .IsRequired() .HasMaxLength(255) @@ -604,8 +592,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.PrimitiveCollection("ThumbnailUrls") - .HasColumnType("jsonb"); + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); b.Property("Type") .HasColumnType("integer"); @@ -714,6 +702,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Product"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") @@ -736,6 +756,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Customer"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => { b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") @@ -798,7 +850,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .OnDelete(DeleteBehavior.NoAction) .IsRequired(); + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + b.Navigation("Book"); + + b.Navigation("References"); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => @@ -812,6 +891,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs similarity index 91% rename from LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs index 774c77a..1c0cb96 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs @@ -1,5 +1,4 @@ using System; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; @@ -31,8 +30,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Website = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), ThumbnailImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), - SocialMedia = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SocialMedia = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -52,8 +51,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Email = table.Column(type: "text", nullable: false), Website = table.Column(type: "text", nullable: false), Phone = table.Column(type: "text", nullable: false), - SocialMedia = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SocialMedia = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -70,7 +69,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), CustomerId = table.Column(type: "bigint", nullable: false), Status = table.Column(type: "integer", nullable: false), - Total = table.Column(type: "numeric(18,2)", nullable: false), + Total = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), Notes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), InvoiceUrl = table.Column(type: "text", nullable: true) }, @@ -92,10 +91,10 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), ImageUrl = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), - ThumbnailUrls = table.Column(type: "jsonb", nullable: true), - Categories = table.Column(type: "jsonb", nullable: true), - Metadata = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false) + ThumbnailUrls = table.Column(type: "text[]", nullable: true), + Categories = table.Column(type: "text[]", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false), + Metadata = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -260,29 +259,29 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations onDelete: ReferentialAction.Restrict); }); - migrationBuilder.CreateTable( - name: "ProductPrice", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - ProductId = table.Column(type: "bigint", nullable: false), - Amount = table.Column(type: "numeric", nullable: false), - Discount = table.Column(type: "numeric", nullable: false), - Enabled = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ProductPrice", x => x.Id); - table.ForeignKey( - name: "FK_ProductPrice_Products_ProductId", - column: x => x.ProductId, - principalTable: "Products", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + //migrationBuilder.CreateTable( + // name: "ProductPrice", + // columns: table => new + // { + // Id = table.Column(type: "bigint", nullable: false) + // .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + // ProductId = table.Column(type: "bigint", nullable: false), + // Amount = table.Column(type: "numeric", nullable: false), + // Discount = table.Column(type: "numeric", nullable: false), + // Enabled = table.Column(type: "boolean", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_ProductPrice", x => x.Id); + // table.ForeignKey( + // name: "FK_ProductPrice_Products_ProductId", + // column: x => x.ProductId, + // principalTable: "Products", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); migrationBuilder.CreateTable( name: "Shippings", @@ -334,9 +333,9 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations ContentType = table.Column(type: "integer", nullable: false), Number = table.Column(type: "integer", nullable: false, defaultValue: 0), Content = table.Column(type: "bytea", nullable: false), - Notes = table.Column(type: "jsonb", nullable: true), - References = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Notes = table.Column(type: "text[]", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + References = table.Column(type: "jsonb", nullable: true) }, constraints: table => { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 04c9235..634218e 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -1,7 +1,6 @@ // using System; using LiteCharms.Features.MidrandBooks.Postgres; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -58,7 +57,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasIndex("ProductId"); - b.ToTable("Books"); + b.ToTable("Books", (string)null); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => @@ -109,9 +108,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Property("PublisherType") .HasColumnType("integer"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("ThumbnailImageUrl") .HasMaxLength(2048) .HasColumnType("character varying(2048)"); @@ -288,9 +284,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("UpdatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -335,7 +328,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("integer"); b.Property("Total") - .HasColumnType("decimal(18,2)"); + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); b.Property("UpdatedAt") .ValueGeneratedOnAdd() @@ -460,7 +454,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasKey("Id"); - b.ToTable("ShippingProviders"); + b.ToTable("ShippingProviders", (string)null); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => @@ -491,17 +485,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("boolean") .HasDefaultValue(true); - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); b.Property("Number") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasDefaultValue(0); - b.Property("References") - .HasColumnType("jsonb"); - b.Property("Type") .HasColumnType("integer"); @@ -567,8 +558,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.PrimitiveCollection("Categories") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -588,9 +579,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(1024) .HasColumnType("character varying(1024)"); - b.Property("Metadata") - .HasColumnType("jsonb"); - b.Property("Name") .IsRequired() .HasMaxLength(255) @@ -601,8 +589,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.PrimitiveCollection("ThumbnailUrls") - .HasColumnType("jsonb"); + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); b.Property("Type") .HasColumnType("integer"); @@ -689,7 +677,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasIndex("ProductId") .IsUnique(); - b.ToTable("ProductPrice"); + b.ToTable("ProductPrice", (string)null); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => @@ -711,6 +699,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Product"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors", (string)null); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") @@ -733,6 +753,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Customer"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers", (string)null); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => { b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") @@ -795,7 +847,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .OnDelete(DeleteBehavior.NoAction) .IsRequired(); + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages", (string)null); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + b.Navigation("Book"); + + b.Navigation("References"); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => @@ -809,6 +888,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products", (string)null); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs index e360385..db18548 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -14,9 +14,10 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Description).HasMaxLength(1024); builder.Property(f => f.ImageUrl).HasMaxLength(1024); - builder.Property(f => f.ThumbnailUrls).IsRequired(false).HasColumnType("jsonb"); - builder.Property(f => f.Metadata).IsRequired(false).HasColumnType("jsonb"); - builder.Property(f => f.Categories).IsRequired(false).HasColumnType("jsonb"); builder.Property(f => f.Enabled).HasDefaultValue(false); + builder.Property(f => f.Categories).IsRequired(false); + builder.Property(f => f.ThumbnailUrls).IsRequired(false); + + builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); }); } } diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs index e78c42b..ba6a593 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs @@ -3,5 +3,5 @@ [EntityTypeConfiguration] public class ProductPrice : Models.ProductPrice { - public virtual Product Product { get; set; } = new(); + public virtual Product? Product { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs deleted file mode 100644 index b237f96..0000000 --- a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Products.Models; - -public sealed class CreateProductPrice -{ - public long ProductId { get; set; } - - public decimal Amount { get; set; } - - public decimal Discount { get; set; } -} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs index 44786dd..9c09551 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -20,3 +20,10 @@ public sealed record CreateProduct public ProductMetadata? Metadata { get; set; } } + +public sealed class CreateProductPrice +{ + public decimal Amount { get; set; } + + public decimal Discount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 99f1c45..4d1b0ab 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -14,17 +14,15 @@ public sealed class ProductService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var productPrice = await context.Prices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + var rowsUpdated = await context.Prices + .Where(p => p.Id == productPriceId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, isEnabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (productPrice is null) - return Result.Fail(new Error($"Product price with ID {productPriceId} not found")); - - productPrice.UpdatedAt = DateTime.UtcNow; - productPrice.Enabled = isEnabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error($"Failed to change status of product price with ID {productPriceId}")); + : Result.Fail(new Error($"Product price with ID {productPriceId} not found")); } catch (Exception ex) { @@ -38,17 +36,15 @@ public sealed class ProductService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + var rowsUpdated = await context.Products + .Where(p => p.Id == productId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, isEnabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (product is null) - return Result.Fail(new Error($"Product with ID {productId} not found")); - - product.UpdatedAt = DateTime.UtcNow; - product.Enabled = isEnabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error($"Failed to change status of product with ID {productId}")); + : Result.Fail(new Error($"Product with ID {productId} not found")); } catch (Exception ex) { @@ -64,17 +60,22 @@ public sealed class ProductService(IDbContextFactory cont var query = context.Products.AsQueryable(); + var cultureInfo = CultureInfo.InvariantCulture; + + if (!string.IsNullOrWhiteSpace(filter.Name)) + query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Name}%")); + if (!string.IsNullOrWhiteSpace(filter.Title)) - query = query.Where(p => p.Name!.Contains(filter.Title)); + query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%")); if (!string.IsNullOrWhiteSpace(filter.Category)) - query = query.Where(p => p.Categories!.Any(c => c == filter.Category)); + query = query.Where(p => p.Categories.Contains(filter.Category)); if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) - query = query.Where(p => p.Metadata!.Manufacturer == filter.Manufacturer); + query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%")); if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) - query = query.Where(p => p.Metadata!.SerialNumber == filter.SerialNumber); + query = query.Where(p => EF.Functions.ILike(p.Metadata!.SerialNumber!, $"%{filter.SerialNumber}%")); if (filter.MinPrice > 0) query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice)); @@ -165,7 +166,7 @@ public sealed class ProductService(IDbContextFactory cont } } - public async ValueTask> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) + public async ValueTask> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) { try { @@ -175,16 +176,16 @@ public sealed class ProductService(IDbContextFactory cont .AsNoTracking() .OrderByDescending(p => p.CreatedAt) .ThenBy(p => p.UpdatedAt) - .FirstOrDefaultAsync(p => p.ProductId == productId, cancellationToken); + .FirstOrDefaultAsync(p => p.ProductId == productId && p.Enabled, cancellationToken); return product is not null - ? Result.Ok(new[] { product.ToModel() }) - : Result.Fail(new Error($"No price found for product with ID {productId}")); + ? Result.Ok(product.ToModel()) + : Result.Fail(new Error($"No price found for product with ID {productId}")); } catch (Exception ex) { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); + return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } @@ -228,8 +229,8 @@ public sealed class ProductService(IDbContextFactory cont { try { - var fromDate = range.From.ToDateTime(TimeOnly.MinValue); - var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -244,7 +245,7 @@ public sealed class ProductService(IDbContextFactory cont .AsSplitQuery() .ToArrayAsync(cancellationToken); - return await context.SaveChangesAsync(cancellationToken) > 0 + return products?.Length > 0 ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) : Result.Fail(new Error("Failed to retrieve products.")); }