From 70860efcfb02289578908711c25b236e5e5eb788 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 26 May 2026 08:24:38 +0200 Subject: [PATCH] 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(); }