From 0e21ec283d4d6aa879b381a8b13520599e14cce5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 31 May 2026 12:05:59 +0200 Subject: [PATCH 1/6] Added payment database objects --- LiteCharms.Features.MidrandBooks/Enums.cs | 62 - .../Extensions/Mappers.cs | 49 + .../Payments/Entities/Payment.cs | 9 + .../Payments/Entities/PaymentConfiguration.cs | 22 + .../Payments/Entities/PaymentGateway.cs | 4 + .../Entities/PaymentGatewayConfiguration.cs | 20 + .../Payments/Entities/PaymentLedger.cs | 16 + .../Entities/PaymentLedgerConfiguration.cs | 41 + .../Payments/Models/Payment.cs | 18 + .../Payments/Models/PaymentGateway.cs | 24 + .../Payments/Models/PaymentLedger.cs | 20 + .../Postgres/MidrandBooksDbContext.cs | 8 + ...0531094401_AddedPaymentObjects.Designer.cs | 1235 +++++++++++++++++ .../20260531094401_AddedPaymentObjects.cs | 185 +++ .../MidrandBooksDbContextModelSnapshot.cs | 228 +++ .../Products/Entities/ProductInventory.cs | 9 + .../Entities/ProductInventoryConfiguration.cs | 27 + .../Products/Models/ProductInventory.cs | 18 + LiteCharms.Features/Enums.cs | 91 ++ 19 files changed, 2024 insertions(+), 62 deletions(-) delete mode 100644 LiteCharms.Features.MidrandBooks/Enums.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs diff --git a/LiteCharms.Features.MidrandBooks/Enums.cs b/LiteCharms.Features.MidrandBooks/Enums.cs deleted file mode 100644 index b3325c2..0000000 --- a/LiteCharms.Features.MidrandBooks/Enums.cs +++ /dev/null @@ -1,62 +0,0 @@ -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/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 1a81b74..0380370 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -4,12 +4,61 @@ using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; +using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Products.Models; namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static PaymentLedger ToModel(this Payments.Entities.PaymentLedger entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CustomerId = entity.CustomerId, + OrderId = entity.OrderId, + PaymentGatewayId = entity.PaymentGatewayId, + PaymentGatewayReference = entity.PaymentGatewayReference, + PaymentId = entity.PaymentId, + Status = entity.Status, + }; + + public static PaymentGateway ToModel(this Payments.Entities.PaymentGateway entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Enabled = entity.Enabled, + IsSandbox = entity.IsSandbox, + MerchantId = entity.MerchantId, + MerchantKey = entity.MerchantKey, + Name = entity.Name, + Passphrase = entity.Passphrase, + Website = entity.Website, + }; + + public static Payment ToModel(this Payments.Entities.Payment entity) => new() + { + Id = entity.Id, + Amount = entity.Amount, + CreatedAt = entity.CreatedAt, + OrderId = entity.OrderId, + Reference = entity.Reference, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + }; + + public static ProductInventory ToModel(this Products.Entities.ProductInventory entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + ProductId = entity.ProductId, + ProductPriceId = entity.ProductPriceId, + Status = entity.Status, + TotalAllocated = entity.TotalAllocated, + TotalReserved = entity.TotalReserved, + }; + public static Category ToModel(this Categories.Entities.Category entity) => new() { Id = entity.Id, diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs new file mode 100644 index 0000000..69e5fcb --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class Payment : Models.Payment +{ + public virtual Order? Order { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs new file mode 100644 index 0000000..d1f32ef --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Payments"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.Reference).IsRequired(); + builder.Property(f => f.OrderId).IsRequired(); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + + builder.HasOne(f => f.Order) + .WithMany() + .HasForeignKey(f => f.OrderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs new file mode 100644 index 0000000..cf47bf9 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs @@ -0,0 +1,4 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class PaymentGateway : Models.PaymentGateway; diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs new file mode 100644 index 0000000..adfb8a5 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Gateways"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt); + builder.Property(f => f.Website).IsRequired(false); + builder.Property(f => f.IsSandbox); + builder.Property(f => f.MerchantKey).IsRequired(); + builder.Property(f => f.MerchantId).IsRequired(); + builder.Property(f => f.Enabled); + builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.Passphrase).IsRequired(); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs new file mode 100644 index 0000000..3bba78b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs @@ -0,0 +1,16 @@ +using LiteCharms.Features.MidrandBooks.Customers.Entities; +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class PaymentLedger : Models.PaymentLedger +{ + public virtual Payment? Payment { get; set; } + + public virtual Order? Order { get; set; } + + public virtual Customer? Customer { get; set; } + + public virtual PaymentGateway? Gateway { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs new file mode 100644 index 0000000..0e51655 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs @@ -0,0 +1,41 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentLedgerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Ledger"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.PaymentGatewayReference).IsRequired(false); + builder.Property(f => f.PaymentGatewayId).IsRequired(false); + builder.Property(f => f.OrderId).IsRequired(); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.PaymentId).IsRequired(); + + builder.HasOne(f => f.Payment) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.PaymentId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Order) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Customer) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.CustomerId); + + builder.HasOne(f => f.Gateway) + .WithMany() + .IsRequired(false) + .HasForeignKey(f => f.PaymentGatewayId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs new file mode 100644 index 0000000..61c22eb --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class Payment +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public decimal Amount { get; set; } + + public long OrderId { get; set; } + + public string? Reference { get; set; } + + public PaymentStatuses Status { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs new file mode 100644 index 0000000..bdb8a69 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs @@ -0,0 +1,24 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class PaymentGateway +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public string? Website { get; set; } + + public string? MerchantId { get; set; } + + public string? MerchantKey { get; set; } + + public string? Passphrase { get; set; } + + public bool IsSandbox { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs new file mode 100644 index 0000000..33f3d25 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class PaymentLedger +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public LedgerStatuses Status { get; set; } + + public long OrderId { get; set; } + + public long PaymentId { get; set; } + + public long CustomerId { get; set; } + + public string? PaymentGatewayReference { get; set; } + + public long? PaymentGatewayId { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 782bb1f..3d0b380 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -40,4 +40,12 @@ public sealed class MidrandBooksDbContext(DbContextOptions Categories => Set(); public DbSet ProductCategories => Set(); + + public DbSet Inventories => Set(); + + public DbSet Payments => Set(); + + public DbSet Gateways => Set(); + + public DbSet Ledger => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs new file mode 100644 index 0000000..9bdf38e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs @@ -0,0 +1,1235 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +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("20260531094401_AddedPaymentObjects")] + partial class AddedPaymentObjects + { + /// + 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("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.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (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("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") + .HasPrecision(18, 2) + .HasColumnType("numeric(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("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + 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.Payment", 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("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", 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("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Passphrase") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", 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("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayReference") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentGatewayId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (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.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("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + 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.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", 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("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (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.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.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .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.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .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.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") + .WithMany() + .HasForeignKey("PaymentGatewayId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Gateway"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + 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.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + 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.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("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs new file mode 100644 index 0000000..b485cd1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs @@ -0,0 +1,185 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedPaymentObjects : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Gateways", + 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), + Name = table.Column(type: "text", nullable: false), + Website = table.Column(type: "text", nullable: true), + MerchantId = table.Column(type: "text", nullable: false), + MerchantKey = table.Column(type: "text", nullable: false), + Passphrase = table.Column(type: "text", nullable: false), + IsSandbox = table.Column(type: "boolean", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Gateways", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Inventories", + 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()"), + Status = table.Column(type: "integer", nullable: false), + ProductId = table.Column(type: "bigint", nullable: false), + ProductPriceId = table.Column(type: "bigint", nullable: false), + TotalAllocated = table.Column(type: "integer", nullable: false), + TotalReserved = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Inventories", x => x.Id); + table.ForeignKey( + name: "FK_Inventories_Prices_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "Prices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Inventories_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Payments", + 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), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + OrderId = table.Column(type: "bigint", nullable: false), + Reference = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Payments", x => x.Id); + table.ForeignKey( + name: "FK_Payments_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Ledger", + 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()"), + Status = table.Column(type: "integer", nullable: false), + OrderId = table.Column(type: "bigint", nullable: false), + PaymentId = table.Column(type: "bigint", nullable: false), + CustomerId = table.Column(type: "bigint", nullable: false), + PaymentGatewayReference = table.Column(type: "text", nullable: true), + PaymentGatewayId = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Ledger", x => x.Id); + table.ForeignKey( + name: "FK_Ledger_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Gateways_PaymentGatewayId", + column: x => x.PaymentGatewayId, + principalTable: "Gateways", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Payments_PaymentId", + column: x => x.PaymentId, + principalTable: "Payments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Inventories_ProductId", + table: "Inventories", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Inventories_ProductPriceId", + table: "Inventories", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_CustomerId", + table: "Ledger", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_OrderId", + table: "Ledger", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_PaymentGatewayId", + table: "Ledger", + column: "PaymentGatewayId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_PaymentId", + table: "Ledger", + column: "PaymentId"); + + migrationBuilder.CreateIndex( + name: "IX_Payments_OrderId", + table: "Payments", + column: "OrderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Inventories"); + + migrationBuilder.DropTable( + name: "Ledger"); + + migrationBuilder.DropTable( + name: "Gateways"); + + migrationBuilder.DropTable( + name: "Payments"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 8af6b80..635ea90 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -536,6 +536,133 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("BookPages", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", 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("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", 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("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Passphrase") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", 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("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayReference") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentGatewayId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => { b.Property("Id") @@ -653,6 +780,43 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("ProductCategories", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", 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("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.Property("Id") @@ -891,6 +1055,51 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("References"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") + .WithMany() + .HasForeignKey("PaymentGatewayId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Gateway"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") @@ -950,6 +1159,25 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Product"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs new file mode 100644 index 0000000..7b43c55 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class ProductInventory : Models.ProductInventory +{ + public virtual Product? Product { get; set; } + + public virtual ProductPrice? Price { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs new file mode 100644 index 0000000..4e68570 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs @@ -0,0 +1,27 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public sealed class ProductInventoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Inventories"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.TotalAllocated).IsRequired(); + builder.Property(f => f.TotalReserved).IsRequired(); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.ProductPriceId).IsRequired(); + + builder.HasOne(f => f.Product) + .WithMany() + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Price) + .WithMany() + .HasForeignKey(f => f.ProductPriceId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs new file mode 100644 index 0000000..3212a0e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductInventory +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public InventoryStatuses Status { get; set; } + + public long ProductId { get; set; } + + public long ProductPriceId { get; set; } + + public int TotalAllocated { get; set; } + + public int TotalReserved { get; set; } +} diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 40aabdc..786d3e5 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -1,5 +1,35 @@ namespace LiteCharms.Features; +public enum InventoryStatuses : int +{ + Adjustment = 0, + Reserved = 1, + Released = 2, + Sold = 3, + Replenished = 4, + Correction = 5, +} + +public enum LedgerStatuses : int +{ + Changed = 0, + Sent = 1, + Received = 2, + Refunded = 3, + Cancelled = 4, + Failed = 5, + Partial = 6, +} + +public enum PaymentStatuses : int +{ + NotPaid = 0, + Paid = 1, + Cancelled = 2, + Requested = 3, + Failed = 4, +} + public enum ShippingProviderTypes : int { Dsv = 0, @@ -114,4 +144,65 @@ public enum Priorities : int Low = 0, Medium = 1, High = 2, +} + +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 } \ No newline at end of file From f88cc42a88a440b606a73a460fe8a09f81cf7da6 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 31 May 2026 18:42:00 +0200 Subject: [PATCH 2/6] Completed payment service implementation --- .../PaymentServiceFeatureTests.cs | 98 +++++++ .../ProductServiceFeatureTests.cs | 41 +++ .../Extensions/Mappers.cs | 13 + .../Payments/Models/Records.cs | 53 ++++ .../Payments/PaymentService.cs | 260 +++++++++++++++++- .../Products/Models/Records.cs | 18 ++ .../Products/ProductService.cs | 118 ++++++++ 7 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs new file mode 100644 index 0000000..3681116 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs @@ -0,0 +1,98 @@ +using LiteCharms.Features.MidrandBooks.Payments; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public sealed class PaymentServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PaymentService paymentService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateRefundAsync_ShouldReturn_ResultWithRefundId() + { + var request = new CreateRefund + { + Amount = 50, + OrderId = 2, + Type = RefundTypes.Partial, + Reason = "Returned damaged book", + Status = RefundStatus.Completed, + }; + + var result = await paymentService.CreateRefundAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new CreateLedgerEntry + { + CustomerId = 1, + OrderId = 1, + PaymentGatewayId = 1, + PaymentGatewayReference = "TEST REFERENCE", + PaymentId = 1, + Status = LedgerStatuses.Received, + }; + + var result = await paymentService.WriteLedgerEntryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetPaymentGatewayAsync_ShouldReturn_ResultWithPaymentGateway() + { + var result = await paymentService.GetPaymentGatewayAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task CreatePaymentGatewayAsync_ShouldReturn_ResultWithGatewayId() + { + var request = new CreatePaymentGateway + { + IsSandbox = true, + MerchantId = "10049307", + MerchantKey = "ju6navn0jcbf0", + Name = "Payfast", + Website = "https://sandbox.payfast.co.za/eng/process", + }; + + var result = await paymentService.CreatePaymentGatewayAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CompletePaymentAsync_ShouldReturn_ResultWithSuccess() + { + var result = await paymentService.CompletePaymentAsync(1, PaymentStatuses.Paid, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdatePaymentAsync_ShouldReturn_ResultWithSuccess() + { + var result = await paymentService.UpdatePaymentAsync(1, 200, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreatePaymentAsync_ShouldReturn_ResultWithPaymentId() + { + var result = await paymentService.CreatePaymentAsync(100, 1, "HASHEDID", fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs index d4e6e4c..827025f 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -9,6 +9,47 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu { private readonly ProductService productService = fixture.Services.GetRequiredService(); + [IntegrationFact] + public async Task CheckProductStockAvailabilityAsync_ShouldReturn_ResultWithProductInventory() + { + var result = await productService.CheckProductStockAvailabilityAsync(1, 1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task ReserveProductInventoryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new ReserveStock + { + ProductId = 1, + ProductPriceId = 1, + Reservation = 100, + }; + + var result = await productService.ReserveProductInventoryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AllocateProductInventoryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new AllocateStock + { + ProductId = 1, + ProductPriceId = 1, + Allocation = 500, + }; + + var result = await productService.AllocateProductInventoryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + [IntegrationFact] public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId() { diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 0380370..e8a8612 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -6,11 +6,24 @@ using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Products.Models; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static Refund ToModel(this Payments.Entities.Refund entity) => new() + { + CreatedAt = entity.CreatedAt, + Amount = entity.Amount, + Id = entity.Id, + OrderId = entity.OrderId, + Reason = entity.Reason, + Status = entity.Status, + Type = entity.Type, + UpdatedAt = entity.UpdatedAt, + }; + public static PaymentLedger ToModel(this Payments.Entities.PaymentLedger entity) => new() { Id = entity.Id, diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs new file mode 100644 index 0000000..2326397 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs @@ -0,0 +1,53 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed record UpdateRefund +{ + public long OrderId { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +}; + +public sealed record CreateRefund +{ + public long OrderId { get; set; } + + public RefundTypes Type { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +} + +public sealed record CreateLedgerEntry +{ + public required LedgerStatuses Status { get; set; } + + public required long OrderId { get; set; } + + public required long PaymentId { get; set; } + + public required long CustomerId { get; set; } + + public string? PaymentGatewayReference { get; set; } + + public long? PaymentGatewayId { get; set; } +} + +public sealed record CreatePaymentGateway +{ + public required string? Name { get; set; } + + public string? Website { get; set; } + + public required string? MerchantId { get; set; } + + public required string? MerchantKey { get; set; } + + public bool IsSandbox { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index ce3668a..5570fb8 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -1,7 +1,265 @@ using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Payments; -public sealed class PaymentService : IService +public sealed class PaymentService(IDbContextFactory contextFactory) : IService { + public async ValueTask> GetRefundAsync(long refundId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.Refunds.AsNoTracking() + .FirstOrDefaultAsync(r => r.Id == refundId, cancellationToken); + + return refund is not null + ? Result.Ok(refund.ToModel()) + : Result.Fail("Could not find refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateRefundAsync(long refundId, UpdateRefund request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail("Order not found"); + + var updatedRows = await context.Refunds + .Where(r => r.Id == refundId && r.OrderId == request.OrderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(r => r.Status, request.Status) + .SetProperty(r => r.Reason, request.Reason) + .SetProperty(r => r.UpdatedAt, DateTime.UtcNow) + .SetProperty(r => r.Amount, request.Amount), cancellationToken); + + return updatedRows > 0 + ? Result.Ok() + : Result.Fail("Failed to update refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateRefundAsync(CreateRefund request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == request.OrderId + && o.Status == OrderStatus.Completed, cancellationToken); + + if (order is null) return Result.Fail("Order not found"); + + if (request.Amount > order.Total) + return Result.Fail("Refund amount cannot be greater than order total"); + + var totalRefundsPaid = await context.Refunds + .Where(r => r.OrderId == request.OrderId) + .SumAsync(r => r.Amount, cancellationToken); + + if (request.Amount > (order.Total - totalRefundsPaid)) + return Result.Fail("Refund amount exceeds amount available for refund"); + + var refund = context.Refunds.Add(new Entities.Refund + { + Amount = request.Amount, + CreatedAt = DateTime.UtcNow, + OrderId = request.OrderId, + Reason = request.Reason, + Status = request.Status, + Type = request.Type, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(refund.Entity.Id) + : Result.Fail("Failed to create refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask WriteLedgerEntryAsync(CreateLedgerEntry request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail("Order not found"); + + if (!await context.Customers.AnyAsync(o => o.Id == request.CustomerId, cancellationToken)) + return Result.Fail("Customer not found"); + + if (!await context.Orders.AnyAsync(oc => oc.Id == request.OrderId && oc.CustomerId == request.CustomerId, cancellationToken)) + return Result.Fail("Customer does not match the order"); + + if (!await context.Payments.AnyAsync(o => o.Id == request.PaymentId && o.OrderId == request.OrderId, cancellationToken)) + return Result.Fail("Payment not found"); + + if (request.PaymentGatewayId is not null) + if (!await context.Gateways.AnyAsync(o => o.Id == request.PaymentGatewayId, cancellationToken)) + return Result.Fail("Gateway not found"); + + context.Ledger.Add(new Entities.PaymentLedger + { + CreatedAt = DateTime.UtcNow, + CustomerId = request.CustomerId, + OrderId = request.OrderId, + PaymentGatewayId = request.PaymentGatewayId, + PaymentGatewayReference = request.PaymentGatewayReference, + PaymentId = request.PaymentId, + Status = request.Status, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to create ledger entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPaymentGatewayAsync(long paymentGatewayId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var gateway = await context.Gateways.AsNoTracking().FirstOrDefaultAsync(g => g.Id == paymentGatewayId, cancellationToken); + + return gateway is not null + ? Result.Ok(gateway.ToModel()) + : Result.Fail("Could not find gateway"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePaymentGatewayAsync(CreatePaymentGateway request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Gateways.AnyAsync(g => g.MerchantId == request.MerchantId && g.MerchantKey == request.MerchantKey, cancellationToken)) + return Result.Fail("A gateway with the same credentials already exists"); + + var gateway = context.Gateways.Add(new Entities.PaymentGateway + { + CreatedAt = DateTime.UtcNow, + Enabled = true, + IsSandbox = request.IsSandbox, + MerchantId = request.MerchantId, + MerchantKey = request.MerchantKey, + Name = request.Name, + Website = request.Website, + Passphrase = "N/A", + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(gateway.Entity.Id) + : Result.Fail("Failed to create payment gateway"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CompletePaymentAsync(long paymentId, PaymentStatuses status, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (status == PaymentStatuses.NotPaid) + return Result.Fail("Cannot finalise a payment using NotPaid status"); + + var updatedRecords = await context.Payments + .Where(p => p.Id == paymentId && p.Status != PaymentStatuses.Paid && p.Status != status) + .ExecuteUpdateAsync(setters => setters + .SetProperty(u => u.Status, status) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return updatedRecords > 0 + ? Result.Ok() + : Result.Fail("Failed to update payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePaymentAsync(long paymentId, decimal amount, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var updatedRecords = await context.Payments + .Where(p => p.Id == paymentId && p.Status == PaymentStatuses.NotPaid) + .ExecuteUpdateAsync(setters => setters + .SetProperty(u => u.Amount, amount) + .SetProperty(u => u.Status, PaymentStatuses.NotPaid) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return updatedRecords > 0 + ? Result.Ok() + : Result.Fail("Failed to update payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePaymentAsync(decimal amount, long orderId, string reference, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Payments.AnyAsync(p => p.OrderId == orderId && p.Amount == amount && p.Status != PaymentStatuses.Paid, cancellationToken)) + return Result.Fail("An order with the same amount already exists in the system"); + + var payment = context.Payments.Add(new Entities.Payment + { + CreatedAt = DateTime.UtcNow, + Amount = amount, + OrderId = orderId, + Reference = reference, + Status = PaymentStatuses.NotPaid, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(payment.Entity.Id) + : Result.Fail("Failed to make payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs index 9c09551..0a5a588 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -2,6 +2,24 @@ namespace LiteCharms.Features.MidrandBooks.Products.Models; +public sealed record ReserveStock +{ + public required long ProductId { get; set; } + + public required long ProductPriceId { get; set; } + + public int Reservation { get; set; } +} + +public sealed record AllocateStock +{ + public required long ProductId { get; set; } + + public required long ProductPriceId { get; set; } + + public int Allocation { get; set; } +} + 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 de96fd8..1682a70 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -4,11 +4,129 @@ using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; +using Org.BouncyCastle.Asn1.Ocsp; namespace LiteCharms.Features.MidrandBooks.Products; public sealed class ProductService(IDbContextFactory contextFactory) : IService { + public async ValueTask> CheckProductStockAvailabilityAsync(long productId, long productPriceId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var inventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == productPriceId && i.ProductId == productId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + return inventory is not null + ? Result.Ok(inventory.ToModel()) + : Result.Fail("Product sold out"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> ReserveProductInventoryAsync(ReserveStock request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var oldInventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + var newAllocation = 0; + var newReservation = 0; + + if (oldInventory is not null) + { + newAllocation = oldInventory.TotalAllocated; + newReservation = oldInventory.TotalReserved + request.Reservation; + } + else + { + newAllocation = 0; + newReservation = request.Reservation; + } + + if (newAllocation - newReservation < 0) + return Result.Fail("Allocation failure: The requested book quantity exceeds current physical inventory availability."); + + var inventory = context.Inventories.Add(new Entities.ProductInventory + { + CreatedAt = DateTime.UtcNow, + ProductId = request.ProductId, + ProductPriceId = request.ProductPriceId, + Status = InventoryStatuses.Reserved, + TotalAllocated = newAllocation, + TotalReserved = newReservation, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(inventory.Entity.Id) + : Result.Fail("Failed to create inventory entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> AllocateProductInventoryAsync(AllocateStock request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var oldInventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + var newAllocation = 0; + var newReservation = 0; + + if (oldInventory is not null) + { + newAllocation = oldInventory.TotalAllocated + request.Allocation; + newReservation = oldInventory.TotalReserved; + } + else + { + newAllocation = request.Allocation; + newReservation = 0; + } + + var inventory = context.Inventories.Add(new Entities.ProductInventory + { + CreatedAt = DateTime.UtcNow, + ProductId = request.ProductId, + ProductPriceId = request.ProductPriceId, + Status = InventoryStatuses.Adjustment, + TotalAllocated = newAllocation, + TotalReserved = newReservation, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(inventory.Entity.Id) + : Result.Fail("Failed to create inventory entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) { try From 48f4cd45f14480cc3b794d47ff1f18adfa9c0ba5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 31 May 2026 19:37:19 +0200 Subject: [PATCH 3/6] Implemented the HashService and its service registration code --- .../Abstractions/IService.cs | 3 - .../AuthorBooks/BooksService.cs | 2 +- .../Authors/AuthorService.cs | 2 +- .../Categories/CategoryService.cs | 2 +- .../Customers/CustomerService.cs | 2 +- .../Extensions/Shop.cs | 2 +- .../Orders/OrderService.cs | 2 +- .../Pages/PageService.cs | 2 +- .../Payments/PaymentService.cs | 2 +- .../Products/ProductService.cs | 2 +- LiteCharms.Features/Abstractions/IService.cs | 3 + LiteCharms.Features/Extensions/Hash.cs | 26 ++-- .../Hasher/Configuration/HasherSettings.cs | 10 ++ LiteCharms.Features/Hasher/HashService.cs | 118 ++++++++++++++++++ .../LiteCharms.Features.csproj | 2 + .../S3/Abstractions/S3ServiceBase.cs | 6 +- 16 files changed, 163 insertions(+), 23 deletions(-) delete mode 100644 LiteCharms.Features.MidrandBooks/Abstractions/IService.cs create mode 100644 LiteCharms.Features/Abstractions/IService.cs create mode 100644 LiteCharms.Features/Hasher/Configuration/HasherSettings.cs create mode 100644 LiteCharms.Features/Hasher/HashService.cs diff --git a/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs b/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs deleted file mode 100644 index 6218faf..0000000 --- a/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs +++ /dev/null @@ -1,3 +0,0 @@ -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 545fe87..2ee001e 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index 3575214..0b30a85 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs index 2a5b9f4..411f163 100644 --- a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs +++ b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs index 03f34c0..5350220 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index f407c5b..fddb438 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; namespace LiteCharms.Features.MidrandBooks.Extensions; diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index 290908b..4ea6252 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs index 26569bc..22db7b7 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index 5570fb8..3dd26dc 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 1682a70..771632b 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features/Abstractions/IService.cs b/LiteCharms.Features/Abstractions/IService.cs new file mode 100644 index 0000000..17ec5e0 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IService.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IService; diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index ca24629..6aa95ed 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -1,13 +1,23 @@ -namespace LiteCharms.Features.Extensions; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.Hasher.Configuration; + +namespace LiteCharms.Features.Extensions; public static class Hash { - public static readonly Func StringToSha256Hash = (input) => - Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!))); + public const string HasherConfigSectionName = "HasherSettings"; - public static readonly Func StreamToSha256Hash = (stream) => - Convert.ToHexString(SHA256.HashData(stream)); + public static IServiceCollection AddHashServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(HasherConfigSectionName)); - public static readonly Func BytesToSha256Hash = (bytes) => - Convert.ToHexString(SHA256.HashData(bytes)); -} + var settings = configuration.GetSection(HasherConfigSectionName).Get(); + + services.AddSingleton(_ => + new Hashids(settings!.Salt, minHashLength: settings.MinHashLength)); + + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs b/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs new file mode 100644 index 0000000..e30fb40 --- /dev/null +++ b/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Hasher.Configuration; + +public sealed class HasherSettings +{ + public string? Salt { get; set; } + + public int MinHashLength { get; set; } + + public string? PayfastPassphrase { get; set; } +} diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs new file mode 100644 index 0000000..853ccb8 --- /dev/null +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -0,0 +1,118 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Hasher.Configuration; + +namespace LiteCharms.Features.Hasher; + +public sealed partial class HashService(IHashids hasher, IOptions options) : IService +{ + private readonly HasherSettings settings = options.Value; + + [System.Text.RegularExpressions.GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")] + private static partial System.Text.RegularExpressions.Regex HexHashRegex(); + + public static readonly Func StringToSha256Hash = (input) => + string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); + + public static readonly Func StreamToSha256Hash = (stream) => + stream is null ? null : Convert.ToHexString(SHA256.HashData(stream)); + + public static readonly Func BytesToSha256Hash = (bytes) => + bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes)); + + public static Result ComputeMd5Hash(string input) + { + if (string.IsNullOrEmpty(input)) + return Result.Fail("Input content cannot be null or empty for MD5 processing."); + + byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input)); + return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); + } + + public Result VerifyPayfastWebhookSignature(IDictionary incomingFormData, string incomingSignature) + { + try + { + if (string.IsNullOrWhiteSpace(incomingSignature)) + return Result.Fail("Validation failed: Missing signature string parameter."); + + // 1. Sort the parameters alphabetically and exclude the signature parameter to prevent recursive checking + var sortedFields = incomingFormData + .Where(field => field.Key != "signature") + .OrderBy(field => field.Key) + .Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}"); + + string payload = string.Join("&", sortedFields); + + // 2. Append the secure, passphrase injected into the container pod from your environment variables + if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase)) + { + payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}"; + } + + // 3. Compute localized hex token + var localHashResult = ComputeMd5Hash(payload); + + if (!localHashResult.IsSuccess) + return Result.Fail(localHashResult.Errors); + + // 4. Constant-time secure text comparison to fully block timing analysis attacks + bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase); + + return Result.Ok(isValid); + } + catch (Exception ex) + { + return Result.Fail(new Error("An error occurred during MD5 verification loop.").CausedBy(ex)); + } + } + + public Result HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input) + ? Result.Fail("Input must be a valid hexadecimal string.") + : Result.Ok(hasher.EncodeHex(input)); + + public Result HashEncodeIntId(int id) => id < 0 + ? Result.Fail("Id cannot be negative.") + : Result.Ok(hasher.Encode(id)); + + public Result HashEncodeLongId(long id) => id < 0 + ? Result.Fail("Id cannot be negative.") + : Result.Ok(hasher.EncodeLong(id)); + + public Result DecodeIntIdHash(string hash) + { + if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); + + int[] decoded = hasher.Decode(hash); + + return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Int hash token."); + } + + public Result DecodeLongIdHash(string hash) + { + if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); + + long[] decoded = hasher.DecodeLong(hash); + + return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Long hash token."); + } + + public Result DecodeHexHash(string hex) + { + try + { + string decoded = hasher.DecodeHex(hex); + + return string.IsNullOrEmpty(decoded) + ? Result.Fail("Invalid or corrupted hex hash.") + : Result.Ok(decoded); + } + catch (FormatException fex) + { + return Result.Fail(new Error("Invalid hash structure.").CausedBy(fex)); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 552ee0a..6079860 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -31,6 +31,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -146,6 +147,7 @@ + diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs index c1b9ae4..3fcb5d8 100644 --- a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -1,4 +1,4 @@ -using static LiteCharms.Features.Extensions.Hash; +using LiteCharms.Features.Hasher; namespace LiteCharms.Features.S3.Abstractions; @@ -26,7 +26,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) stream.Seek(0, SeekOrigin.Begin); - var fileHash = StreamToSha256Hash(stream); + var fileHash = HashService.StreamToSha256Hash(stream); if(string.IsNullOrWhiteSpace(fileHash)) return Result.Fail("Failed to compute file hash."); @@ -39,7 +39,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) Key = fileKey, InputStream = stream, ContentType = contentType, - UseChunkEncoding = false + UseChunkEncoding = false, }; stream.Seek(0, SeekOrigin.Begin); From c4f73fd999bf32d5e1d8712038a9f4b1ec6daec5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 31 May 2026 19:38:03 +0200 Subject: [PATCH 4/6] Removed comments from function blocks --- LiteCharms.Features/Hasher/HashService.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs index 853ccb8..fc18e38 100644 --- a/LiteCharms.Features/Hasher/HashService.cs +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -35,7 +35,6 @@ public sealed partial class HashService(IHashids hasher, IOptions("Validation failed: Missing signature string parameter."); - // 1. Sort the parameters alphabetically and exclude the signature parameter to prevent recursive checking var sortedFields = incomingFormData .Where(field => field.Key != "signature") .OrderBy(field => field.Key) @@ -43,19 +42,14 @@ public sealed partial class HashService(IHashids hasher, IOptions(localHashResult.Errors); - // 4. Constant-time secure text comparison to fully block timing analysis attacks bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase); return Result.Ok(isValid); From 8fe129e19ce5aba964b9c380055aa6acc1b9de5f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 09:15:14 +0200 Subject: [PATCH 5/6] Implemented HashService and tests --- .../CategorySeederService.cs | 2 +- .../Configuration/CdnSettings.cs | 2 +- .../CustomerSeederService.cs | 2 +- .../ProductsSeederService.cs | 2 +- .../AuthorServiceFeatureTests.cs | 2 +- .../Common/Fixture.cs | 1 - .../Leads/LeadService.cs | 5 +- LiteCharms.Features.Tests/Fixture.cs | 1 + .../HashServiceFeatureTests.cs | 152 ++++++++++++++++++ .../LiteCharms.Features.Tests.csproj | 2 + LiteCharms.Features/Hasher/HashService.cs | 46 ++++-- .../LiteCharms.Features.csproj | 2 + LiteCharms.Features/Models/SearchState.cs | 12 -- 13 files changed, 193 insertions(+), 38 deletions(-) create mode 100644 LiteCharms.Features.Tests/HashServiceFeatureTests.cs delete mode 100644 LiteCharms.Features/Models/SearchState.cs diff --git a/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs index 5856761..c4a2aae 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs @@ -3,7 +3,7 @@ using LiteCharms.Features.MidrandBooks.Products; namespace LiteCharms.Features.MidrandBooks.Seed; -public class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features, +public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs index 6eb1107..7b3a904 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Seed.Configuration; -public class CdnSettings +public sealed class CdnSettings { public string? BaseCdn { get; set; } diff --git a/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs index 6814a6d..0a22738 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Orders.Models; namespace LiteCharms.Features.MidrandBooks.Seed; -public class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features, +public sealed class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs index 868a454..cb96bfe 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration; namespace LiteCharms.Features.MidrandBooks.Seed; -public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService, +public sealed class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService, IFeatureManager features, IOptions options, ILogger logger) : BackgroundService { private readonly CdnSettings cdnSettings = options.Value; diff --git a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs index 1a0826a..d74aac1 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Tests; -public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +public class AuthorServiceFeatureTests(Fixture fixture) : IClassFixture { private readonly AuthorService authorService = fixture.Services.GetRequiredService(); diff --git a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs index 6948d48..dae42e0 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs @@ -1,5 +1,4 @@ using LiteCharms.Features.Extensions; -using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; namespace LiteCharms.Features.MidrandBooks.Tests.Common; diff --git a/LiteCharms.Features.TechShop/Leads/LeadService.cs b/LiteCharms.Features.TechShop/Leads/LeadService.cs index 79c9858..4469e03 100644 --- a/LiteCharms.Features.TechShop/Leads/LeadService.cs +++ b/LiteCharms.Features.TechShop/Leads/LeadService.cs @@ -1,9 +1,8 @@ -using LiteCharms.Features.Extensions; +using LiteCharms.Features.Hasher; using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Extensions; using LiteCharms.Features.TechShop.Leads.Models; using LiteCharms.Features.TechShop.Postgres; -using static LiteCharms.Features.Extensions.Hash; namespace LiteCharms.Features.TechShop.Leads; @@ -29,7 +28,7 @@ public class LeadService(IDbContextFactory contextFactory) FeedItemId = request.FeedItemId, Status = LeadStatus.New, TargetId = request.TargetId, - AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") + AttributionHash = HashService.StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") }); return await context.SaveChangesAsync(cancellationToken) > 0 diff --git a/LiteCharms.Features.Tests/Fixture.cs b/LiteCharms.Features.Tests/Fixture.cs index 466d0a0..1ad8e4a 100644 --- a/LiteCharms.Features.Tests/Fixture.cs +++ b/LiteCharms.Features.Tests/Fixture.cs @@ -26,6 +26,7 @@ public class Fixture : IDisposable .AddGarageS3(Configuration) .AddEmailServices(Configuration) .AddSingleton(Configuration) + .AddHashServices(Configuration) .BuildServiceProvider(); Mediator = Services.GetRequiredService(); diff --git a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs new file mode 100644 index 0000000..1515107 --- /dev/null +++ b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs @@ -0,0 +1,152 @@ +using LiteCharms.Features.Hasher; + +namespace LiteCharms.Features.Tests; + +public class HashServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly HashService hashService = fixture.Services.GetRequiredService(); + private readonly string payfastPassphrase = fixture.Configuration.GetSection("HasherSettings:PayfastPassphrase").Value!; + + [Fact] + public void StringToSha256Hash_Should_GenerateHash() + { + var input = "We are the best"; + var expectedHash = "96E17275B53F6BEB7A0D1C4F789F226D3C71CBE398585F25B3028F2B432E78AB"; + + var result = HashService.StringToSha256Hash(input); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void StreamToSha256Hash_Should_GenerateHash() + { + var input = "We are successful"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(input)); + var expectedHash = "C27872EE494B09D72203C98FC858268F3CD3492D62AA7B766A873520C2C73AFB"; + + var result = HashService.StreamToSha256Hash(stream); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void BytesToSha256Hash_Should_GenerateHash() + { + var inputBytes = Encoding.UTF8.GetBytes("We are wealthy"); + var expectedHash = "3876BF98F6E4A8E42B22C40415687D6FF13F0E887F3F508B71852298FC665737"; + + var result = HashService.BytesToSha256Hash(inputBytes); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void ToMd5Hash_Should_GenerateHash() + { + var input = "We manifest our desired destiny"; + var expectedMd5Lowercase = "6c7816869bcebe4634f7afe9c66dfa08"; + + var result = HashService.ToMd5Hash(input); + + Assert.True(result.IsSuccess); + Assert.True(HashService.IsMd5Hash(result.Value)); + Assert.Equal(expectedMd5Lowercase, result.Value); + } + + [Fact] + public void VerifyPayfastWebhookSignature_Should_GenerateHash() + { + var paymentId = hashService.HashEncodeLongId(1001).Value; + + var incomingForm = new Dictionary + { + { "m_payment_id", paymentId }, + { "amount", "350.00" }, + { "item_name", "System Architecture Book" } + }; + + var rawPayload = $"amount=350.00&item_name=System+Architecture+Book&m_payment_id={paymentId}&passphrase={payfastPassphrase}"; + var generatedSignature = HashService.ToMd5Hash(rawPayload).Value; + + var result = hashService.VerifyPayfastWebhookSignature(incomingForm, generatedSignature); + + Assert.True(result.IsSuccess); + Assert.True(result.Value); + } + + [Fact] + public void HashEncodeHex_Should_GenerateHash() + { + var validHexInput = "DEADBEEF42"; + + var result = hashService.HashEncodeHex(validHexInput); + + Assert.True(result.IsSuccess); + Assert.False(string.IsNullOrWhiteSpace(result.Value)); + } + + [Fact] + public void HashEncodeIntId_Should_GenerateHash() + { + int targetId = 42; + + var result = hashService.HashEncodeIntId(targetId); + + Assert.True(result.IsSuccess); + Assert.True(result.Value.Length >= 10); + } + + [Fact] + public void HashEncodeLongId_Should_GenerateHash() + { + long targetId = 9904185012L; + + var result = hashService.HashEncodeLongId(targetId); + + Assert.True(result.IsSuccess); + Assert.True(result.Value.Length >= 10); + } + + [Fact] + public void DecodeIntIdHash_Should_GenerateHash() + { + int originalId = 88041; + var hashedString = hashService.HashEncodeIntId(originalId).Value; + + var result = hashService.DecodeIntIdHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalId, result.Value); + } + + [Fact] + public void DecodeLongIdHash_Should_GenerateHash() + { + long originalId = 9081230491823L; + var hashedString = hashService.HashEncodeLongId(originalId).Value; + + var result = hashService.DecodeLongIdHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalId, result.Value); + } + + [Fact] + public void DecodeHexHash_Should_GenerateHash() + { + var originalHex = "ABCDEF12345"; + var hashedString = hashService.HashEncodeHex(originalHex).Value; + + var result = hashService.DecodeHexHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalHex, result.Value); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj index 6f93280..015c0b1 100644 --- a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -27,6 +27,8 @@ + + diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs index fc18e38..bbe4a47 100644 --- a/LiteCharms.Features/Hasher/HashService.cs +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -7,25 +7,37 @@ public sealed partial class HashService(IHashids hasher, IOptions StringToSha256Hash = (input) => - string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); + [GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex Md5Regex { get; } - public static readonly Func StreamToSha256Hash = (stream) => + [GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex Sha256Regex { get; } + + public static bool IsMd5Hash(string? value) => + !string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value); + + public static bool IsSha256Hash(string? value) => + !string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value); + + public static string? StringToSha256Hash(string? input) => + string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); + + public static string? StreamToSha256Hash(Stream stream) => stream is null ? null : Convert.ToHexString(SHA256.HashData(stream)); - public static readonly Func BytesToSha256Hash = (bytes) => + public static string? BytesToSha256Hash(byte[] bytes) => bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes)); - - public static Result ComputeMd5Hash(string input) + + public static Result ToMd5Hash(string input) { if (string.IsNullOrEmpty(input)) return Result.Fail("Input content cannot be null or empty for MD5 processing."); byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input)); - return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); + return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); } public Result VerifyPayfastWebhookSignature(IDictionary incomingFormData, string incomingSignature) @@ -36,16 +48,16 @@ public sealed partial class HashService(IHashids hasher, IOptions("Validation failed: Missing signature string parameter."); var sortedFields = incomingFormData - .Where(field => field.Key != "signature") - .OrderBy(field => field.Key) - .Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}"); + .Where(field => !string.Equals(field.Key, "signature", StringComparison.OrdinalIgnoreCase)) + .OrderBy(field => field.Key, StringComparer.Ordinal) + .Select(field => $"{field.Key}={WebUtility.UrlEncode(field.Value)}"); string payload = string.Join("&", sortedFields); if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase)) - payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}"; + payload += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}"; - var localHashResult = ComputeMd5Hash(payload); + var localHashResult = ToMd5Hash(payload); if (!localHashResult.IsSuccess) return Result.Fail(localHashResult.Errors); @@ -60,9 +72,9 @@ public sealed partial class HashService(IHashids hasher, IOptions HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input) - ? Result.Fail("Input must be a valid hexadecimal string.") - : Result.Ok(hasher.EncodeHex(input)); + public Result HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input) + ? Result.Fail("Input must be a valid hexadecimal string.") + : Result.Ok(hasher.EncodeHex(input)); public Result HashEncodeIntId(int id) => id < 0 ? Result.Fail("Id cannot be negative.") diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 6079860..df4841b 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -148,6 +148,8 @@ + + diff --git a/LiteCharms.Features/Models/SearchState.cs b/LiteCharms.Features/Models/SearchState.cs deleted file mode 100644 index 6c483cd..0000000 --- a/LiteCharms.Features/Models/SearchState.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace LiteCharms.Features.Models; - -public class SearchState -{ - public string Query { get; private set; } = string.Empty; - - public event Action? OnSearchSubmitted; - - public void UpdateQuery(string newQuery) => Query = newQuery; - - public void SubmitSearch() => OnSearchSubmitted?.Invoke(); -} From f1ef614cbba61889185eb29b1aee003e69794f18 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 09:19:08 +0200 Subject: [PATCH 6/6] Removed passphrase from PaymentGateway --- .../Extensions/Mappers.cs | 1 - .../Entities/PaymentGatewayConfiguration.cs | 1 - .../Payments/Models/PaymentGateway.cs | 2 - .../Payments/PaymentService.cs | 1 - ...edPassphraseFromPaymentGateway.Designer.cs | 1231 +++++++++++++++++ ...804_RemovedPassphraseFromPaymentGateway.cs | 29 + .../MidrandBooksDbContextModelSnapshot.cs | 4 - 7 files changed, 1260 insertions(+), 9 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index e8a8612..118dc79 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -46,7 +46,6 @@ public static class Mappers MerchantId = entity.MerchantId, MerchantKey = entity.MerchantKey, Name = entity.Name, - Passphrase = entity.Passphrase, Website = entity.Website, }; diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs index adfb8a5..43873ed 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs @@ -15,6 +15,5 @@ public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration f.MerchantId).IsRequired(); builder.Property(f => f.Enabled); builder.Property(f => f.Name).IsRequired(); - builder.Property(f => f.Passphrase).IsRequired(); } } diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs index bdb8a69..4c2701c 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs @@ -16,8 +16,6 @@ public class PaymentGateway public string? MerchantKey { get; set; } - public string? Passphrase { get; set; } - public bool IsSandbox { get; set; } public bool Enabled { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index 3dd26dc..58e418b 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -174,7 +174,6 @@ public sealed class PaymentService(IDbContextFactory cont MerchantKey = request.MerchantKey, Name = request.Name, Website = request.Website, - Passphrase = "N/A", }); return await context.SaveChangesAsync(cancellationToken) > 0 diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs new file mode 100644 index 0000000..bcf52cd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs @@ -0,0 +1,1231 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +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("20260601071804_RemovedPassphraseFromPaymentGateway")] + partial class RemovedPassphraseFromPaymentGateway + { + /// + 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("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.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (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("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") + .HasPrecision(18, 2) + .HasColumnType("numeric(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("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + 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.Payment", 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("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", 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("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", 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("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayReference") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentGatewayId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (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.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("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + 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.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", 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("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (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.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.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .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.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .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.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") + .WithMany() + .HasForeignKey("PaymentGatewayId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Gateway"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + 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.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + 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.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("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs new file mode 100644 index 0000000..9401647 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class RemovedPassphraseFromPaymentGateway : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Passphrase", + table: "Gateways"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Passphrase", + table: "Gateways", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 635ea90..79de829 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -604,10 +604,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("Passphrase") - .IsRequired() - .HasColumnType("text"); - b.Property("UpdatedAt") .HasColumnType("timestamp with time zone");