From 2546c34ffc19499b0115cfacf21f6824d10dd6d4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 29 May 2026 01:05:22 +0200 Subject: [PATCH] Wrote tests for most services, applied EF core optimisations --- .../AuthorServiceFeatureTests.cs | 81 +++++++ .../BooksServiceFeatureTests.cs | 52 +++++ .../{ => Common}/Fixture.cs | 2 +- .../{ => Common}/IntegrationFactAttribute.cs | 2 +- .../CustomerServiceFeatureTests.cs | 201 ++++++++++++++++++ .../OrderServiceFeatureTests.cs | 167 +++++++++++++++ .../PageServiceFeatureTests.cs | 63 ++++++ ...eTest.cs => ProductServiceFeatureTests.cs} | 3 +- .../AuthorBooks/BooksService.cs | 34 +-- .../AuthorBooks/Entities/AuthorBook.cs | 2 +- .../Authors/AuthorService.cs | 69 ++---- .../Authors/Models/Author.cs | 2 +- .../Customers/CustomerService.cs | 112 +++++----- .../Customers/Models/Customer.cs | 2 +- .../Customers/Models/Records.cs | 2 +- .../Orders/Models/Records.cs | 2 +- .../Orders/OrderService.cs | 158 +++++++------- .../Pages/Models/BookPage.cs | 2 +- .../Pages/Models/CreateBookPage.cs | 2 +- .../Pages/PageService.cs | 86 +++----- .../MidrandBooksDbContextModelSnapshot.cs | 14 +- .../Products/ProductService.cs | 32 ++- 22 files changed, 793 insertions(+), 297 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs rename LiteCharms.Features.MidrandBooks.Tests/{ => Common}/Fixture.cs (95%) rename LiteCharms.Features.MidrandBooks.Tests/{ => Common}/IntegrationFactAttribute.cs (77%) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs rename LiteCharms.Features.MidrandBooks.Tests/{ProductServiceFeatureTest.cs => ProductServiceFeatureTests.cs} (96%) 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 95% rename from LiteCharms.Features.MidrandBooks.Tests/Fixture.cs rename to LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs index d99ea23..6948d48 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs @@ -2,7 +2,7 @@ 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 { diff --git a/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs similarity index 77% rename from LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs rename to LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs index 24194e6..304f78e 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Tests; +namespace LiteCharms.Features.MidrandBooks.Tests.Common; public class IntegrationFactAttribute : FactAttribute { 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/OrderServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs new file mode 100644 index 0000000..e14e007 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs @@ -0,0 +1,167 @@ +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() + { + + } + + [IntegrationFact] + public async Task UpdateShippingStatusAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task GetShippingByOrderIdAsync_ShouldReturn_ResultWithShipping() + { + + } + + [IntegrationFact] + public async Task RemoveShippingFromOrderAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task UpdateShippingTrackingNumberAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task GetShippingProvidersAsync_ShouldReturn_ResultWithShippingProviderList() + { + + } + + [IntegrationFact] + public async Task GetShippingProviderAsync_ShouldReturn_ResultWithShippingProvider() + { + + } + + [IntegrationFact] + public async Task UpdateShippingProviderAsync_ShouldReturn_ResultWithSuccess() + { + + } +} 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/ProductServiceFeatureTest.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs similarity index 96% rename from LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs rename to LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs index c498909..6dc7e88 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -1,10 +1,11 @@ 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 ProductServiceFeatureTest(Fixture fixture, ITestOutputHelper output) : IClassFixture +public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture { private readonly ProductService productService = fixture.Services.GetRequiredService(); 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/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/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/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs index f5453bc..c4c072a 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -1,6 +1,6 @@ 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); diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index c15b2bf..84c7d86 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) { @@ -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) { @@ -440,24 +440,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/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/CreateBookPage.cs index d4af2e7..5c691c9 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs @@ -14,5 +14,5 @@ public class CreateBookPage public string[]? Notes { get; set; } - public PageReference[]? References { get; set; } + public ICollection? References { get; set; } } 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/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 6340386..634218e 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -57,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 => @@ -454,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 => @@ -677,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 => @@ -718,7 +718,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("AuthorId", "__synthesizedOrdinal"); - b1.ToTable("Authors"); + b1.ToTable("Authors", (string)null); b1 .ToJson("SocialMedia") @@ -772,7 +772,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("CustomerId", "__synthesizedOrdinal"); - b1.ToTable("Customers"); + b1.ToTable("Customers", (string)null); b1 .ToJson("SocialMedia") @@ -862,7 +862,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("BookPageId", "__synthesizedOrdinal"); - b1.ToTable("BookPages"); + b1.ToTable("BookPages", (string)null); b1 .ToJson("References") @@ -904,7 +904,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("ProductId"); - b1.ToTable("Products"); + b1.ToTable("Products", (string)null); b1 .ToJson("Metadata") diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index aef7710..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) {