From d55bf4f82f99849004cb3492c53a2ade48856956 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 25 May 2026 22:18:53 +0200 Subject: [PATCH 1/6] Created Author, Book, AuthorBook, Page and Product with Price --- .../AuthorBooks/BooksService.cs | 104 ++++++++ .../AuthorBooks/Entities/AuthorBook.cs | 14 + .../Entities/AuthorBookConfiguration.cs | 28 ++ .../AuthorBooks/Models/AuthorBook.cs | 20 ++ .../Authors/AuthorService.cs | 182 +++++++++++++ .../Authors/Entities/Author.cs | 9 + .../Authors/Entities/AuthorConfiguration.cs | 24 ++ .../Authors/Models/Author.cs | 36 +++ .../Authors/Models/Records.cs | 30 +++ LiteCharms.Features.MidrandBooks/Enums.cs | 62 +++++ .../Extensions/HealthChecks.cs | 2 +- .../Extensions/Mappers.cs | 84 ++++++ .../Extensions/Postgres.cs | 6 +- .../PostgresMidrandShopHealthCheck.cs | 6 +- .../Pages/Entities/BookPage.cs | 9 + .../Pages/Entities/BookPageConfiguration.cs | 26 ++ .../Pages/Models/BookPage.cs | 28 ++ .../Pages/Models/CreateBookPage.cs | 18 ++ .../Pages/Models/UpdateBookPage.cs | 3 + .../Pages/PagesService.cs | 225 ++++++++++++++++ .../Postgres/MidrandBooksDbContext.cs | 19 ++ ...ory.cs => MidrandBooksDbContextFactory.cs} | 12 +- .../Postgres/MidrandShopDbContext.cs | 6 - .../Products/Entities/Product.cs | 7 + .../Products/Entities/ProductConfiguration.cs | 22 ++ .../Products/Entities/ProductPrice.cs | 7 + .../Entities/ProductPriceConfiguration.cs | 22 ++ .../Products/Models/CreateProductPrice.cs | 10 + .../Products/Models/Product.cs | 30 +++ .../Products/Models/ProductPrice.cs | 18 ++ .../Products/Models/Records.cs | 22 ++ .../Products/ProductService.cs | 252 ++++++++++++++++++ .../Customers/CustomerService.cs | 3 +- .../Products/Models/Product.cs | 4 +- LiteCharms.Features/Enums.cs | 13 + LiteCharms.Features/Models/PageReference.cs | 10 + LiteCharms.Features/Models/ProductFilter.cs | 18 ++ .../Models/ProductMetadata.cs | 2 +- LiteCharms.Features/Models/SocialMedia.cs | 13 + 39 files changed, 1383 insertions(+), 23 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs create mode 100644 LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs create mode 100644 LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs create mode 100644 LiteCharms.Features.MidrandBooks/Enums.cs create mode 100644 LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/PagesService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs rename LiteCharms.Features.MidrandBooks/Postgres/{MidrandShopDbContextFactory.cs => MidrandBooksDbContextFactory.cs} (58%) delete mode 100644 LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/Product.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/Records.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/ProductService.cs create mode 100644 LiteCharms.Features/Models/PageReference.cs create mode 100644 LiteCharms.Features/Models/ProductFilter.cs rename {LiteCharms.Features.TechShop/Products => LiteCharms.Features}/Models/ProductMetadata.cs (79%) create mode 100644 LiteCharms.Features/Models/SocialMedia.cs diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs new file mode 100644 index 0000000..f3ba48b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -0,0 +1,104 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks; + +public class BooksService(IDbContextFactory contextFactory) +{ + public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var book = await context.Books.FirstOrDefaultAsync(b => b.Id == bookId, 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 + ? Result.Ok() + : Result.Fail(new Error($"Failed to change status of book with ID {bookId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> PublishBookAsync(long authorId, long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken)) + return Result.Fail("Author not found."); + + if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) + return Result.Fail("Product not found."); + + var book = context.Books.Add(new Entities.AuthorBook + { + AuthorId = authorId, + ProductId = productId, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(book.Entity.Id) + : Result.Fail("Failed to create book."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetBookAsync(long bookId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var book = await context.Books + .AsNoTracking().FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); + + return book is null + ? Result.Fail(new Error($"Book with ID {bookId} not found")) + : Result.Ok(book.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetBooksByAuthorAsync(long authorId, CancellationToken cancellationToken = default) + { + try + { + 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")); + + var books = await context.Books + .AsNoTracking() + .OrderByDescending(b => b.CreatedAt) + .Where(b => b.AuthorId == authorId) + .ToListAsync(cancellationToken); + + return books?.Count > 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)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs new file mode 100644 index 0000000..9c44f39 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs @@ -0,0 +1,14 @@ +using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Pages.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +public class AuthorBook : Models.AuthorBook +{ + public virtual Author Author { get; set; } = new(); + + public virtual Product Book { get; set; } = new(); + + public virtual ICollection Pages { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs new file mode 100644 index 0000000..1f18c56 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +public class AuthorBookConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Books"); + + builder.HasKey(f => f.AuthorId); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.AuthorId).IsRequired(); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.Rating).IsRequired(false); + builder.Property(f => f.Ranking).IsRequired(false); + builder.Property(f => f.Enabled).HasDefaultValue(true); + + builder.HasOne(f => f.Author) + .WithMany(a => a.Books) + .HasForeignKey(f => f.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Book) + .WithMany() + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs new file mode 100644 index 0000000..5d77fe9 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models; + +public class AuthorBook +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long AuthorId { get; set; } + + public long ProductId { get; set; } + + public int Rating { get; set; } + + public int Ranking { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs new file mode 100644 index 0000000..b9cc707 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -0,0 +1,182 @@ +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors; + +public class AuthorService(IDbContextFactory contextFactory) +{ + public async ValueTask> GetAuthorBooksAsync(long authorId, 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")); + + var books = await context.Books.AsNoTracking() + .OrderByDescending(b => b.CreatedAt) + .Where(p => p.AuthorId == authorId) + .Select(p => p.Book.ToModel()) + .ToArrayAsync(cancellationToken); + + return books?.Length > 0 + ? Result.Ok(books) + : 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 + ? Result.Ok() + : Result.Fail(new Error($"Failed to change status of author with ID {authorId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAuthorAsync(long authorId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + + return author is not null + ? Result.Ok(author.ToModel()) + : Result.Fail(new Error($"Author with ID {authorId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAuthors(DateRange range, CancellationToken cancellationToken) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var authors = await context.Authors.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .ThenByDescending(o => o.UpdatedAt) + .Where(a => a.CreatedAt >= fromDate && a.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return authors?.Length > 0 + ? Result.Ok(authors.Select(a => a.ToModel()).ToArray()) + : Result.Fail(new Error("No authors found in the specified date range.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken) + { + 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) + return Result.Fail(new Error($"Author with ID {authorId} not found")); + + author.UpdatedAt = DateTime.UtcNow; + author.PublisherType = request.PublisherType; + author.Company = request.Company; + author.VatNumber = request.VatNumber; + author.Name = request.Name; + author.LastName = request.LastName; + author.Biography = request.Biography; + author.Email = request.Email; + author.Website = request.Website; + author.ImageUrl = request.ImageUrl; + author.ThumbnailImageUrl = request.ThumbnailImageUrl; + author.SocialMedia = request.SocialMedia; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update author with ID {authorId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateAuthorAsync(CreateAuthor request, CancellationToken cancellationToken = default) + { + try + { + 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 newAuthor = context.Authors.Add(new Entities.Author + { + Company = request.Company, + VatNumber = request.VatNumber, + PublisherType = request.PublisherType, + Name = request.Name, + LastName = request.LastName, + Biography = request.Biography, + Email = request.Email, + Website = request.Website, + ImageUrl = request.ImageUrl, + ThumbnailImageUrl = request.ThumbnailImageUrl, + SocialMedia = request.SocialMedia + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newAuthor.Entity.Id) + : Result.Fail(new Error($"Failed to create author {request.Name} {request.LastName}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs new file mode 100644 index 0000000..60b6a2b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +namespace LiteCharms.Features.MidrandBooks.Authors.Entities; + +[EntityTypeConfiguration] +public class Author : Models.Author +{ + public ICollection Books { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs new file mode 100644 index 0000000..cd6eda1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs @@ -0,0 +1,24 @@ +namespace LiteCharms.Features.MidrandBooks.Authors.Entities; + +public class AuthorConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Authors"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.PublisherType).IsRequired(); + builder.Property(f => f.VatNumber).IsRequired(false).HasMaxLength(255); + builder.Property(f => f.Name).IsRequired().HasMaxLength(255); + builder.Property(f => f.LastName).IsRequired().HasMaxLength(255); + builder.Property(f => f.Biography).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Email).IsRequired().HasMaxLength(512); + 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); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs new file mode 100644 index 0000000..0546a7b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs @@ -0,0 +1,36 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors.Models; + +public class Author +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public PublisherTypes PublisherType { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Biography { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? ImageUrl { get; set; } + + public string? ThumbnailImageUrl { get; set; } + + public SocialMedia[]? SocialMedia { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs new file mode 100644 index 0000000..ba54191 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors.Models; + +public record UpdateAuthor : CreateAuthor; + +public record CreateAuthor +{ + public required PublisherTypes PublisherType { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public required string Name { get; set; } + + public required string LastName { get; set; } + + public string? Biography { get; set; } + + public required string Email { get; set; } + + public string? Website { get; set; } + + public required string ImageUrl { get; set; } + + public string? ThumbnailImageUrl { get; set; } + + public SocialMedia[]? SocialMedia { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Enums.cs b/LiteCharms.Features.MidrandBooks/Enums.cs new file mode 100644 index 0000000..b3325c2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Enums.cs @@ -0,0 +1,62 @@ +namespace LiteCharms.Features.MidrandBooks; + +public enum PublisherTypes : int +{ + Individual = 0, + Company = 1, + Organization = 2, + SelfPublished = 3, + UniversityPress = 4, + GovernmentAgency = 5, + NonProfit = 6, + Independent = 7 +} + +public enum BookTypes : int +{ + Fiction = 0, + NonFiction = 1, + Academic = 2, + SelfHelp = 3, + Biography = 4, + Poetry = 5, + Children = 6, + YoungAdult = 7, + ScienceFiction = 8, + Fantasy = 9 +} + +public enum BookContentTypes : int +{ + Text = 0, + Image = 1, + Video = 2, + Audio = 3, + Interactive = 4, + Markdown = 5, + Html = 6, + Json = 7, + Yaml = 8 +} + +public enum BookPageTypes : int +{ + Cover = 0, + Preface = 1, + Introduction = 2, + Content = 3, + Closing = 4, + Referencer = 5, + Credits = 6, + BackCover = 7 +} + +public enum ProductTypes : int +{ + Book = 1, + Journal = 2, + Magazine = 3, + EBook = 4, + Audiobook = 5, + Accessory = 6 +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs b/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs index fbc3e1f..bf35ee8 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs @@ -15,7 +15,7 @@ public static class HealthChecks public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services) { - services.AddHealthChecks().AddCheck(MidrandShopDbConfigName); + services.AddHealthChecks().AddCheck(MidrandBooksDbConfigName); return services; } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs new file mode 100644 index 0000000..21607dc --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -0,0 +1,84 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Pages.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class Mappers +{ + public static BookPage ToModel(this Pages.Entities.BookPage entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + AuthorBookId = entity.AuthorBookId, + Content = entity.Content, + ContentType = entity.ContentType, + Number = entity.Number, + Enabled = entity.Enabled, + Notes = entity.Notes, + References = entity.References, + Type = entity.Type + }; + + public static AuthorBook ToModel(this AuthorBooks.Entities.AuthorBook entity) => new() + { + Id = entity.Id, + ProductId = entity.ProductId, + CreatedAt = entity.CreatedAt, + AuthorId = entity.AuthorId, + Ranking = entity.Ranking, + Rating = entity.Rating, + Enabled = entity.Enabled + }; + + public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + ProductId = entity.ProductId, + Amount = entity.Amount, + Discount = entity.Discount, + Enabled = entity.Enabled + }; + + public static Product ToModel(this Products.Entities.Product entity) + { + return new Product + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + Summary = entity.Summary, + Description = entity.Description, + Type = entity.Type, + ImageUrl = entity.ImageUrl, + ThumbnailUrls = entity.ThumbnailUrls, + Metadata = entity.Metadata, + Categories = entity.Categories, + Enabled = entity.Enabled + }; + } + + public static Author ToModel(this Authors.Entities.Author entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + LastName = entity.LastName, + Biography = entity.Biography, + Email = entity.Email, + Website = entity.Website, + ImageUrl = entity.ImageUrl, + ThumbnailImageUrl = entity.ThumbnailImageUrl, + SocialMedia = entity.SocialMedia, + Enabled = entity.Enabled, + Company = entity.Company, + PublisherType = entity.PublisherType, + VatNumber = entity.VatNumber + }; +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs index 86173d9..228634d 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs @@ -4,12 +4,12 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Postgres { - public const string MidrandShopDbConfigName = "PostgresMidrandBooks"; + public const string MidrandBooksDbConfigName = "PostgresMidrandBooks"; public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) { - services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName))); + services.AddPooledDbContextFactory(options => + options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName))); return services; } diff --git a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs index e60ef2c..5a8b44e 100644 --- a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs @@ -4,7 +4,7 @@ namespace LiteCharms.Features.MidrandBooks.HealthChecks; public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck { - private readonly string connectionString = configuration.GetConnectionString(MidrandShopDbConfigName)!; + private readonly string connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName)!; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { @@ -18,11 +18,11 @@ public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHea await command.ExecuteScalarAsync(cancellationToken); - return HealthCheckResult.Healthy($"{MidrandShopDbConfigName} is responsive."); + return HealthCheckResult.Healthy($"{MidrandBooksDbConfigName} is responsive."); } catch (Exception ex) { - return HealthCheckResult.Unhealthy($"{MidrandShopDbConfigName} is unreachable.", ex); + return HealthCheckResult.Unhealthy($"{MidrandBooksDbConfigName} is unreachable.", ex); } } } \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs new file mode 100644 index 0000000..bba4436 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +namespace LiteCharms.Features.MidrandBooks.Pages.Entities; + +[EntityTypeConfiguration] +public class BookPage : Models.BookPage +{ + public virtual AuthorBook Book { get; set; } = new(); +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs new file mode 100644 index 0000000..48c8196 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Pages.Entities; + +public class BookPageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("BookPages"); + + builder.HasKey(bp => bp.Id); + builder.Property(bp => bp.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(bp => bp.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(bp => bp.Number).IsRequired().HasDefaultValue(0); + 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.Enabled).HasDefaultValue(true); + + builder.HasOne(f =>f.Book) + .WithMany(b => b.Pages) + .HasForeignKey(f => f.AuthorBookId) + .OnDelete(DeleteBehavior.NoAction); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs new file mode 100644 index 0000000..d95cfa1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs @@ -0,0 +1,28 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Pages.Models; + +public class BookPage +{ + public long Id { get; set; } + + public long AuthorBookId { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public BookPageTypes Type { get; set; } + + public BookContentTypes ContentType { get; set; } + + public int Number { get; set; } + + public byte[]? Content { get; set; } + + public string[]? Notes { get; set; } + + public PageReference[]? 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 new file mode 100644 index 0000000..d4af2e7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs @@ -0,0 +1,18 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Pages.Models; + +public class CreateBookPage +{ + public BookPageTypes Type { get; set; } + + public BookContentTypes ContentType { get; set; } + + public int Number { get; set; } + + public byte[]? Content { get; set; } + + public string[]? Notes { get; set; } + + public PageReference[]? References { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs new file mode 100644 index 0000000..d78b732 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.MidrandBooks.Pages.Models; + +public class UpdateBookPage : CreateBookPage; diff --git a/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs b/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs new file mode 100644 index 0000000..18c822b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs @@ -0,0 +1,225 @@ +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Pages.Models; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Pages; + +public class PagesService(IDbContextFactory contextFactory) +{ + public async ValueTask DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default) + { + try + { + 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 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(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteByPageTypeAsync(long authorBookId, int pageNumber, BookPageTypes pageType, CancellationToken cancellationToken = default) + { + try + { + 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); + + if (page is null) + return Result.Fail("Page not found"); + + context.Pages.Remove(page); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to delete page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePageStatusAsync(long bookPageId, bool enabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + + if (page is null) + return Result.Fail("Page not found"); + + page.UpdatedAt = DateTime.UtcNow; + page.Enabled = enabled; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to update page status"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeletePageAsync(long bookPageId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + + if (page is null) + return Result.Fail("Page not found"); + + context.Pages.Remove(page); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to delete page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePageAsync(long bookPageId, UpdateBookPage request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, 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 + ? Result.Ok() + : Result.Fail("Failed to update page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePageAsync(long authorBookId, CreateBookPage request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) + return Result.Fail("Book not found"); + + if (await context.Pages.AnyAsync(p => p.AuthorBookId == authorBookId && p.Number == request.Number && p.Type == request.Type, cancellationToken)) + return Result.Fail("A page with the same number already exists for this book"); + + var page = context.Pages.Add(new Entities.BookPage + { + AuthorBookId = authorBookId, + Type = request.Type, + ContentType = request.ContentType, + Number = request.Number, + Content = request.Content, + Notes = request.Notes, + References = request.References, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(page.Entity.Id) + : Result.Fail("Failed to create page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPagesAsync(long authorBookId, CancellationToken cancellationToken = default) + { + try + { + 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 pages = await context.Pages.AsNoTracking() + .Where(p => p.AuthorBookId == authorBookId).ToArrayAsync(cancellationToken); + + return pages?.Length > 0 + ? Result.Ok(pages.Select(p => p.ToModel()).ToArray()) + : Result.Fail("No pages found for the specified book"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPageByNumberAsync(long pageId, int number, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId && p.Number == number, cancellationToken); + + return page is not null + ? page.ToModel() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPageAsync(long pageId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId, cancellationToken); + + return page is not null + ? page.ToModel() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs new file mode 100644 index 0000000..1bb907d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; +using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Pages.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.Postgres; + +public class MidrandBooksDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Authors => Set(); + + public DbSet Products => Set(); + + public DbSet Prices => Set(); + + public DbSet Books => Set(); + + public DbSet Pages => Set(); +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs similarity index 58% rename from LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs rename to LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs index 6aca9e4..e518cfd 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs @@ -2,20 +2,20 @@ namespace LiteCharms.Features.MidrandBooks.Postgres; -public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory +public class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory { - public MidrandShopDbContext CreateDbContext(string[] args) + public MidrandBooksDbContext CreateDbContext(string[] args) { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) - .AddUserSecrets(typeof(MidrandShopDbContext).Assembly) + .AddUserSecrets(typeof(MidrandBooksDbContext).Assembly) .AddJsonFile("appsettings.json") .AddEnvironmentVariables() .Build(); - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)); + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName)); - return new MidrandShopDbContext(optionsBuilder.Options); + return new MidrandBooksDbContext(optionsBuilder.Options); } } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs deleted file mode 100644 index 19e6697..0000000 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Postgres; - -public class MidrandShopDbContext(DbContextOptions options) : DbContext(options) -{ - -} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs new file mode 100644 index 0000000..e7e82c6 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class Product : Models.Product +{ + public virtual ICollection Prices { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs new file mode 100644 index 0000000..71515b2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Products"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.Type).IsRequired(); + builder.Property(f => f.Name).IsRequired().HasMaxLength(255); + 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); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs new file mode 100644 index 0000000..e78c42b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class ProductPrice : Models.ProductPrice +{ + public virtual Product Product { get; set; } = new(); +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs new file mode 100644 index 0000000..4705ba5 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public class ProductPriceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Prices"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Discount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Enabled).HasDefaultValue(false); + + builder.HasOne(f => f.Product) + .WithMany(p => p.Prices) + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs new file mode 100644 index 0000000..2fec81e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public 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/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs new file mode 100644 index 0000000..42a977f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class Product +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ProductTypes Type { get; set; } + + public string? Name { get; set; } + + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? ImageUrl { get; set; } + + public string[]? ThumbnailUrls { get; set; } + + public string[]? Categories { get; set; } + + public ProductMetadata? Metadata { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs new file mode 100644 index 0000000..a297068 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductPrice +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long ProductId { get; set; } + + public decimal Amount { get; set; } + + public decimal Discount { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs new file mode 100644 index 0000000..642a739 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -0,0 +1,22 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public record CreateProduct +{ + public required ProductTypes Type { get; set; } + + public required string Name { get; set; } + + public required string Summary { get; set; } + + public required string Description { get; set; } + + public required string ImageUrl { get; set; } + + public string[]? ThumbnailUrls { get; set; } + + public string[]? Categories { get; set; } + + public ProductMetadata? Metadata { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs new file mode 100644 index 0000000..5ee455f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -0,0 +1,252 @@ +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Products; + +public class ProductService(IDbContextFactory contextFactory) +{ + public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var productPrice = await context.Prices.FirstOrDefaultAsync(p => p.Id == productPriceId, 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 + ? Result.Ok() + : Result.Fail(new Error($"Failed to change status of product price with ID {productPriceId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateProductStatusAsync(long productId, bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, 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 + ? Result.Ok() + : Result.Fail(new Error($"Failed to change status of product with ID {productId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> SearchProductsAsync(ProductFilter filter, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var query = context.Products.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(filter.Title)) + query = query.Where(p => p.Name!.Contains(filter.Title)); + + if (!string.IsNullOrWhiteSpace(filter.Category)) + query = query.Where(p => p.Categories!.Any(c => c == filter.Category)); + + if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) + query = query.Where(p => p.Metadata!.Manufacturer == filter.Manufacturer); + + if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) + query = query.Where(p => 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)); + + var products = await query.AsNoTracking().Where(p => p.Enabled).ToListAsync(cancellationToken); + + return products?.Count > 0 + ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) + : Result.Fail("No products found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductPriceAsync(long productId, CreateProductPrice request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) + return Result.Fail($"Product with ID {productId} does not exist."); + + var existingPrices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); + + if (existingPrices.Count > 0) + foreach (var existingPrice in existingPrices) + { + existingPrice.Enabled = false; + existingPrice.UpdatedAt = DateTime.UtcNow; + context.Prices.Update(existingPrice); + } + + var price = context.Prices.Add(new Entities.ProductPrice + { + ProductId = productId, + Amount = request.Amount, + Discount = request.Discount, + Enabled = true, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(price.Entity.Id) + : Result.Fail("Failed to create product price."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail("A product with the same name already exists."); + + if(request.Metadata is not null) + if (await context.Products.AnyAsync(p => p.Metadata!.SerialNumber == request.Metadata.SerialNumber, cancellationToken)) + return Result.Fail("A product with the same metadata already exists."); + + var product = context.Products.Add(new Entities.Product + { + UpdatedAt = DateTime.UtcNow, + Type = request.Type, + Name = request.Name, + Summary = request.Summary, + Description = request.Description, + ImageUrl = request.ImageUrl, + ThumbnailUrls = request.ThumbnailUrls, + Metadata = request.Metadata, + Categories = request.Categories, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(product.Entity.Id) + : Result.Fail("Failed to create product."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Prices + .AsNoTracking() + .OrderByDescending(p => p.CreatedAt) + .ThenBy(p => p.UpdatedAt) + .FirstOrDefaultAsync(p => p.ProductId == productId, cancellationToken); + + return product is not null + ? Result.Ok(new[] { 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)); + } + } + + public async ValueTask> GetProductPricesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var prices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); + + return prices?.Count > 0 + ? prices.Select(p => p.ToModel()).ToArray() + : Result.Fail(new Error($"No prices found for product with ID {productId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + return product is null + ? Result.Fail(new Error($"Product with ID {productId} not found.")) + : Result.Ok(product.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductsAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var products = await context.Products + .AsNoTracking() + .OrderByDescending(p => p.CreatedAt) + .ThenBy(p => p.UpdatedAt) + .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) + : Result.Fail(new Error("Failed to retrieve products.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.TechShop/Customers/CustomerService.cs b/LiteCharms.Features.TechShop/Customers/CustomerService.cs index c8ea2cd..c4983c6 100644 --- a/LiteCharms.Features.TechShop/Customers/CustomerService.cs +++ b/LiteCharms.Features.TechShop/Customers/CustomerService.cs @@ -1,5 +1,4 @@ -using LiteCharms.Features.Extensions; -using LiteCharms.Features.Models; +using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Customers.Models; using LiteCharms.Features.TechShop.Extensions; using LiteCharms.Features.TechShop.Postgres; diff --git a/LiteCharms.Features.TechShop/Products/Models/Product.cs b/LiteCharms.Features.TechShop/Products/Models/Product.cs index b5fd71b..db9dba7 100644 --- a/LiteCharms.Features.TechShop/Products/Models/Product.cs +++ b/LiteCharms.Features.TechShop/Products/Models/Product.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.TechShop.Products.Models; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.TechShop.Products.Models; public class Product { diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 29a915f..510d826 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -1,5 +1,18 @@ namespace LiteCharms.Features; +public enum SocialMediaTypes : int +{ + Twitter = 0, + Facebook = 1, + Instagram = 2, + LinkedIn = 3, + TikTok = 4, + YouTube = 5, + Pinterest = 6, + Reddit = 7, + Tumblr = 8 +} + public enum EmailStatuses : int { GeneralError = 0, diff --git a/LiteCharms.Features/Models/PageReference.cs b/LiteCharms.Features/Models/PageReference.cs new file mode 100644 index 0000000..ab04eb9 --- /dev/null +++ b/LiteCharms.Features/Models/PageReference.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Models; + +public class PageReference +{ + public string? Tag { get; set; } + + public string? Description { get; set; } + + public string? Url { get; set; } +} diff --git a/LiteCharms.Features/Models/ProductFilter.cs b/LiteCharms.Features/Models/ProductFilter.cs new file mode 100644 index 0000000..c6cc275 --- /dev/null +++ b/LiteCharms.Features/Models/ProductFilter.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.Models; + +public class ProductFilter +{ + public string? Name { get; set; } + + public string? Title { get; set; } + + public string? Category { get; set; } + + public string? Manufacturer { get; set; } + + public string? SerialNumber { get; set; } + + public decimal MinPrice { get; set; } + + public decimal MaxPrice { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs b/LiteCharms.Features/Models/ProductMetadata.cs similarity index 79% rename from LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs rename to LiteCharms.Features/Models/ProductMetadata.cs index 168f2bd..ee193fe 100644 --- a/LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs +++ b/LiteCharms.Features/Models/ProductMetadata.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.TechShop.Products.Models; +namespace LiteCharms.Features.Models; public class ProductMetadata { diff --git a/LiteCharms.Features/Models/SocialMedia.cs b/LiteCharms.Features/Models/SocialMedia.cs new file mode 100644 index 0000000..296d979 --- /dev/null +++ b/LiteCharms.Features/Models/SocialMedia.cs @@ -0,0 +1,13 @@ + +namespace LiteCharms.Features.Models; + +public class SocialMedia +{ + public SocialMediaTypes Type { get; set; } + + public string? Name { get; set; } + + public string? ImageUrl { get; set; } + + public string? Url { get; set; } +} From 4a85d01d1acf1633cbfbf8040763940586b5559a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 25 May 2026 23:00:17 +0200 Subject: [PATCH 2/6] Included navigation fields in get queries --- .../AuthorBooks/BooksService.cs | 40 ++++++++++++++++++- .../AuthorBooks/Entities/AuthorBook.cs | 2 +- .../Entities/AuthorBookConfiguration.cs | 2 +- .../AuthorBooks/Models/AuthorBook.cs | 6 ++- .../Authors/AuthorService.cs | 20 ++++++---- .../Extensions/Mappers.cs | 6 ++- .../Products/Models/Product.cs | 2 + .../Products/ProductService.cs | 17 ++++---- 8 files changed, 73 insertions(+), 22 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index f3ba48b..2fdfe2b 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -30,7 +30,7 @@ public class BooksService(IDbContextFactory contextFactor } } - public async ValueTask> PublishBookAsync(long authorId, long productId, CancellationToken cancellationToken = default) + public async ValueTask> CreateBookAsync(long authorId, long productId, CancellationToken cancellationToken = default) { try { @@ -65,7 +65,11 @@ public class BooksService(IDbContextFactory contextFactor await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var book = await context.Books - .AsNoTracking().FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product!.Price) + .Include(b => b.Pages) + .FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); return book is null ? Result.Fail(new Error($"Book with ID {bookId} not found")) @@ -88,6 +92,8 @@ public class BooksService(IDbContextFactory contextFactor var books = await context.Books .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product!.Price) .OrderByDescending(b => b.CreatedAt) .Where(b => b.AuthorId == authorId) .ToListAsync(cancellationToken); @@ -101,4 +107,34 @@ public class BooksService(IDbContextFactory contextFactor return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask> GetPublishedBooksAsync(int offset, int limit, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var books = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product!.Price) + .Include(b => b.Pages) + .Where(b => b.Enabled && b.Product!.Enabled && b.Author.Enabled) + .OrderByDescending(b => b.Ranking) + .ThenByDescending(b => b.Ranking) + .ThenByDescending(b => b.CreatedAt) + .ThenByDescending(b => b.UpdatedAt) + .Skip(offset).Take(limit) + .AsSplitQuery() + .ToArrayAsync(cancellationToken); + + return books?.Length > 0 + ? Result.Ok(books.Select(b => b.ToModel()).ToArray()) + : Result.Fail(new Error("No published books found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs index 9c44f39..c1282e8 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs @@ -8,7 +8,7 @@ public class AuthorBook : Models.AuthorBook { public virtual Author Author { get; set; } = new(); - public virtual Product Book { get; set; } = new(); + public new virtual Product? Product { get; set; } public virtual ICollection Pages { get; set; } = []; } diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs index 1f18c56..3f852ac 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs @@ -20,7 +20,7 @@ public class AuthorBookConfiguration : IEntityTypeConfiguration .HasForeignKey(f => f.AuthorId) .OnDelete(DeleteBehavior.Restrict); - builder.HasOne(f => f.Book) + builder.HasOne(f => f.Product) .WithMany() .HasForeignKey(f => f.ProductId) .OnDelete(DeleteBehavior.Restrict); diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs index 5d77fe9..6ac8dce 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models; public class AuthorBook { @@ -16,5 +18,7 @@ public class AuthorBook public int Ranking { get; set; } + public Product? Product { get; set; } + public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index b9cc707..3897349 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -1,4 +1,5 @@ -using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; @@ -8,7 +9,7 @@ namespace LiteCharms.Features.MidrandBooks.Authors; public class AuthorService(IDbContextFactory contextFactory) { - public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) + public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) { try { @@ -17,21 +18,24 @@ public class AuthorService(IDbContextFactory contextFacto 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")); + return Result.Fail(new Error($"Author with ID {authorId} not found")); - var books = await context.Books.AsNoTracking() + var books = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product!.Price) .OrderByDescending(b => b.CreatedAt) .Where(p => p.AuthorId == authorId) - .Select(p => p.Book.ToModel()) + .AsSplitQuery() .ToArrayAsync(cancellationToken); return books?.Length > 0 - ? Result.Ok(books) - : Result.Fail(new Error($"No books found for author with ID {authorId}")); + ? 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)); + return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 21607dc..4ec3897 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -30,7 +30,8 @@ public static class Mappers AuthorId = entity.AuthorId, Ranking = entity.Ranking, Rating = entity.Rating, - Enabled = entity.Enabled + Enabled = entity.Enabled, + Product = entity.Product?.ToModel(), }; public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new() @@ -59,7 +60,8 @@ public static class Mappers ThumbnailUrls = entity.ThumbnailUrls, Metadata = entity.Metadata, Categories = entity.Categories, - Enabled = entity.Enabled + Enabled = entity.Enabled, + Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null, }; } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs index 42a977f..1207c34 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs @@ -26,5 +26,7 @@ public class Product public ProductMetadata? Metadata { get; set; } + public ProductPrice? Price { get; set; } + public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 5ee455f..4cc30ab 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -74,7 +74,7 @@ public class ProductService(IDbContextFactory contextFact if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) query = query.Where(p => 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)); @@ -96,7 +96,7 @@ public class ProductService(IDbContextFactory contextFact { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if(!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) + if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) return Result.Fail($"Product with ID {productId} does not exist."); var existingPrices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); @@ -133,10 +133,10 @@ public class ProductService(IDbContextFactory contextFact { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if(await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) + if (await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) return Result.Fail("A product with the same name already exists."); - if(request.Metadata is not null) + if (request.Metadata is not null) if (await context.Products.AnyAsync(p => p.Metadata!.SerialNumber == request.Metadata.SerialNumber, cancellationToken)) return Result.Fail("A product with the same metadata already exists."); @@ -223,7 +223,7 @@ public class ProductService(IDbContextFactory contextFact } } - public async ValueTask> GetProductsAsync(DateRange range, CancellationToken cancellationToken = default) + public async ValueTask> GetProductsAsync(int offset, DateRange range, CancellationToken cancellationToken = default) { try { @@ -234,11 +234,14 @@ public class ProductService(IDbContextFactory contextFact var products = await context.Products .AsNoTracking() + .Include(p => p.Prices) .OrderByDescending(p => p.CreatedAt) - .ThenBy(p => p.UpdatedAt) + .ThenByDescending(p => p.UpdatedAt) .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Skip(offset) .Take(range.MaxRecords) - .ToArrayAsync(cancellationToken); + .AsSplitQuery() + .ToArrayAsync(cancellationToken); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) From 7136e4fc70f254077f9d15b88485b2b7ec367d3e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 26 May 2026 00:27:11 +0200 Subject: [PATCH 3/6] Added Customer, Contact and Address with Service Labeled all service to enable assembly scanning --- .../Abstractions/IService.cs | 3 + .../AuthorBooks/BooksService.cs | 5 +- .../Authors/AuthorService.cs | 6 +- .../Customers/CustomerService.cs | 408 ++++++++++++++++++ .../Customers/Entities/Address.cs | 7 + .../Entities/AddressConfiguration.cs | 29 ++ .../Customers/Entities/Contact.cs | 7 + .../Entities/ContactConfiguration.cs | 26 ++ .../Customers/Entities/Customer.cs | 9 + .../Entities/CustomerConfiguration.cs | 20 + .../Customers/Models/Address.cs | 32 ++ .../Customers/Models/Contact.cs | 26 ++ .../Customers/Models/Customer.cs | 26 ++ .../Customers/Models/Records.cs | 60 +++ .../Extensions/Mappers.cs | 47 ++ .../Pages/{PagesService.cs => PageService.cs} | 5 +- .../Postgres/MidrandBooksDbContext.cs | 7 + .../Products/ProductService.cs | 5 +- .../LiteCharms.Features.TechShop.csproj | 5 + LiteCharms.Features/Enums.cs | 25 ++ 20 files changed, 749 insertions(+), 9 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Abstractions/IService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs rename LiteCharms.Features.MidrandBooks/Pages/{PagesService.cs => PageService.cs} (97%) diff --git a/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs b/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs new file mode 100644 index 0000000..6218faf --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.MidrandBooks.Abstractions; + +public interface IService; diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 2fdfe2b..26a60ec 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -1,10 +1,11 @@ -using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.AuthorBooks; -public class BooksService(IDbContextFactory contextFactory) +public class BooksService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index 3897349..43ec4aa 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -1,13 +1,13 @@ -using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +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; -using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Authors; -public class AuthorService(IDbContextFactory contextFactory) +public class AuthorService(IDbContextFactory contextFactory) : IService { public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs new file mode 100644 index 0000000..fdb7451 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -0,0 +1,408 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Customers; + +public class CustomerService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Customers.AnyAsync(c => c.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) + return Result.Fail(new Error($"Customer with email '{request.Email}' already exists.")); + + var customer = context.Customers.Add(new Entities.Customer + { + Company = request.Company, + VatNumber = request.VatNumber, + Email = request.Email, + Website = request.Website, + Phone = request.Phone, + SocialMedia = request.SocialMedia, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(customer.Entity.Id) + : Result.Fail(new Error("Failed to create customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCustomerContactAsync(long customerId, CreateCustomerContact request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + 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)) + return Result.Fail(new Error($"Contact with email '{request.Email}' already exists for this customer.")); + + var contact = context.Contacts.Add(new Entities.Contact + { + CustomerId = customerId, + Name = request.Name, + Email = request.Email, + Phone = request.Phone, + LastName = request.LastName, + Type = request.Type, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(contact.Entity.Id) + : Result.Fail(new Error("Failed to create customer contact.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCustomerAddressAsync(long customerId, CreateCustomerAddress request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var address = context.Addresses.Add(new Entities.Address + { + CustomerId = customerId, + Street = request.Street, + City = request.City, + State = request.State, + PostalCode = request.PostalCode, + Country = request.Country, + Type = request.Type, + Enabled = true, + BuildingType = request.BuildingType, + IsPrimary = request.IsPrimary, + Name = request.Name + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(address.Entity.Id) + : Result.Fail(new Error("Failed to create customer address.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAsync(long customerId, UpdateCustomer request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + if (customer is null) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + customer.UpdatedAt = DateTime.UtcNow; + customer.Company = request.Company; + customer.VatNumber = request.VatNumber; + customer.Email = request.Email; + customer.Website = request.Website; + customer.Phone = request.Phone; + customer.SocialMedia = request.SocialMedia; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerContactAsync(long contactId, UpdateCustomerContact request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, 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 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer contact.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAddressAsync(long addressId, UpdateCustomerAddress request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, 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 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer address.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerStatusAsync(long customerId, bool enabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, 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 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer status.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerContactStatusAsync(long contactId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, 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 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer contact status.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAddressStatusAsync(long addressId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, 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 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer address status.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomersAsync(CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customers = await context.Customers + .AsNoTracking() + .Include(c => c.Contacts) + .Include(c => c.Addresses) + .OrderByDescending(c => c.CreatedAt) + .ThenByDescending(c => c.UpdatedAt) + .AsSplitQuery() + .ToListAsync(cancellationToken); + + return customers?.Count > 0 + ? Result.Ok(customers.Select(c => c.ToModel()).ToArray()) + : Result.Fail(new Error("No customers found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerContactsAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var contacts = await context.Contacts + .AsNoTracking() + .Where(cc => cc.CustomerId == customerId) + .OrderByDescending(cc => cc.CreatedAt) + .ThenByDescending(cc => cc.UpdatedAt) + .ToListAsync(cancellationToken); + + return contacts?.Count > 0 + ? Result.Ok(contacts.Select(cc => cc.ToModel()).ToArray()) + : Result.Fail(new Error("No contacts found for the specified customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAddressesAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var addresses = await context.Addresses + .AsNoTracking() + .Where(a => a.CustomerId == customerId) + .OrderByDescending(a => a.CreatedAt) + .ThenByDescending(a => a.UpdatedAt) + .ToListAsync(cancellationToken); + + return addresses?.Count > 0 + ? Result.Ok(addresses.Select(a => a.ToModel()).ToArray()) + : Result.Fail(new Error($"No addresses found for customer with ID '{customerId}'.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers + .AsNoTracking() + .Include(c => c.Contacts) + .Include(c => c.Addresses) + .FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + return customer is not null + ? Result.Ok(customer.ToModel()) + : Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerContactAsync(long contactId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var contact = await context.Contacts + .AsNoTracking() + .FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + + return contact is not null + ? Result.Ok(contact.ToModel()) + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAddressAsync(long addressId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var address = await context.Addresses + .AsNoTracking() + .FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + + return address is not null + ? Result.Ok(address.ToModel()) + : Result.Fail
(new Error($"Address with ID '{addressId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail
(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs new file mode 100644 index 0000000..d54f7cd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Address : Models.Address +{ + public virtual Customer? Customer { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs new file mode 100644 index 0000000..6b6997d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs @@ -0,0 +1,29 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public class AddressConfiguration : IEntityTypeConfiguration
+{ + public void Configure(EntityTypeBuilder
builder) + { + builder.ToTable("Addresses"); + + builder.HasKey(a => a.Id); + builder.Property(a => a.CustomerId).IsRequired(); + builder.Property(a => a.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(a => a.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(a => a.Name).IsRequired(); + builder.Property(a => a.Type).IsRequired(); + builder.Property(a => a.BuildingType).IsRequired(); + builder.Property(a => a.Street).IsRequired(); + builder.Property(a => a.City).IsRequired(); + builder.Property(a => a.State).IsRequired(); + builder.Property(a => a.PostalCode).IsRequired(); + builder.Property(a => a.Country).IsRequired(); + builder.Property(a => a.IsPrimary).HasDefaultValue(false); + builder.Property(a => a.Enabled).HasDefaultValue(true); + + builder.HasOne(a => a.Customer) + .WithMany(c => c.Addresses) + .HasForeignKey(a => a.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs new file mode 100644 index 0000000..3d5509e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Contact : Models.Contact +{ + public virtual Customer? Customer { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs new file mode 100644 index 0000000..8d6fac0 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public class ContactConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Contacts"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.CustomerId).IsRequired(); + builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(c => c.Name).IsRequired(); + builder.Property(c => c.LastName).IsRequired(); + builder.Property(c => c.Type).IsRequired(); + builder.Property(c => c.Phone).IsRequired(); + builder.Property(c => c.Email).IsRequired(); + builder.Property(c => c.IsPrimary).HasDefaultValue(false); + builder.Property(c => c.Enabled).HasDefaultValue(true); + + builder.HasOne(c => c.Customer) + .WithMany(c => c.Contacts) + .HasForeignKey(c => c.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs new file mode 100644 index 0000000..4443225 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Customer : Models.Customer +{ + public virtual ICollection Contacts { get; set; } = []; + + public virtual ICollection
Addresses { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs new file mode 100644 index 0000000..2b05c59 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public class CustomerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Customers"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(c => c.Company).IsRequired(false); + builder.Property(c => c.VatNumber).IsRequired(false); + 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); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs new file mode 100644 index 0000000..35c7816 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs @@ -0,0 +1,32 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Address +{ + public long Id { get; set; } + + public long CustomerId { get; set; } + + public string? Name { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public AddressType Type { get; set; } + + public AddressBuildingTypes BuildingType { get; set; } + + public string? Street { get; set; } + + public string? City { get; set; } + + public string? State { get; set; } + + public string? PostalCode { get; set; } + + public string? Country { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs new file mode 100644 index 0000000..7eed90e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Contact +{ + public long Id { get; set; } + + public long CustomerId { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ContactTypes Type { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Email { get; set; } + + public string? Phone { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs new file mode 100644 index 0000000..13204ce --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs @@ -0,0 +1,26 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Customer +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public SocialMedia[]? 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 new file mode 100644 index 0000000..ceb022c --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs @@ -0,0 +1,60 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public record CreateCustomer +{ + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public SocialMedia[]? SocialMedia { get; set; } +} + +public record UpdateCustomer : CreateCustomer; + +public record CreateCustomerContact +{ + public ContactTypes Type { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Email { get; set; } + + public string? Phone { get; set; } +} + +public record UpdateCustomerContact : CreateCustomerContact; + +public record CreateCustomerAddress +{ + public string? Name { get; set; } + + public AddressType Type { get; set; } + + public AddressBuildingTypes BuildingType { get; set; } + + public string? Street { get; set; } + + public string? City { get; set; } + + public string? State { get; set; } + + public string? PostalCode { get; set; } + + public string? Country { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} + +public record UpdateCustomerAddress : CreateCustomerAddress; \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 4ec3897..1703710 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Products.Models; @@ -7,6 +8,52 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static Customer ToModel(this Customers.Entities.Customer entiry) => new() + { + Id = entiry.Id, + Company = entiry.Company, + CreatedAt = entiry.CreatedAt, + Email = entiry.Email, + Enabled = entiry.Enabled, + Phone = entiry.Phone, + SocialMedia = entiry.SocialMedia, + UpdatedAt = entiry.UpdatedAt, + VatNumber = entiry.VatNumber, + Website = entiry.Website + }; + + public static Address ToModel(this Customers.Entities.Address entity) => new() + { + Id = entity.Id, + BuildingType = entity.BuildingType, + CreatedAt = entity.CreatedAt, + CustomerId = entity.CustomerId, + Enabled = entity.Enabled, + IsPrimary = entity.IsPrimary, + Name = entity.Name, + PostalCode = entity.PostalCode, + Type = entity.Type, + UpdatedAt = entity.UpdatedAt, + Street = entity.Street, + City = entity.City, + State = entity.State, + Country = entity.Country + }; + + public static Contact ToModel(this Customers.Entities.Contact entity) => new() + { + Id = entity.Id, + Type = entity.Type, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + CustomerId = entity.CustomerId, + Email = entity.Email, + Enabled = entity.Enabled, + LastName = entity.LastName, + Name = entity.Name, + Phone = entity.Phone + }; + public static BookPage ToModel(this Pages.Entities.BookPage entity) => new() { Id = entity.Id, diff --git a/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs similarity index 97% rename from LiteCharms.Features.MidrandBooks/Pages/PagesService.cs rename to LiteCharms.Features.MidrandBooks/Pages/PageService.cs index 18c822b..5c6f98a 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -1,10 +1,11 @@ -using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Pages; -public class PagesService(IDbContextFactory contextFactory) +public class PageService(IDbContextFactory contextFactory) : IService { public async ValueTask DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 1bb907d..d786b93 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Customers.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities; using LiteCharms.Features.MidrandBooks.Products.Entities; @@ -16,4 +17,10 @@ public class MidrandBooksDbContext(DbContextOptions optio public DbSet Books => Set(); public DbSet Pages => Set(); + + public DbSet Contacts => Set(); + + public DbSet
Addresses => Set
(); + + public DbSet Customers => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 4cc30ab..0232048 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -1,11 +1,12 @@ -using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Products; -public class ProductService(IDbContextFactory contextFactory) +public class ProductService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj index a8478ad..0318fd4 100644 --- a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -23,6 +23,11 @@ utility;dotnet icon.png + + + + + diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 510d826..951c3ff 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -1,5 +1,30 @@ namespace LiteCharms.Features; +public enum ContactTypes : int +{ + Personal = 0, + Business = 1, + Other = 2 +} + +public enum AddressType +{ + Billing = 1, + Shipping = 2, + Other = 3 +} + +public enum AddressBuildingTypes : int +{ + Residential = 0, + Commercial = 1, + Industrial = 2, + MixedUse = 3, + Agricultural = 4, + Institutional = 5, + Recreational = 6, +} + public enum SocialMediaTypes : int { Twitter = 0, From 20b747e89c71fb4ee9962b1b226797fcdacd5d7e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 26 May 2026 00:47:07 +0200 Subject: [PATCH 4/6] Added Order models --- .../LiteCharms.Features.MidrandBooks.csproj | 4 ++ .../Order/Models/Order.cs | 22 +++++++ .../Order/Models/Refund.cs | 20 +++++++ .../Order/Models/Shipping.cs | 14 +++++ .../Order/Models/ShippingProvider.cs | 18 ++++++ .../Orders/Models/Order.cs | 4 +- LiteCharms.Features/Enums.cs | 58 +++++++++++++++++++ 7 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Order/Models/Order.cs create mode 100644 LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs create mode 100644 LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs create mode 100644 LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 552ce45..34c16e0 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -163,4 +163,8 @@ + + + + diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs b/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs new file mode 100644 index 0000000..e0c1c73 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Order.Models; + +public class Order +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long CustomerId { get; set; } + + public OrderStatus Status { get; set; } + + public decimal Total { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Terms { get; set; } + + public string? InvoiceUrl { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs b/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs new file mode 100644 index 0000000..920145e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Order.Models; + +public class Refund +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public Guid OrderId { get; set; } + + public RefundTypes Type { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs b/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs new file mode 100644 index 0000000..975ab91 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.MidrandBooks.Order.Models; + +public class Shipping +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long OrderId { get; set; } + + public ShippingStatuses Status { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs new file mode 100644 index 0000000..8c129b4 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Order.Models; + +public class ShippingProvider +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ShippingProviderTypes Type { get; set; } + + public string? Name { get; set; } + + public decimal? Price { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Orders/Models/Order.cs b/LiteCharms.Features.TechShop/Orders/Models/Order.cs index 2146423..b050337 100644 --- a/LiteCharms.Features.TechShop/Orders/Models/Order.cs +++ b/LiteCharms.Features.TechShop/Orders/Models/Order.cs @@ -1,6 +1,4 @@ -using LiteCharms.Features.TechShop; - -namespace LiteCharms.Features.TechShop.Orders.Models; +namespace LiteCharms.Features.TechShop.Orders.Models; public class Order { diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 951c3ff..9cff19c 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -1,5 +1,63 @@ namespace LiteCharms.Features; +public enum ShippingProviderTypes : int +{ + Dsv = 0, + Pargo = 1, + Ram = 2, + TheCourierGuy = 3, + Paxi = 4, + FastWay = 5, + MdsCollivery = 6, + PostNet = 7, + Aramex = 8, + DHL = 9, + FedEx = 10, + UPS = 11, + USPS = 12, + AmazonLogistics = 13, + LocalCourier = 14, + Other = 15 +} + +public enum ShippingStatuses : int +{ + Pending = 0, + Shipped = 1, + Delivered = 2, + Returned = 3, + Cancelled = 4, +} + +public enum RefundTypes : int +{ + Full = 0, + Partial = 1, + StoreCredit = 2, + Exchange = 3, + Other = 4 +} + +public enum RefundStatus : int +{ + Pending = 0, + Approved = 1, + Rejected = 2, + Completed = 3, + Failed = 4, +} + +public enum OrderStatus : int +{ + Pending = 0, + Completed = 1, + Cancelled = 2, + Failed = 3, + Refunded = 4, + Error = 5, + OnHold = 6, +} + public enum ContactTypes : int { Personal = 0, From 70860efcfb02289578908711c25b236e5e5eb788 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 26 May 2026 08:24:38 +0200 Subject: [PATCH 5/6] Created Order, Refund, Shipping --- .../LiteCharms.Features.MidrandBooks.csproj | 4 - .../Orders/Entities/Order.cs | 11 ++ .../Orders/Entities/OrderConfiguration.cs | 17 +++ .../Orders/Entities/OrderItem.cs | 14 +++ .../Orders/Entities/OrderItemConfiguration.cs | 31 +++++ .../Orders/Entities/Refund.cs | 7 ++ .../Orders/Entities/RefundConfiguration.cs | 22 ++++ .../Orders/Entities/Shipping.cs | 13 +++ .../Orders/Entities/ShippingConfiguration.cs | 32 +++++ .../Orders/Entities/ShippingProvider.cs | 6 + .../Entities/ShippingProviderConfiguration.cs | 17 +++ .../{Order => Orders}/Models/Order.cs | 6 +- .../Orders/Models/OrderItem.cs | 16 +++ .../Orders/Models/Records.cs | 5 + .../{Order => Orders}/Models/Refund.cs | 2 +- .../{Order => Orders}/Models/Shipping.cs | 6 +- .../Models/ShippingProvider.cs | 2 +- .../Orders/OrderService.cs | 109 ++++++++++++++++++ .../Postgres/MidrandBooksDbContext.cs | 11 ++ 19 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs rename LiteCharms.Features.MidrandBooks/{Order => Orders}/Models/Order.cs (69%) create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs rename LiteCharms.Features.MidrandBooks/{Order => Orders}/Models/Refund.cs (85%) rename LiteCharms.Features.MidrandBooks/{Order => Orders}/Models/Shipping.cs (61%) rename LiteCharms.Features.MidrandBooks/{Order => Orders}/Models/ShippingProvider.cs (84%) create mode 100644 LiteCharms.Features.MidrandBooks/Orders/OrderService.cs diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 34c16e0..552ce45 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -163,8 +163,4 @@ - - - - diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs new file mode 100644 index 0000000..ddc8468 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs @@ -0,0 +1,11 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class Order : Models.Order +{ + public virtual Shipping? Shipping { get; set; } + + public virtual ICollection OrderItems { get; set; } = []; + + public virtual ICollection Refunds { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs new file mode 100644 index 0000000..4063456 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs @@ -0,0 +1,17 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Orders"); + + builder.HasKey(o => o.Id); + builder.Property(o => o.CustomerId).IsRequired(); + 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.Notes).HasMaxLength(1000); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs new file mode 100644 index 0000000..d255a04 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs @@ -0,0 +1,14 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class OrderItem : Models.OrderItem +{ + public virtual Order? Order { get; set; } + + public virtual AuthorBook? AuthorBook { get; set; } + + public virtual ProductPrice? ProductPrice { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs new file mode 100644 index 0000000..50ac4da --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs @@ -0,0 +1,31 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class OrderItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("OrderItems"); + + builder.HasKey(oi => oi.Id); + builder.Property(oi => oi.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("new()"); + builder.Property(oi => oi.OrderId).IsRequired(); + builder.Property(oi => oi.AuthorBookId).IsRequired(); + builder.Property(oi => oi.ProductPriceId).IsRequired(); + builder.Property(oi => oi.Quantity).IsRequired(); + + builder.HasOne(oi => oi.Order) + .WithMany(o => o.OrderItems) + .HasForeignKey(oi => oi.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(oi => oi.AuthorBook) + .WithMany() + .HasForeignKey(oi => oi.AuthorBookId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(oi => oi.ProductPrice) + .WithMany() + .HasForeignKey(oi => oi.ProductPriceId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs new file mode 100644 index 0000000..3116896 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class Refund : Models.Refund +{ + public virtual Order? Order { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs new file mode 100644 index 0000000..566b779 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class RefundConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Refunds"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.OrderId).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(o => o.Status).IsRequired(); + builder.Property(r => r.Amount).IsRequired().HasPrecision(18, 2); + builder.Property(r => r.Reason).HasMaxLength(1000); + + builder.HasOne(r => r.Order) + .WithMany(o => o.Refunds) + .HasForeignKey(r => r.OrderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs new file mode 100644 index 0000000..d8f9e07 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs @@ -0,0 +1,13 @@ +using LiteCharms.Features.MidrandBooks.Customers.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class Shipping : Models.Shipping +{ + public virtual Order? Order { get; set; } + + public virtual Address? Address { get; set; } + + public virtual ShippingProvider? ShippingProvider { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs new file mode 100644 index 0000000..2da9465 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs @@ -0,0 +1,32 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class ShippingConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Shippings"); + + builder.HasKey(s => s.Id); + builder.Property(s => s.OrderId).IsRequired(); + builder.Property(s => s.AddressId).IsRequired(); + builder.Property(s => s.ShippingProviderId).IsRequired(); + builder.Property(s => s.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(s => s.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(s => s.Status).IsRequired(); + + builder.HasOne(s => s.Order) + .WithOne(o => o.Shipping) + .HasForeignKey(s => s.OrderId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(s => s.Address) + .WithMany() + .HasForeignKey(s => s.AddressId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.ShippingProvider) + .WithMany(f => f.Shippings) + .HasForeignKey(f => f.ShippingProviderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs new file mode 100644 index 0000000..1fc3127 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class ShippingProvider : Models.ShippingProvider +{ + public virtual ICollection Shippings { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs new file mode 100644 index 0000000..003ab98 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs @@ -0,0 +1,17 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class ShippingProviderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ShippingProviders"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.Type).IsRequired(); + builder.Property(f => f.Name).IsRequired().HasMaxLength(100); + builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Enabled).HasDefaultValue(true); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs similarity index 69% rename from LiteCharms.Features.MidrandBooks/Order/Models/Order.cs rename to LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs index e0c1c73..a86c03e 100644 --- a/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Order.Models; +namespace LiteCharms.Features.MidrandBooks.Orders.Models; public class Order { @@ -14,9 +14,7 @@ public class Order public decimal Total { get; set; } - public string[]? Notes { get; set; } - - public string[]? Terms { get; set; } + public string? Notes { get; set; } public string? InvoiceUrl { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs new file mode 100644 index 0000000..424ad4f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public class OrderItem +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public long OrderId { get; set; } + + public long AuthorBookId { get; set; } + + public long ProductPriceId { get; set; } + + public int Quantity { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs new file mode 100644 index 0000000..a77d3aa --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -0,0 +1,5 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); + +public record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs similarity index 85% rename from LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs rename to LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs index 920145e..3f5eec4 100644 --- a/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Order.Models; +namespace LiteCharms.Features.MidrandBooks.Orders.Models; public class Refund { diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs similarity index 61% rename from LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs rename to LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs index 975ab91..c43bc1f 100644 --- a/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Order.Models; +namespace LiteCharms.Features.MidrandBooks.Orders.Models; public class Shipping { @@ -10,5 +10,9 @@ public class Shipping public long OrderId { get; set; } + public long AddressId { get; set; } + + public long ShippingProviderId { get; set; } + public ShippingStatuses Status { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs similarity index 84% rename from LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs rename to LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs index 8c129b4..76a396e 100644 --- a/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Order.Models; +namespace LiteCharms.Features.MidrandBooks.Orders.Models; public class ShippingProvider { diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs new file mode 100644 index 0000000..9e37122 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -0,0 +1,109 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Orders.Models; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Orders; + +public class OrderService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> CreateOrderAsync(long customerId, CreateOrder request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail("Customer not found."); + + var order = context.Orders.Add(new Entities.Order + { + CustomerId = customerId, + Status = OrderStatus.Pending, + Total = request.TotalPrice + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(order.Entity.Id) + : Result.Fail("Failed to create order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> AddItemToOrderAsync(long orderId, CreateOrderItem request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + if(!await context.Books.AnyAsync(ab => ab.Id == request.AuthorBookId, cancellationToken)) + return Result.Fail("Author book not found."); + + if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken)) + return Result.Fail("Product price not found."); + + var orderItem = context.OrderItems.Add(new Entities.OrderItem + { + OrderId = orderId, + AuthorBookId = request.AuthorBookId, + ProductPriceId = request.ProductPriceId, + Quantity = request.Quantity + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(orderItem.Entity.Id) + : Result.Fail("Failed to add item to order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AddItemsToOrderAsync(long orderId, CreateOrderItem[] items, CancellationToken cancellationToken = default) + { + try + { + if(items.Length == 0) + return Result.Fail("No items to add."); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + 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) + { + if (!await context.Books.AnyAsync(ab => ab.Id == item.AuthorBookId, cancellationToken)) + return Result.Fail($"Author book with ID {item.AuthorBookId} not found."); + + 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 + }); + } + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to add items to order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index d786b93..901933e 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -1,6 +1,7 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; using LiteCharms.Features.MidrandBooks.Authors.Entities; using LiteCharms.Features.MidrandBooks.Customers.Entities; +using LiteCharms.Features.MidrandBooks.Orders.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities; using LiteCharms.Features.MidrandBooks.Products.Entities; @@ -23,4 +24,14 @@ public class MidrandBooksDbContext(DbContextOptions optio public DbSet
Addresses => Set
(); public DbSet Customers => Set(); + + public DbSet Orders => Set(); + + public DbSet OrderItems => Set(); + + public DbSet Refunds => Set(); + + public DbSet Shippings => Set(); + + public DbSet ShippingProviders => Set(); } From 902942eee672f7fab6aa8d3d6cb87769f665ca6a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 27 May 2026 09:12:04 +0200 Subject: [PATCH 6/6] Completed initial database design Sealed qualifying public classes Migrated database changes --- .editorconfig | 39 + .../AuthorBooks/BooksService.cs | 2 +- .../Entities/AuthorBookConfiguration.cs | 2 +- .../Authors/AuthorService.cs | 2 +- .../Authors/Entities/Author.cs | 2 +- .../Authors/Entities/AuthorConfiguration.cs | 2 +- .../Authors/Models/Records.cs | 2 +- .../Customers/CustomerService.cs | 2 +- .../Entities/AddressConfiguration.cs | 2 +- .../Entities/ContactConfiguration.cs | 2 +- .../Entities/CustomerConfiguration.cs | 2 +- .../Customers/Models/Records.cs | 6 +- .../Extensions/Mappers.cs | 45 + .../Extensions/Shop.cs | 23 + .../MidrandShopQuartzHealthCheck.cs | 2 +- .../PostgresMidrandShopHealthCheck.cs | 2 +- .../LiteCharms.Features.MidrandBooks.csproj | 12 +- .../Orders/Entities/Order.cs | 4 +- .../Orders/Entities/OrderConfiguration.cs | 2 +- .../Orders/Entities/OrderItemConfiguration.cs | 4 +- .../Orders/Entities/ShippingConfiguration.cs | 3 +- .../Entities/ShippingProviderConfiguration.cs | 3 +- .../Orders/Models/Records.cs | 10 +- .../Orders/Models/Shipping.cs | 2 + .../Orders/Models/ShippingProvider.cs | 2 + .../Orders/OrderService.cs | 356 ++++++- .../Pages/Entities/BookPageConfiguration.cs | 2 +- .../Pages/Models/UpdateBookPage.cs | 2 +- .../Pages/PageService.cs | 2 +- .../{Orders => Payments}/Entities/Refund.cs | 4 +- .../Entities/RefundConfiguration.cs | 4 +- .../{Orders => Payments}/Models/Refund.cs | 6 +- .../Payments/PaymentService.cs | 7 + .../Postgres/MidrandBooksDbContext.cs | 3 +- .../Postgres/MidrandBooksDbContextFactory.cs | 3 +- .../20260527070840_Init.Designer.cs | 875 ++++++++++++++++++ .../Migrations/20260527070840_Init.cs | 505 ++++++++++ .../MidrandBooksDbContextModelSnapshot.cs | 872 +++++++++++++++++ .../Products/Entities/ProductConfiguration.cs | 2 +- .../Entities/ProductPriceConfiguration.cs | 2 +- .../Products/Models/CreateProductPrice.cs | 2 +- .../Products/Models/Records.cs | 2 +- .../Products/ProductService.cs | 2 +- .../appsettings.json | 22 - ...520191059_AddedProductMetadata.Designer.cs | 1 + .../20260520191059_AddedProductMetadata.cs | 1 + .../Migrations/ShopDbContextModelSnapshot.cs | 1 + .../Products/Models/Records.cs | 4 +- .../Products/ProductService.cs | 3 +- LiteCharms.Features/Abstractions/EventBase.cs | 2 +- .../Email/Configuration/Account.cs | 2 +- .../Email/Configuration/SmtpSettings.cs | 2 +- LiteCharms.Features/Email/EmailService.cs | 77 +- .../Email/Models/Attachment.cs | 2 +- LiteCharms.Features/Email/Models/Body.cs | 2 +- .../Email/Models/BodyProperties.cs | 2 +- LiteCharms.Features/Email/Models/Message.cs | 2 +- LiteCharms.Features/Email/Models/Party.cs | 2 +- LiteCharms.Features/Email/Models/Response.cs | 2 +- LiteCharms.Features/Extensions/Hash.cs | 6 +- LiteCharms.Features/Extensions/Quartz.cs | 2 +- LiteCharms.Features/Extensions/Timezones.cs | 2 +- .../LiteCharms.Features.csproj | 5 + LiteCharms.Features/Models/DateRange.cs | 2 +- LiteCharms.Features/Models/PageReference.cs | 2 +- LiteCharms.Features/Models/ProductFilter.cs | 2 +- LiteCharms.Features/Models/ProductMetadata.cs | 2 +- LiteCharms.Features/Models/SocialMedia.cs | 2 +- LiteCharms.Features/Quartz/JobOrchestrator.cs | 8 +- LiteCharms.Features/Quartz/MediatorJob.cs | 2 +- .../Quartz/RetryJobListener.cs | 2 +- .../S3/Abstractions/S3ServiceBase.cs | 2 +- .../S3/BookshopInvoicesS3Service.cs | 2 +- .../S3/BookshopQuotesS3Service.cs | 2 +- LiteCharms.Features/S3/BookshopS3Service.cs | 2 +- .../S3/Configuration/S3Settings.cs | 2 +- .../ServiceBus/EmailServiceBus.cs | 2 +- .../ServiceBus/Exchanges/EmailExchange.cs | 2 +- .../ServiceBus/Exchanges/GeneralExchange.cs | 2 +- .../ServiceBus/Exchanges/SalesExchange.cs | 2 +- .../ServiceBus/GeneralServiceBus.cs | 2 +- .../ServiceBus/Queues/EmailQueue.cs | 2 +- .../ServiceBus/Queues/GeneralQueue.cs | 2 +- .../ServiceBus/Queues/SalesQueue.cs | 2 +- .../ServiceBus/SalesServiceBus.cs | 2 +- LiteCharmsShared.slnx | 1 + 86 files changed, 2883 insertions(+), 140 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Extensions/Shop.cs rename LiteCharms.Features.MidrandBooks/{Orders => Payments}/Entities/Refund.cs (53%) rename LiteCharms.Features.MidrandBooks/{Orders => Payments}/Entities/RefundConfiguration.cs (84%) rename LiteCharms.Features.MidrandBooks/{Orders => Payments}/Models/Refund.cs (68%) create mode 100644 LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs delete mode 100644 LiteCharms.Features.MidrandBooks/appsettings.json diff --git a/.editorconfig b/.editorconfig index 4a5bdbf..6ab8a85 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,6 +3,45 @@ root = true # C# files [*.cs] +# IDE0250: Prefer make struct 'readonly' +dotnet_diagnostic.IDE0250.severity = warning + +# IDE0060: Remove unused parameters (Good cleanup pairing) +dotnet_diagnostic.IDE0060.severity = warning + +# CA1852: Seal internal types (Available in modern .NET) +dotnet_diagnostic.CA1852.severity = warning + +# MA0018: Add sealed modifier to types that are never inherited +dotnet_diagnostic.MA0018.severity = warning + +# Enforce that classes should be sealed +dotnet_diagnostic.MA0053.severity = warning + +# CRITICAL: Force the analyzer to also flag PUBLIC classes, not just internal ones +meziantou_analyzer.MA0053.public_class_should_be_sealed = true +MA0053.public_class_should_be_sealed = true + +# Keep the rule active as a warning by default +dotnet_diagnostic.MA0048.severity = warning + +# Specific exclusions for Meziantou.Analyzer MA0048 +# Disable the rule for enums +meziantou_analyzer.MA0048.exclude_enums = true + +# Disable the rule for records +meziantou_analyzer.MA0048.exclude_records = true + +#EXCLUDE specific files that are meant to hold grouped enums/records +dotnet_diagnostic.MA0048.severity = warning + +# Disable the requirement to specify ConfigureAwait(false) +dotnet_diagnostic.MA0004.severity = none + +# ALTERNATIVE: Exclude any file ending with 'Enums.cs' or 'Records.cs' +# (e.g., BillingEnums.cs, CustomerRecords.cs) +[**/*{Enums,Records}.cs] +dotnet_diagnostic.MA0048.severity = none #### Core EditorConfig Options #### diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 26a60ec..81ff39f 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.AuthorBooks; -public class BooksService(IDbContextFactory contextFactory) : IService +public sealed class BooksService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs index 3f852ac..1bc1259 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; -public class AuthorBookConfiguration : IEntityTypeConfiguration +public sealed class AuthorBookConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index 43ec4aa..2f71d02 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -7,7 +7,7 @@ using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Authors; -public class AuthorService(IDbContextFactory contextFactory) : IService +public sealed class AuthorService(IDbContextFactory contextFactory) : IService { public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs index 60b6a2b..28ea8da 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs @@ -3,7 +3,7 @@ namespace LiteCharms.Features.MidrandBooks.Authors.Entities; [EntityTypeConfiguration] -public class Author : Models.Author +public sealed class Author : Models.Author { public ICollection Books { get; set; } = []; } diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs index cd6eda1..0c0af10 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Authors.Entities; -public class AuthorConfiguration : IEntityTypeConfiguration +public sealed class AuthorConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs index ba54191..2b6a055 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.MidrandBooks.Authors.Models; -public record UpdateAuthor : CreateAuthor; +public sealed record UpdateAuthor : CreateAuthor; public record CreateAuthor { diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs index fdb7451..d73292d 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Customers; -public class CustomerService(IDbContextFactory contextFactory) : IService +public sealed class CustomerService(IDbContextFactory contextFactory) : IService { public async ValueTask> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs index 6b6997d..4934bac 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Customers.Entities; -public class AddressConfiguration : IEntityTypeConfiguration
+public sealed class AddressConfiguration : IEntityTypeConfiguration
{ public void Configure(EntityTypeBuilder
builder) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs index 8d6fac0..69c931c 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Customers.Entities; -public class ContactConfiguration : IEntityTypeConfiguration +public sealed class ContactConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs index 2b05c59..f5015a7 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Customers.Entities; -public class CustomerConfiguration : IEntityTypeConfiguration +public sealed class CustomerConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs index ceb022c..10c8dae 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs @@ -17,7 +17,7 @@ public record CreateCustomer public SocialMedia[]? SocialMedia { get; set; } } -public record UpdateCustomer : CreateCustomer; +public sealed record UpdateCustomer : CreateCustomer; public record CreateCustomerContact { @@ -32,7 +32,7 @@ public record CreateCustomerContact public string? Phone { get; set; } } -public record UpdateCustomerContact : CreateCustomerContact; +public sealed record UpdateCustomerContact : CreateCustomerContact; public record CreateCustomerAddress { @@ -57,4 +57,4 @@ public record CreateCustomerAddress public bool Enabled { get; set; } } -public record UpdateCustomerAddress : CreateCustomerAddress; \ No newline at end of file +public sealed record UpdateCustomerAddress : CreateCustomerAddress; \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 1703710..72e57d5 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -1,6 +1,7 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Products.Models; @@ -8,6 +9,50 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static ShippingProvider ToModel(this Orders.Entities.ShippingProvider entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + Type = entity.Type, + Price = entity.Price, + Enabled = entity.Enabled + }; + + public static Shipping ToModel(this Orders.Entities.Shipping entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + OrderId = entity.OrderId, + AddressId = entity.AddressId, + Status = entity.Status, + TrackingNumber = entity.TrackingNumber + }; + + public static OrderItem ToModel(this Orders.Entities.OrderItem entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + OrderId = entity.OrderId, + Quantity = entity.Quantity, + AuthorBookId = entity.AuthorBookId, + ProductPriceId = entity.ProductPriceId + }; + + public static Order ToModel(this Orders.Entities.Order entity) => new() + { + Id = entity.Id, + CustomerId = entity.CustomerId, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Status = entity.Status, + Total = entity.Total, + InvoiceUrl = entity.InvoiceUrl, + Notes = entity.Notes + }; + public static Customer ToModel(this Customers.Entities.Customer entiry) => new() { Id = entiry.Id, diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs new file mode 100644 index 0000000..43e407e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -0,0 +1,23 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class Shop +{ + public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime) + { + var serviceType = typeof(IService); + + var implementations = assembly.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); + } + + return services; + } +} diff --git a/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs index 23bdf5a..0de8562 100644 --- a/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.MidrandBooks.HealthChecks; -public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +public sealed class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck { public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs index 5a8b44e..dba8e72 100644 --- a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.MidrandBooks.HealthChecks; -public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck +public sealed class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck { private readonly string connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName)!; diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 552ce45..17db6dd 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -31,6 +31,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -143,6 +147,8 @@ + + @@ -157,10 +163,4 @@ - - - PreserveNewest - - - diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs index ddc8468..657af6f 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Entities; +using LiteCharms.Features.MidrandBooks.Payments.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; [EntityTypeConfiguration] public class Order : Models.Order diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs index 4063456..c7ae590 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Entities; -public class OrderConfiguration : IEntityTypeConfiguration +public sealed class OrderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs index 50ac4da..5a82a1a 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs @@ -1,13 +1,13 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Entities; -public class OrderItemConfiguration : IEntityTypeConfiguration +public sealed class OrderItemConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("OrderItems"); builder.HasKey(oi => oi.Id); - builder.Property(oi => oi.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("new()"); + builder.Property(oi => oi.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(oi => oi.OrderId).IsRequired(); builder.Property(oi => oi.AuthorBookId).IsRequired(); builder.Property(oi => oi.ProductPriceId).IsRequired(); diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs index 2da9465..5938ff2 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Entities; -public class ShippingConfiguration : IEntityTypeConfiguration +public sealed class ShippingConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { @@ -13,6 +13,7 @@ public class ShippingConfiguration : IEntityTypeConfiguration builder.Property(s => s.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(s => s.UpdatedAt).HasDefaultValueSql("now()"); builder.Property(s => s.Status).IsRequired(); + builder.Property(s => s.TrackingNumber).HasMaxLength(255); builder.HasOne(s => s.Order) .WithOne(o => o.Shipping) diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs index 003ab98..83b4755 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Entities; -public class ShippingProviderConfiguration : IEntityTypeConfiguration +public sealed class ShippingProviderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { @@ -13,5 +13,6 @@ public class ShippingProviderConfiguration : IEntityTypeConfiguration f.Name).IsRequired().HasMaxLength(100); builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); builder.Property(f => f.Enabled).HasDefaultValue(true); + builder.Property(f => f.TrackingUrl).HasMaxLength(200); } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs index a77d3aa..f5453bc 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -1,5 +1,11 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Models; -public record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); +public sealed record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); -public record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); +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 CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl); + +public sealed record UpdateShippingProvider(long ProviderId, bool Enabled, string Name, decimal Price, string TrackingUrl); diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs index c43bc1f..51491fe 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs @@ -14,5 +14,7 @@ public class Shipping public long ShippingProviderId { get; set; } + public string? TrackingNumber { get; set; } + public ShippingStatuses Status { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs index 76a396e..a26d934 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs @@ -14,5 +14,7 @@ public class ShippingProvider public decimal? Price { get; set; } + public string? TrackingUrl { get; set; } + public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index 9e37122..c15b2bf 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -1,10 +1,12 @@ using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Orders; -public class OrderService(IDbContextFactory contextFactory) : IService +public sealed class OrderService(IDbContextFactory contextFactory) : IService { public async ValueTask> CreateOrderAsync(long customerId, CreateOrder request, CancellationToken cancellationToken = default) { @@ -106,4 +108,356 @@ public class OrderService(IDbContextFactory contextFactor return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask RemoveItemFromOrderAsync(long orderId, long orderItemId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var orderItem = await context.OrderItems.FirstOrDefaultAsync(oi => oi.Id == orderItemId && oi.OrderId == orderId, cancellationToken); + + if (orderItem is null) + return Result.Fail("Order item not found."); + + context.OrderItems.Remove(orderItem); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to remove item from order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask ClearOrderItemasAsync(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); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to clear order items."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CancelOrderAsync(long orderId, CancellationToken cancellationToken = default) => + await UpdateOrderStatusAsync(orderId, OrderStatus.Cancelled, cancellationToken); + + public async ValueTask> GetOrderAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + return order is not null + ? Result.Ok(order.ToModel()) + : Result.Fail("Order not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrdersByCustomerAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail("Customer not found."); + + var orders = await context.Orders + .AsNoTracking() + .Where(o => o.CustomerId == customerId) + .ToListAsync(cancellationToken); + + return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrdersAsync(DateRange range, int index, CancellationToken cancellationToken = default) + { + try + { + 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) + .ToListAsync(cancellationToken); + + return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateOrderStatusAsync(long orderId, OrderStatus newStatus, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + if (order is null) + return Result.Fail("Order not found."); + + order.UpdatedAt = DateTime.UtcNow; + order.Status = newStatus; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to update order status."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + if(!await context.Addresses.AnyAsync(a => a.Id == request.AddressId, cancellationToken)) + return Result.Fail("Address not found."); + + if(!await context.ShippingProviders.AnyAsync(sp => sp.Id == request.ShippingProviderId && sp.Enabled, cancellationToken)) + return Result.Fail("Shipping provider not found or disabled."); + + if(await context.Shippings.AnyAsync(s => s.OrderId == orderId, cancellationToken)) + return Result.Fail("Shipping already exists for this order."); + + var shipping = context.Shippings.Add(new Entities.Shipping + { + OrderId = orderId, + AddressId = request.AddressId, + ShippingProviderId = request.ShippingProviderId, + Status = ShippingStatuses.Pending, + TrackingNumber = request.TrackingNumber + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to add shipping to order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingStatusAsync(long orderId, ShippingStatuses newStatus, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId, 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 + ? Result.Ok() + : Result.Fail("Failed to update shipping status."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetShippingByOrderIdAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var shipping = await context.Shippings + .AsNoTracking() + .FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken); + + return shipping is not null + ? Result.Ok(shipping.ToModel()) + : Result.Fail("Shipping not found for this order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task RemoveShippingFromOrderAsync(long orderId, long shippingId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(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 + ? Result.Ok() + : Result.Fail("Failed to remove shipping from order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingTrackingNumberAsync(long orderId, long shippingId, string trackingNumber, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, 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 + ? Result.Ok() + : Result.Fail("Failed to update shipping tracking number."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(await context.ShippingProviders.AnyAsync(sp => sp.Type == request.Type, cancellationToken)) + return Result.Fail("Shipping provider with the same type already exists."); + + var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider + { + Name = request.Name, + Type = request.Type, + Price = request.Price, + TrackingUrl = request.TrackingUrl + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to create shipping provider."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShippingProvidersAsync(bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var providers = await context.ShippingProviders.AsNoTracking().Where(sp => sp.Enabled == isEnabled) + .ToListAsync(cancellationToken); + + return Result.Ok(providers.Select(sp => sp.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShippingProviderAsync(long providerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var provider = await context.ShippingProviders.AsNoTracking() + .FirstOrDefaultAsync(sp => sp.Id == providerId, cancellationToken); + + return provider is not null + ? Result.Ok(provider.ToModel()) + : Result.Fail("Shipping provider not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingProviderAsync(UpdateShippingProvider request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var provider = await context.ShippingProviders.FirstOrDefaultAsync(sp => sp.Id == request.ProviderId, 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 + ? Result.Ok() + : Result.Fail("Failed to update shipping provider status."); + } + 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 48c8196..d6c9fed 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Pages.Entities; -public class BookPageConfiguration : IEntityTypeConfiguration +public sealed class BookPageConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs index d78b732..7016067 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs @@ -1,3 +1,3 @@ namespace LiteCharms.Features.MidrandBooks.Pages.Models; -public class UpdateBookPage : CreateBookPage; +public sealed class UpdateBookPage : CreateBookPage; diff --git a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs index 5c6f98a..80f3a5d 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Pages; -public class PageService(IDbContextFactory contextFactory) : IService +public sealed class PageService(IDbContextFactory contextFactory) : IService { public async ValueTask DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs similarity index 53% rename from LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs rename to LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs index 3116896..585ee1a 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Entities; +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; [EntityTypeConfiguration] public class Refund : Models.Refund diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs similarity index 84% rename from LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs rename to LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs index 566b779..5227c6e 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs @@ -1,6 +1,6 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Entities; +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; -public class RefundConfiguration : IEntityTypeConfiguration +public sealed class RefundConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs similarity index 68% rename from LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs rename to LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs index 3f5eec4..5e81d3a 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs @@ -1,14 +1,14 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Models; +namespace LiteCharms.Features.MidrandBooks.Payments.Models; public class Refund { - public Guid Id { get; set; } + public long Id { get; set; } public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } - public Guid OrderId { get; set; } + public long OrderId { get; set; } public RefundTypes Type { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs new file mode 100644 index 0000000..ce3668a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -0,0 +1,7 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; + +namespace LiteCharms.Features.MidrandBooks.Payments; + +public sealed class PaymentService : IService +{ +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 901933e..a70e1cf 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -3,11 +3,12 @@ using LiteCharms.Features.MidrandBooks.Authors.Entities; using LiteCharms.Features.MidrandBooks.Customers.Entities; using LiteCharms.Features.MidrandBooks.Orders.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities; +using LiteCharms.Features.MidrandBooks.Payments.Entities; using LiteCharms.Features.MidrandBooks.Products.Entities; namespace LiteCharms.Features.MidrandBooks.Postgres; -public class MidrandBooksDbContext(DbContextOptions options) : DbContext(options) +public sealed class MidrandBooksDbContext(DbContextOptions options) : DbContext(options) { public DbSet Authors => Set(); diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs index e518cfd..fe25033 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs @@ -2,14 +2,13 @@ namespace LiteCharms.Features.MidrandBooks.Postgres; -public class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory +public sealed class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory { public MidrandBooksDbContext CreateDbContext(string[] args) { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddUserSecrets(typeof(MidrandBooksDbContext).Assembly) - .AddJsonFile("appsettings.json") .AddEnvironmentVariables() .Build(); diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs new file mode 100644 index 0000000..856fd68 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs @@ -0,0 +1,875 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260527070840_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("SocialMedia") + .HasColumnType("jsonb"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialMedia") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("References") + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("Categories") + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasColumnType("numeric"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId") + .IsUnique(); + + b.ToTable("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Book"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null) + .WithOne("Price") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Price"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs new file mode 100644 index 0000000..774c77a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs @@ -0,0 +1,505 @@ +using System; +using LiteCharms.Features.Models; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Authors", + 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, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + PublisherType = table.Column(type: "integer", nullable: false), + Company = table.Column(type: "text", nullable: true), + VatNumber = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + LastName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Biography = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Email = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Authors", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Customers", + 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, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Company = table.Column(type: "text", nullable: true), + VatNumber = table.Column(type: "text", nullable: true), + 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Orders", + 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, defaultValueSql: "now()"), + 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), + Notes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + InvoiceUrl = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + 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, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ShippingProviders", + 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), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "text", nullable: true), + Price = table.Column(type: "numeric", nullable: true), + TrackingUrl = table.Column(type: "text", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShippingProviders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CustomerId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + BuildingType = table.Column(type: "integer", nullable: false), + Street = table.Column(type: "text", nullable: false), + City = table.Column(type: "text", nullable: false), + State = table.Column(type: "text", nullable: false), + PostalCode = table.Column(type: "text", nullable: false), + Country = table.Column(type: "text", nullable: false), + IsPrimary = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + table.ForeignKey( + name: "FK_Addresses_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Contacts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CustomerId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + IsPrimary = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Contacts", x => x.Id); + table.ForeignKey( + name: "FK_Contacts_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Refunds", + 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, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Reason = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Refunds", x => x.Id); + table.ForeignKey( + name: "FK_Refunds_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Books", + 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), + AuthorId = table.Column(type: "bigint", nullable: false), + ProductId = table.Column(type: "bigint", nullable: false), + Rating = table.Column(type: "integer", nullable: false), + Ranking = table.Column(type: "integer", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Books", x => x.Id); + table.ForeignKey( + name: "FK_Books_Authors_AuthorId", + column: x => x.AuthorId, + principalTable: "Authors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Books_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Prices", + 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, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + ProductId = table.Column(type: "bigint", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Prices", x => x.Id); + table.ForeignKey( + name: "FK_Prices_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + 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: "Shippings", + 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, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + AddressId = table.Column(type: "bigint", nullable: false), + ShippingProviderId = table.Column(type: "bigint", nullable: false), + TrackingNumber = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Shippings", x => x.Id); + table.ForeignKey( + name: "FK_Shippings_Addresses_AddressId", + column: x => x.AddressId, + principalTable: "Addresses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shippings_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shippings_ShippingProviders_ShippingProviderId", + column: x => x.ShippingProviderId, + principalTable: "ShippingProviders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "BookPages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AuthorBookId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + 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) + }, + constraints: table => + { + table.PrimaryKey("PK_BookPages", x => x.Id); + table.ForeignKey( + name: "FK_BookPages_Books_AuthorBookId", + column: x => x.AuthorBookId, + principalTable: "Books", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "OrderItems", + 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, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + AuthorBookId = table.Column(type: "bigint", nullable: false), + ProductPriceId = table.Column(type: "bigint", nullable: false), + Quantity = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderItems", x => x.Id); + table.ForeignKey( + name: "FK_OrderItems_Books_AuthorBookId", + column: x => x.AuthorBookId, + principalTable: "Books", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrderItems_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderItems_Prices_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "Prices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Addresses_CustomerId", + table: "Addresses", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_BookPages_AuthorBookId", + table: "BookPages", + column: "AuthorBookId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_AuthorId", + table: "Books", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_ProductId", + table: "Books", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Contacts_CustomerId", + table: "Contacts", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_AuthorBookId", + table: "OrderItems", + column: "AuthorBookId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_OrderId", + table: "OrderItems", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_ProductPriceId", + table: "OrderItems", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_Prices_ProductId", + table: "Prices", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductPrice_ProductId", + table: "ProductPrice", + column: "ProductId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Refunds_OrderId", + table: "Refunds", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_AddressId", + table: "Shippings", + column: "AddressId"); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_OrderId", + table: "Shippings", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_ShippingProviderId", + table: "Shippings", + column: "ShippingProviderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BookPages"); + + migrationBuilder.DropTable( + name: "Contacts"); + + migrationBuilder.DropTable( + name: "OrderItems"); + + migrationBuilder.DropTable( + name: "ProductPrice"); + + migrationBuilder.DropTable( + name: "Refunds"); + + migrationBuilder.DropTable( + name: "Shippings"); + + migrationBuilder.DropTable( + name: "Books"); + + migrationBuilder.DropTable( + name: "Prices"); + + migrationBuilder.DropTable( + name: "Addresses"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "ShippingProviders"); + + migrationBuilder.DropTable( + name: "Authors"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Customers"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs new file mode 100644 index 0000000..04c9235 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -0,0 +1,872 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + partial class MidrandBooksDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("SocialMedia") + .HasColumnType("jsonb"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialMedia") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("References") + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("Categories") + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasColumnType("numeric"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId") + .IsUnique(); + + b.ToTable("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Book"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null) + .WithOne("Price") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Price"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs index 71515b2..e360385 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Products.Entities; -public class ProductConfiguration : IEntityTypeConfiguration +public sealed class ProductConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs index 4705ba5..635ec8c 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Products.Entities; -public class ProductPriceConfiguration : IEntityTypeConfiguration +public sealed class ProductPriceConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs index 2fec81e..b237f96 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Products.Models; -public class CreateProductPrice +public sealed class CreateProductPrice { public long ProductId { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs index 642a739..44786dd 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.MidrandBooks.Products.Models; -public record CreateProduct +public sealed record CreateProduct { public required ProductTypes Type { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 0232048..99f1c45 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -6,7 +6,7 @@ using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Products; -public class ProductService(IDbContextFactory contextFactory) : IService +public sealed class ProductService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/appsettings.json b/LiteCharms.Features.MidrandBooks/appsettings.json deleted file mode 100644 index e3261e6..0000000 --- a/LiteCharms.Features.MidrandBooks/appsettings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "Email": { - "Credentials": { - "Username": "shop@litecharms.co.za" - }, - "Port": 465, - "Host": "mail.litecharms.co.za", - "UseSsl": true - }, - "Monitoring": { - "ApiKey": "", - "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", - "ServiceName": "MidrandBooks" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs index 82a7a0c..9013654 100644 --- a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs @@ -1,5 +1,6 @@ // using System; +using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Postgres; using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore; diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs index 64abe3b..06d1891 100644 --- a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs @@ -1,4 +1,5 @@ using System; +using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs index 7fd9a00..8c1946e 100644 --- a/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Postgres; using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore; diff --git a/LiteCharms.Features.TechShop/Products/Models/Records.cs b/LiteCharms.Features.TechShop/Products/Models/Records.cs index 22b7dfb..8c7d274 100644 --- a/LiteCharms.Features.TechShop/Products/Models/Records.cs +++ b/LiteCharms.Features.TechShop/Products/Models/Records.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.TechShop.Products.Models; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.TechShop.Products.Models; public record CreateProduct { diff --git a/LiteCharms.Features.TechShop/Products/ProductService.cs b/LiteCharms.Features.TechShop/Products/ProductService.cs index 7c9eb83..c414262 100644 --- a/LiteCharms.Features.TechShop/Products/ProductService.cs +++ b/LiteCharms.Features.TechShop/Products/ProductService.cs @@ -1,4 +1,5 @@ -using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Extensions; using LiteCharms.Features.TechShop.Postgres; using LiteCharms.Features.TechShop.Products.Models; diff --git a/LiteCharms.Features/Abstractions/EventBase.cs b/LiteCharms.Features/Abstractions/EventBase.cs index 32def99..f64d71e 100644 --- a/LiteCharms.Features/Abstractions/EventBase.cs +++ b/LiteCharms.Features/Abstractions/EventBase.cs @@ -7,7 +7,7 @@ public abstract class EventBase { public Guid Id { get; set; } = Guid.CreateVersion7(); - public DateTimeOffset EnqueueAt { get; set; } = SouthAfricanTimeZone.UtcNow(); + public DateTimeOffset EnqueueAt { get; set; } = (DateTimeOffset)SouthAfricanTimeZone.UtcNow(); public string CorrelationId { get; set; } = Guid.CreateVersion7().ToString(); } diff --git a/LiteCharms.Features/Email/Configuration/Account.cs b/LiteCharms.Features/Email/Configuration/Account.cs index e0c65e7..02694d7 100644 --- a/LiteCharms.Features/Email/Configuration/Account.cs +++ b/LiteCharms.Features/Email/Configuration/Account.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Configuration; -public class Account +public sealed class Account { public string? Username { get; set; } diff --git a/LiteCharms.Features/Email/Configuration/SmtpSettings.cs b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs index 78798f2..27ab574 100644 --- a/LiteCharms.Features/Email/Configuration/SmtpSettings.cs +++ b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Configuration; -public class SmtpSettings +public sealed class SmtpSettings { public Account? Credentials { get; set; } diff --git a/LiteCharms.Features/Email/EmailService.cs b/LiteCharms.Features/Email/EmailService.cs index 28c38e0..00d5f63 100644 --- a/LiteCharms.Features/Email/EmailService.cs +++ b/LiteCharms.Features/Email/EmailService.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.Email.Models; namespace LiteCharms.Features.Email; -public class EmailService(IOptions options) : IDisposable +public sealed class EmailService(IOptions options) : IDisposable { private readonly SmtpSettings settings = options.Value; private readonly SmtpClient client = new(); @@ -16,6 +16,7 @@ public class EmailService(IOptions options) : IDisposable public async ValueTask> SendEmailAsync(Message message, CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Send"); + activity?.SetTag("email.recipient", message.Recipient?.Address); try @@ -27,21 +28,7 @@ public class EmailService(IOptions options) : IDisposable return Result.Fail("Smtp service is disconnected."); } - var email = new MimeMessage(); - email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!)); - email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!)); - email.Subject = message.Subject!; - - var bodyBuilder = new BodyBuilder(); - - if (message.Body!.Properties.HasAttachments) - foreach (var attachment in message.Body?.Attachments!) - bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); - - if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; - if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; - - email.Body = bodyBuilder.ToMessageBody(); + var email = ConstructEmail(message, cancellationToken); var response = await client.SendAsync(email, cancellationToken); @@ -69,21 +56,9 @@ public class EmailService(IOptions options) : IDisposable await DisconnectAsync(cancellationToken); - if (response.Contains("421")) - { - Status = EmailStatuses.TooManyConnections; + var failCheckResult = HandleNegativeResponse(response); - return Result.Fail(response); - } - - if (response.Contains("451")) - { - Status = EmailStatuses.ConnectionAborted; - - return Result.Fail(response); - } - - EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } }); + if (failCheckResult.IsFailed) return failCheckResult; Status = EmailStatuses.Disconnected; @@ -100,6 +75,48 @@ public class EmailService(IOptions options) : IDisposable } } + private static MimeMessage ConstructEmail(Message message, CancellationToken cancellationToken) + { + var email = new MimeMessage(); + email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!)); + email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!)); + email.Subject = message.Subject!; + + var bodyBuilder = new BodyBuilder(); + + if (message.Body!.Properties.HasAttachments) + foreach (var attachment in message.Body?.Attachments!) + bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); + + if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; + if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; + + email.Body = bodyBuilder.ToMessageBody(); + + return email; + } + + private Result HandleNegativeResponse(string response) + { + if (response.Contains("421", StringComparison.Ordinal)) + { + Status = EmailStatuses.TooManyConnections; + + return Result.Fail(response); + } + + if (response.Contains("451", StringComparison.Ordinal)) + { + Status = EmailStatuses.ConnectionAborted; + + return Result.Fail(response); + } + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } }); + + return Result.Fail(response); + } + public async ValueTask> ConnectAsync(CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Connect"); diff --git a/LiteCharms.Features/Email/Models/Attachment.cs b/LiteCharms.Features/Email/Models/Attachment.cs index 6558058..09dbaf8 100644 --- a/LiteCharms.Features/Email/Models/Attachment.cs +++ b/LiteCharms.Features/Email/Models/Attachment.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Attachment +public sealed class Attachment { public string? Name { get; set; } diff --git a/LiteCharms.Features/Email/Models/Body.cs b/LiteCharms.Features/Email/Models/Body.cs index 99156d8..42de5e3 100644 --- a/LiteCharms.Features/Email/Models/Body.cs +++ b/LiteCharms.Features/Email/Models/Body.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Body : IDisposable +public sealed class Body : IDisposable { public string? Message { get; set; } diff --git a/LiteCharms.Features/Email/Models/BodyProperties.cs b/LiteCharms.Features/Email/Models/BodyProperties.cs index 7bdfa01..f3564c3 100644 --- a/LiteCharms.Features/Email/Models/BodyProperties.cs +++ b/LiteCharms.Features/Email/Models/BodyProperties.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class BodyProperties +public sealed class BodyProperties { public bool IsHtml { get; set; } diff --git a/LiteCharms.Features/Email/Models/Message.cs b/LiteCharms.Features/Email/Models/Message.cs index 7432545..44d7f3f 100644 --- a/LiteCharms.Features/Email/Models/Message.cs +++ b/LiteCharms.Features/Email/Models/Message.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Message : IDisposable +public sealed class Message : IDisposable { public Party? Sender { get; set; } diff --git a/LiteCharms.Features/Email/Models/Party.cs b/LiteCharms.Features/Email/Models/Party.cs index 65c2e85..6aab9e3 100644 --- a/LiteCharms.Features/Email/Models/Party.cs +++ b/LiteCharms.Features/Email/Models/Party.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Party +public sealed class Party { public string? Name { get; set; } diff --git a/LiteCharms.Features/Email/Models/Response.cs b/LiteCharms.Features/Email/Models/Response.cs index 6bc1c49..5557095 100644 --- a/LiteCharms.Features/Email/Models/Response.cs +++ b/LiteCharms.Features/Email/Models/Response.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Response +public sealed class Response { public int Code { get; set; } diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index d903299..ca24629 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -2,12 +2,12 @@ public static class Hash { - public static Func StringToSha256Hash = (input) => + public static readonly Func StringToSha256Hash = (input) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!))); - public static Func StreamToSha256Hash = (stream) => + public static readonly Func StreamToSha256Hash = (stream) => Convert.ToHexString(SHA256.HashData(stream)); - public static Func BytesToSha256Hash = (bytes) => + public static readonly Func BytesToSha256Hash = (bytes) => Convert.ToHexString(SHA256.HashData(bytes)); } diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index 6c4c32e..315a973 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -64,7 +64,7 @@ public static class Quartz config.UseDefaultThreadPool(options => options.MaxConcurrency = 1); config.UseTimeZoneConverter(); - config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString()); + config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); config.UsePersistentStore(storage => { diff --git a/LiteCharms.Features/Extensions/Timezones.cs b/LiteCharms.Features/Extensions/Timezones.cs index 3976240..2c2a068 100644 --- a/LiteCharms.Features/Extensions/Timezones.cs +++ b/LiteCharms.Features/Extensions/Timezones.cs @@ -20,7 +20,7 @@ public static class Timezones ? new DateTimeOffset(sourceDateAdjusted.Ticks, SouthAfricanTimeZone.BaseUtcOffset).LocaliseDateTimeOffset(SouthAfricanTimeZone.BaseUtcOffset) : new DateTimeOffset(sourceDateAdjusted.Ticks, timezone!.BaseUtcOffset).LocaliseDateTimeOffset(timezone.BaseUtcOffset); - return DateTimeOffset.Parse(localised!); + return DateTimeOffset.Parse(localised!, CultureInfo.InvariantCulture); } public static DateTime UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone).UtcDateTime; diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index fab834b..552ee0a 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -31,6 +31,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -142,6 +146,7 @@ + diff --git a/LiteCharms.Features/Models/DateRange.cs b/LiteCharms.Features/Models/DateRange.cs index c82cba3..a5616b4 100644 --- a/LiteCharms.Features/Models/DateRange.cs +++ b/LiteCharms.Features/Models/DateRange.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Models; -public class DateRange +public sealed class DateRange { public DateOnly From { get; set; } diff --git a/LiteCharms.Features/Models/PageReference.cs b/LiteCharms.Features/Models/PageReference.cs index ab04eb9..12d53cb 100644 --- a/LiteCharms.Features/Models/PageReference.cs +++ b/LiteCharms.Features/Models/PageReference.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Models; -public class PageReference +public sealed class PageReference { public string? Tag { get; set; } diff --git a/LiteCharms.Features/Models/ProductFilter.cs b/LiteCharms.Features/Models/ProductFilter.cs index c6cc275..bfed022 100644 --- a/LiteCharms.Features/Models/ProductFilter.cs +++ b/LiteCharms.Features/Models/ProductFilter.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Models; -public class ProductFilter +public sealed class ProductFilter { public string? Name { get; set; } diff --git a/LiteCharms.Features/Models/ProductMetadata.cs b/LiteCharms.Features/Models/ProductMetadata.cs index ee193fe..d059f36 100644 --- a/LiteCharms.Features/Models/ProductMetadata.cs +++ b/LiteCharms.Features/Models/ProductMetadata.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Models; -public class ProductMetadata +public sealed class ProductMetadata { public string? Manufacturer { get; set; } diff --git a/LiteCharms.Features/Models/SocialMedia.cs b/LiteCharms.Features/Models/SocialMedia.cs index 296d979..b4f10c2 100644 --- a/LiteCharms.Features/Models/SocialMedia.cs +++ b/LiteCharms.Features/Models/SocialMedia.cs @@ -1,7 +1,7 @@  namespace LiteCharms.Features.Models; -public class SocialMedia +public sealed class SocialMedia { public SocialMediaTypes Type { get; set; } diff --git a/LiteCharms.Features/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs index 4929543..fae63c3 100644 --- a/LiteCharms.Features/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -3,7 +3,7 @@ using LiteCharms.Features.Quartz.Abstractions; namespace LiteCharms.Features.Quartz; -public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator +public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator { public async Task SendAsync(TNotification notification, CancellationToken cancellationToken = default) where TNotification : IEvent @@ -11,7 +11,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra var chainedJobGroup = "onetime-jobs"; var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - var jobKey = new JobKey($"{notification.Name.ToLower()}-{notification.CorrelationId.ToLower()}", chainedJobGroup); + var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}-{notification.CorrelationId.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup); var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); var job = JobBuilder.Create>() @@ -35,7 +35,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra var chainedJobGroup = "scheduled-jobs"; var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - var jobKey = new JobKey($"{notification.Name.ToLower()}", chainedJobGroup); + var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup); var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); var job = JobBuilder.Create>() @@ -53,7 +53,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra .WithDescription($"Scheduled via Main Job at {now:g}") .WithCronSchedule(cronExpression, cron => cron .WithMisfireHandlingInstructionIgnoreMisfires()) - .StartAt(now) + .StartAt((DateTimeOffset)now) .Build(); await scheduler.AddJob(job, replace: true, cancellationToken); diff --git a/LiteCharms.Features/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs index 9b52972..5fc7649 100644 --- a/LiteCharms.Features/Quartz/MediatorJob.cs +++ b/LiteCharms.Features/Quartz/MediatorJob.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.Mediator; namespace LiteCharms.Features.Quartz; [DisallowConcurrentExecution] -public class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent +public sealed class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent { public async Task Execute(IJobExecutionContext context) { diff --git a/LiteCharms.Features/Quartz/RetryJobListener.cs b/LiteCharms.Features/Quartz/RetryJobListener.cs index 968b8bb..cc12662 100644 --- a/LiteCharms.Features/Quartz/RetryJobListener.cs +++ b/LiteCharms.Features/Quartz/RetryJobListener.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Quartz; -public class RetryJobListener : IJobListener +public sealed class RetryJobListener : IJobListener { public string Name => "RetryJobListener"; diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs index 6679d5d..c1b9ae4 100644 --- a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -31,7 +31,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) if(string.IsNullOrWhiteSpace(fileHash)) return Result.Fail("Failed to compute file hash."); - var fileKey = $"{fileHash.ToLower()}{Path.GetExtension(fileName)}"; + var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}"; var putRequest = new PutObjectRequest { diff --git a/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs b/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs index 8093591..5f4ddac 100644 --- a/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs +++ b/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs @@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants; namespace LiteCharms.Features.S3; -public class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : +public sealed class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:BucketName").Value ?? ""; diff --git a/LiteCharms.Features/S3/BookshopQuotesS3Service.cs b/LiteCharms.Features/S3/BookshopQuotesS3Service.cs index 0f87fa0..2362d66 100644 --- a/LiteCharms.Features/S3/BookshopQuotesS3Service.cs +++ b/LiteCharms.Features/S3/BookshopQuotesS3Service.cs @@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants; namespace LiteCharms.Features.S3; -public class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) : +public sealed class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:BucketName").Value ?? ""; diff --git a/LiteCharms.Features/S3/BookshopS3Service.cs b/LiteCharms.Features/S3/BookshopS3Service.cs index aff9cf5..024b829 100644 --- a/LiteCharms.Features/S3/BookshopS3Service.cs +++ b/LiteCharms.Features/S3/BookshopS3Service.cs @@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants; namespace LiteCharms.Features.S3; -public class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) : +public sealed class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{BookshopS3SettingsSection}:BucketName").Value ?? ""; diff --git a/LiteCharms.Features/S3/Configuration/S3Settings.cs b/LiteCharms.Features/S3/Configuration/S3Settings.cs index 6be7460..72984fa 100644 --- a/LiteCharms.Features/S3/Configuration/S3Settings.cs +++ b/LiteCharms.Features/S3/Configuration/S3Settings.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.S3.Configuration; -public class S3Settings +public sealed class S3Settings { public string? ServiceUrl { get; set; } diff --git a/LiteCharms.Features/ServiceBus/EmailServiceBus.cs b/LiteCharms.Features/ServiceBus/EmailServiceBus.cs index c79438e..f83ed8e 100644 --- a/LiteCharms.Features/ServiceBus/EmailServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/EmailServiceBus.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues; namespace LiteCharms.Features.ServiceBus; -public class EmailServiceBus(EmailQueue messages) : IEventBus +public sealed class EmailServiceBus(EmailQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs index 5ccae66..5a74145 100644 --- a/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs @@ -3,7 +3,7 @@ using LiteCharms.Features.ServiceBus.Queues; namespace LiteCharms.Features.ServiceBus.Exchanges; -public class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService +public sealed class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs index c94fb5d..32d84f1 100644 --- a/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ServiceBus.Exchanges; -public class GeneralExchange(GeneralQueue messages) : BackgroundService +public sealed class GeneralExchange(GeneralQueue messages) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs index 645ab49..3705993 100644 --- a/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ServiceBus.Exchanges; -public class SalesExchange(SalesQueue messages) : BackgroundService +public sealed class SalesExchange(SalesQueue messages) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs b/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs index 94edb37..0694c5c 100644 --- a/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues; namespace LiteCharms.Features.ServiceBus; -public class GeneralServiceBus(GeneralQueue messages) : IEventBus +public sealed class GeneralServiceBus(GeneralQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs b/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs index 508ad5f..700a1f1 100644 --- a/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs +++ b/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs @@ -2,4 +2,4 @@ namespace LiteCharms.Features.ServiceBus.Queues; -public class EmailQueue : EventBusQueueBase, IEventBusQueue; +public sealed class EmailQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs b/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs index 3d79a2f..ef155ec 100644 --- a/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs +++ b/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs @@ -2,4 +2,4 @@ namespace LiteCharms.Features.ServiceBus.Queues; -public class GeneralQueue : EventBusQueueBase, IEventBusQueue; +public sealed class GeneralQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs b/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs index 8dc5601..bb76de8 100644 --- a/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs +++ b/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs @@ -2,4 +2,4 @@ namespace LiteCharms.Features.ServiceBus.Queues; -public class SalesQueue : EventBusQueueBase, IEventBusQueue; +public sealed class SalesQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/SalesServiceBus.cs b/LiteCharms.Features/ServiceBus/SalesServiceBus.cs index 853657b..d30050c 100644 --- a/LiteCharms.Features/ServiceBus/SalesServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/SalesServiceBus.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues; namespace LiteCharms.Features.ServiceBus; -public class SalesServiceBus(SalesQueue messages) : IEventBus +public sealed class SalesServiceBus(SalesQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 7b1a9f7..1945799 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -1,6 +1,7 @@ +