From d9efababa61ab84232d30f49e118b59dab6b0f85 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Fri, 3 Oct 2025 18:35:43 +0100 Subject: [PATCH] Feature: Add product variant price override support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables individual variants to have their own prices, overriding the base product price. **Database Changes:** - Added Price (decimal?, nullable) to ProductVariants table - Added ProductVariantId to OrderItems table with foreign key relationship - Created index on OrderItems.ProductVariantId for performance **API Changes:** - ProductVariantDto: Added Price field - CreateProductVariantDto: Added Price field with validation - UpdateProductVariantDto: Added Price field - OrderItemDto: Added ProductVariantId and ProductVariantName - CreateOrderItemDto: Added ProductVariantId **Business Logic:** - OrderService: Variant price overrides base price (but multi-buy takes precedence) - ProductService: All variant CRUD operations support Price field **Admin UI:** - CreateVariant: Price input with £ symbol and base price placeholder - EditVariant: Price editing with £ symbol - ProductVariants list: Shows variant price or "(base)" indicator **Client Library:** - Updated all DTOs to match server-side changes - Full support for variant pricing in order creation **Migration:** - EF Core migration: 20251003173458_AddVariantPricing - Backward compatible: NULL values supported for existing data **Use Case:** Products with size/color variants can now have different prices: - Small T-shirt: £15.00 (variant override) - Medium T-shirt: £18.00 (uses base price) - Large T-shirt: £20.00 (variant override) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- LittleShop.Client/Models/Order.cs | 5 +- LittleShop.Client/Models/Product.cs | 1 + .../Admin/Views/Products/CreateVariant.cshtml | 10 + .../Admin/Views/Products/EditVariant.cshtml | 10 + .../Views/Products/ProductVariants.cshtml | 11 + LittleShop/DTOs/OrderDto.cs | 6 +- LittleShop/DTOs/ProductDto.cs | 7 + ...251003173458_AddVariantPricing.Designer.cs | 1739 +++++++++++++++++ .../20251003173458_AddVariantPricing.cs | 59 + .../LittleShopContextModelSnapshot.cs | 14 + LittleShop/Models/OrderItem.cs | 3 + LittleShop/Models/ProductVariant.cs | 2 + LittleShop/Services/OrderService.cs | 22 + LittleShop/Services/ProductService.cs | 7 + 14 files changed, 1894 insertions(+), 2 deletions(-) create mode 100644 LittleShop/Migrations/20251003173458_AddVariantPricing.Designer.cs create mode 100644 LittleShop/Migrations/20251003173458_AddVariantPricing.cs diff --git a/LittleShop.Client/Models/Order.cs b/LittleShop.Client/Models/Order.cs index 8e162a7..d0001f2 100644 --- a/LittleShop.Client/Models/Order.cs +++ b/LittleShop.Client/Models/Order.cs @@ -27,7 +27,9 @@ public class OrderItem { public Guid Id { get; set; } public Guid ProductId { get; set; } + public Guid? ProductVariantId { get; set; } public string? ProductName { get; set; } + public string? ProductVariantName { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal TotalPrice { get; set; } @@ -55,6 +57,7 @@ public class CreateOrderItem { public Guid ProductId { get; set; } public Guid? ProductMultiBuyId { get; set; } // Optional: if specified, use multi-buy pricing - public string? SelectedVariant { get; set; } // Optional: variant choice (color/flavor) + public Guid? ProductVariantId { get; set; } // Optional: specific variant (used for variant price override) + public string? SelectedVariant { get; set; } // Optional: variant choice (color/flavor) - deprecated, use ProductVariantId public int Quantity { get; set; } } \ No newline at end of file diff --git a/LittleShop.Client/Models/Product.cs b/LittleShop.Client/Models/Product.cs index e6ebdd7..e11c5c9 100644 --- a/LittleShop.Client/Models/Product.cs +++ b/LittleShop.Client/Models/Product.cs @@ -49,5 +49,6 @@ public class ProductVariant public int StockLevel { get; set; } public decimal? Weight { get; set; } // Optional: override product weight public int? WeightUnit { get; set; } // Optional: override product weight unit + public decimal? Price { get; set; } // Optional: override product price (if null, uses product.Price) public bool IsActive { get; set; } } \ No newline at end of file diff --git a/LittleShop/Areas/Admin/Views/Products/CreateVariant.cshtml b/LittleShop/Areas/Admin/Views/Products/CreateVariant.cshtml index c3f6a40..7d210a5 100644 --- a/LittleShop/Areas/Admin/Views/Products/CreateVariant.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/CreateVariant.cshtml @@ -55,6 +55,16 @@ Track inventory for this specific variant (optional) +
+ +
+ £ + +
+ + Override base price for this variant (optional - defaults to £@product?.Price.ToString("F2")) +
+
diff --git a/LittleShop/Areas/Admin/Views/Products/EditVariant.cshtml b/LittleShop/Areas/Admin/Views/Products/EditVariant.cshtml index df21c4a..70deee4 100644 --- a/LittleShop/Areas/Admin/Views/Products/EditVariant.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/EditVariant.cshtml @@ -51,6 +51,16 @@
+
+ +
+ £ + +
+ + Override base price (optional - defaults to £@product?.Price.ToString("F2")) +
+
diff --git a/LittleShop/Areas/Admin/Views/Products/ProductVariants.cshtml b/LittleShop/Areas/Admin/Views/Products/ProductVariants.cshtml index f780ea7..0539122 100644 --- a/LittleShop/Areas/Admin/Views/Products/ProductVariants.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/ProductVariants.cshtml @@ -33,6 +33,7 @@ Name Type + Price Stock Level Sort Order Status @@ -49,6 +50,16 @@ @variant.VariantType + + @if (variant.Price.HasValue) + { + £@variant.Price.Value.ToString("F2") + } + else + { + £@product?.Price.ToString("F2") (base) + } + @if (variant.StockLevel > 0) { diff --git a/LittleShop/DTOs/OrderDto.cs b/LittleShop/DTOs/OrderDto.cs index d4ee4ef..7ee203c 100644 --- a/LittleShop/DTOs/OrderDto.cs +++ b/LittleShop/DTOs/OrderDto.cs @@ -51,8 +51,10 @@ public class OrderItemDto public Guid Id { get; set; } public Guid ProductId { get; set; } public Guid? ProductMultiBuyId { get; set; } + public Guid? ProductVariantId { get; set; } public string ProductName { get; set; } = string.Empty; public string? ProductMultiBuyName { get; set; } + public string? ProductVariantName { get; set; } public string? SelectedVariant { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } @@ -97,7 +99,9 @@ public class CreateOrderItemDto public Guid? ProductMultiBuyId { get; set; } // Optional: if specified, use multi-buy pricing - public string? SelectedVariant { get; set; } // Optional: variant choice (color/flavor) + public Guid? ProductVariantId { get; set; } // Optional: specific variant (used for variant price override) + + public string? SelectedVariant { get; set; } // Optional: variant choice (color/flavor) - deprecated, use ProductVariantId [Range(1, int.MaxValue)] public int Quantity { get; set; } diff --git a/LittleShop/DTOs/ProductDto.cs b/LittleShop/DTOs/ProductDto.cs index 5007cbb..e14ba30 100644 --- a/LittleShop/DTOs/ProductDto.cs +++ b/LittleShop/DTOs/ProductDto.cs @@ -125,6 +125,7 @@ public class ProductVariantDto public string VariantType { get; set; } = "Standard"; public int SortOrder { get; set; } public int StockLevel { get; set; } + public decimal? Price { get; set; } // If null, uses product.Price public bool IsActive { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } @@ -170,6 +171,9 @@ public class CreateProductVariantDto [Range(0, int.MaxValue)] public int StockLevel { get; set; } = 0; + + [Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than 0")] + public decimal? Price { get; set; } // Optional: If null, uses product.Price } public class UpdateProductMultiBuyDto @@ -205,5 +209,8 @@ public class UpdateProductVariantDto [Range(0, int.MaxValue)] public int? StockLevel { get; set; } + [Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than 0")] + public decimal? Price { get; set; } // Optional: If null, uses product.Price + public bool? IsActive { get; set; } } \ No newline at end of file diff --git a/LittleShop/Migrations/20251003173458_AddVariantPricing.Designer.cs b/LittleShop/Migrations/20251003173458_AddVariantPricing.Designer.cs new file mode 100644 index 0000000..e6cc7fa --- /dev/null +++ b/LittleShop/Migrations/20251003173458_AddVariantPricing.Designer.cs @@ -0,0 +1,1739 @@ +// +using System; +using LittleShop.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LittleShop.Migrations +{ + [DbContext(typeof(LittleShopContext))] + [Migration("20251003173458_AddVariantPricing")] + partial class AddVariantPricing + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + + modelBuilder.Entity("LittleShop.Models.Bot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BotKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("LastConfigSyncAt") + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PersonalityName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlatformDisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("PlatformId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PlatformUsername") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BotKey") + .IsUnique(); + + b.HasIndex("Name"); + + b.HasIndex("Status"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("LittleShop.Models.BotActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ActivityDescription") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ActivityType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BotId") + .HasColumnType("TEXT"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProductId") + .HasColumnType("TEXT"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("SessionIdentifier") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("UserDisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActivityType"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.HasIndex("SessionIdentifier"); + + b.HasIndex("Timestamp"); + + b.HasIndex("BotId", "Timestamp"); + + b.ToTable("BotActivities"); + }); + + modelBuilder.Entity("LittleShop.Models.BotContact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BotId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EncryptedContactData") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("FirstContactDate") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsRecovered") + .HasColumnType("INTEGER"); + + b.Property("LastContactDate") + .HasColumnType("TEXT"); + + b.Property("LastKnownLanguage") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Preferences") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RecoveredAt") + .HasColumnType("TEXT"); + + b.Property("RecoveredFromBotId") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StatusReason") + .HasColumnType("TEXT"); + + b.Property("TelegramUserId") + .HasColumnType("INTEGER"); + + b.Property("TelegramUsername") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("TotalInteractions") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BotId"); + + b.HasIndex("CustomerId"); + + b.ToTable("BotContacts"); + }); + + modelBuilder.Entity("LittleShop.Models.BotMetric", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BotId") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MetricType") + .HasColumnType("INTEGER"); + + b.Property("RecordedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetricType"); + + b.HasIndex("BotId", "RecordedAt"); + + b.ToTable("BotMetrics"); + }); + + modelBuilder.Entity("LittleShop.Models.BotSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BotId") + .HasColumnType("TEXT"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EndedAt") + .HasColumnType("TEXT"); + + b.Property("IsAnonymous") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastActivityAt") + .HasColumnType("TEXT"); + + b.Property("MessageCount") + .HasColumnType("INTEGER"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrderCount") + .HasColumnType("INTEGER"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SessionIdentifier") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("StartedAt") + .HasColumnType("TEXT"); + + b.Property("TotalSpent") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LastActivityAt"); + + b.HasIndex("StartedAt"); + + b.HasIndex("BotId", "SessionIdentifier"); + + b.ToTable("BotSessions"); + }); + + modelBuilder.Entity("LittleShop.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("LittleShop.Models.CryptoPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BTCPayInvoiceId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasColumnType("INTEGER"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("PaidAmount") + .HasColumnType("decimal(18,8)"); + + b.Property("PaidAt") + .HasColumnType("TEXT"); + + b.Property("RequiredAmount") + .HasColumnType("decimal(18,8)"); + + b.Property("SilverPayOrderId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TransactionHash") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("WalletAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BTCPayInvoiceId"); + + b.HasIndex("OrderId"); + + b.HasIndex("WalletAddress"); + + b.ToTable("CryptoPayments"); + }); + + modelBuilder.Entity("LittleShop.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AllowMarketing") + .HasColumnType("INTEGER"); + + b.Property("AllowOrderUpdates") + .HasColumnType("INTEGER"); + + b.Property("AverageOrderValue") + .HasColumnType("decimal(18,2)"); + + b.Property("BlockReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CancelledOrders") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CustomerNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DataRetentionDate") + .HasColumnType("TEXT"); + + b.Property("DisputedOrders") + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FirstOrderDate") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsBlocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("LastActiveAt") + .HasColumnType("TEXT"); + + b.Property("LastOrderDate") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RiskScore") + .HasColumnType("INTEGER"); + + b.Property("SuccessfulOrders") + .HasColumnType("INTEGER"); + + b.Property("TelegramDisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("TelegramFirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TelegramLastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TelegramUserId") + .HasColumnType("INTEGER"); + + b.Property("TelegramUsername") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Timezone") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("TotalOrders") + .HasColumnType("INTEGER"); + + b.Property("TotalSpent") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DataRetentionDate"); + + b.HasIndex("Email"); + + b.HasIndex("LastActiveAt"); + + b.HasIndex("TelegramUserId") + .IsUnique(); + + b.HasIndex("TelegramUsername"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("LittleShop.Models.CustomerMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AdminUserId") + .HasColumnType("TEXT"); + + b.Property("ArchivedAt") + .HasColumnType("TEXT"); + + b.Property("AutoGenerationTrigger") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .HasColumnType("TEXT"); + + b.Property("DeliveredAt") + .HasColumnType("TEXT"); + + b.Property("Direction") + .HasColumnType("INTEGER"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("FailedAt") + .HasColumnType("TEXT"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsArchived") + .HasColumnType("INTEGER"); + + b.Property("IsAutoGenerated") + .HasColumnType("INTEGER"); + + b.Property("IsMarketing") + .HasColumnType("INTEGER"); + + b.Property("IsUrgent") + .HasColumnType("INTEGER"); + + b.Property("NextRetryAt") + .HasColumnType("TEXT"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("ParentMessageId") + .HasColumnType("TEXT"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PlatformMessageId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ReadAt") + .HasColumnType("TEXT"); + + b.Property("RequiresResponse") + .HasColumnType("INTEGER"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("ScheduledFor") + .HasColumnType("TEXT"); + + b.Property("SentAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ThreadId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdminUserId"); + + b.HasIndex("Direction"); + + b.HasIndex("OrderId"); + + b.HasIndex("ParentMessageId"); + + b.HasIndex("ScheduledFor"); + + b.HasIndex("Status"); + + b.HasIndex("ThreadId"); + + b.HasIndex("Type"); + + b.HasIndex("CustomerId", "CreatedAt"); + + b.ToTable("CustomerMessages"); + }); + + modelBuilder.Entity("LittleShop.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AcceptedAt") + .HasColumnType("TEXT"); + + b.Property("AcceptedByUser") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ActualDeliveryDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .HasColumnType("TEXT"); + + b.Property("DispatchedAt") + .HasColumnType("TEXT"); + + b.Property("DispatchedByUser") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ExpectedDeliveryDate") + .HasColumnType("TEXT"); + + b.Property("IdentityReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OnHoldAt") + .HasColumnType("TEXT"); + + b.Property("OnHoldReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("PackedByUser") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PackingStartedAt") + .HasColumnType("TEXT"); + + b.Property("PaidAt") + .HasColumnType("TEXT"); + + b.Property("ShippedAt") + .HasColumnType("TEXT"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ShippingCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ShippingCountry") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ShippingName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ShippingPostCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TrackingNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CustomerId"); + + b.HasIndex("IdentityReference"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("LittleShop.Models.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("ProductId") + .HasColumnType("TEXT"); + + b.Property("ProductMultiBuyId") + .HasColumnType("TEXT"); + + b.Property("ProductVariantId") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("SelectedVariants") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductMultiBuyId"); + + b.HasIndex("ProductVariantId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("LittleShop.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("StockQuantity") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("VariantCollectionId") + .HasColumnType("TEXT"); + + b.Property("VariantsJson") + .HasColumnType("TEXT"); + + b.Property("Weight") + .HasColumnType("decimal(18,4)"); + + b.Property("WeightUnit") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("VariantCollectionId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("LittleShop.Models.ProductMultiBuy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("PricePerUnit") + .HasColumnType("decimal(18,2)"); + + b.Property("ProductId") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("ProductId", "Quantity") + .IsUnique(); + + b.HasIndex("ProductId", "SortOrder"); + + b.ToTable("ProductMultiBuys"); + }); + + modelBuilder.Entity("LittleShop.Models.ProductPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AltText") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("ProductId") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPhotos"); + }); + + modelBuilder.Entity("LittleShop.Models.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("ProductId") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("StockLevel") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("VariantType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Weight") + .HasColumnType("TEXT"); + + b.Property("WeightUnit") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("ProductId", "Name") + .IsUnique(); + + b.HasIndex("ProductId", "SortOrder"); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("LittleShop.Models.PushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .HasColumnType("TEXT"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SubscribedAt") + .HasColumnType("TEXT"); + + b.Property("UserAgent") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("Endpoint") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.HasIndex("SubscribedAt"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("LittleShop.Models.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApprovedAt") + .HasColumnType("TEXT"); + + b.Property("ApprovedByUserId") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsApproved") + .HasColumnType("INTEGER"); + + b.Property("IsVerifiedPurchase") + .HasColumnType("INTEGER"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("ProductId") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedByUserId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CustomerId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsApproved"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.HasIndex("Rating"); + + b.HasIndex("CustomerId", "ProductId") + .IsUnique(); + + b.HasIndex("ProductId", "IsApproved", "IsActive"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("LittleShop.Models.SalesLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Cryptocurrency") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("FiatCurrency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("TEXT"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("ProductId") + .HasColumnType("TEXT"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("SalePriceBTC") + .HasColumnType("decimal(18,8)"); + + b.Property("SalePriceFiat") + .HasColumnType("decimal(18,2)"); + + b.Property("SoldAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.HasIndex("SoldAt"); + + b.HasIndex("ProductId", "SoldAt"); + + b.ToTable("SalesLedgers"); + }); + + modelBuilder.Entity("LittleShop.Models.ShippingRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("MaxDeliveryDays") + .HasColumnType("INTEGER"); + + b.Property("MaxWeight") + .HasColumnType("decimal(18,2)"); + + b.Property("MinDeliveryDays") + .HasColumnType("INTEGER"); + + b.Property("MinWeight") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ShippingRates"); + }); + + modelBuilder.Entity("LittleShop.Models.SystemSetting", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("LittleShop.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("LittleShop.Models.VariantCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertiesJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name"); + + b.ToTable("VariantCollections"); + }); + + modelBuilder.Entity("LittleShop.Models.BotActivity", b => + { + b.HasOne("LittleShop.Models.Bot", "Bot") + .WithMany() + .HasForeignKey("BotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LittleShop.Models.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("LittleShop.Models.Product", "Product") + .WithMany("Activities") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Bot"); + + b.Navigation("Order"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LittleShop.Models.BotContact", b => + { + b.HasOne("LittleShop.Models.Bot", "Bot") + .WithMany() + .HasForeignKey("BotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LittleShop.Models.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId"); + + b.Navigation("Bot"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LittleShop.Models.BotMetric", b => + { + b.HasOne("LittleShop.Models.Bot", "Bot") + .WithMany("Metrics") + .HasForeignKey("BotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bot"); + }); + + modelBuilder.Entity("LittleShop.Models.BotSession", b => + { + b.HasOne("LittleShop.Models.Bot", "Bot") + .WithMany("Sessions") + .HasForeignKey("BotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bot"); + }); + + modelBuilder.Entity("LittleShop.Models.CryptoPayment", b => + { + b.HasOne("LittleShop.Models.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LittleShop.Models.CustomerMessage", b => + { + b.HasOne("LittleShop.Models.User", "AdminUser") + .WithMany() + .HasForeignKey("AdminUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("LittleShop.Models.Customer", "Customer") + .WithMany("Messages") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LittleShop.Models.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("LittleShop.Models.CustomerMessage", "ParentMessage") + .WithMany("Replies") + .HasForeignKey("ParentMessageId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AdminUser"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("ParentMessage"); + }); + + modelBuilder.Entity("LittleShop.Models.Order", b => + { + b.HasOne("LittleShop.Models.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LittleShop.Models.OrderItem", b => + { + b.HasOne("LittleShop.Models.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LittleShop.Models.Product", "Product") + .WithMany("OrderItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LittleShop.Models.ProductMultiBuy", "ProductMultiBuy") + .WithMany("OrderItems") + .HasForeignKey("ProductMultiBuyId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("LittleShop.Models.ProductVariant", "ProductVariant") + .WithMany() + .HasForeignKey("ProductVariantId"); + + b.Navigation("Order"); + + b.Navigation("Product"); + + b.Navigation("ProductMultiBuy"); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("LittleShop.Models.Product", b => + { + b.HasOne("LittleShop.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LittleShop.Models.VariantCollection", "VariantCollection") + .WithMany() + .HasForeignKey("VariantCollectionId"); + + b.Navigation("Category"); + + b.Navigation("VariantCollection"); + }); + + modelBuilder.Entity("LittleShop.Models.ProductMultiBuy", b => + { + b.HasOne("LittleShop.Models.Product", "Product") + .WithMany("MultiBuys") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LittleShop.Models.ProductPhoto", b => + { + b.HasOne("LittleShop.Models.Product", "Product") + .WithMany("Photos") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LittleShop.Models.ProductVariant", b => + { + b.HasOne("LittleShop.Models.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LittleShop.Models.PushSubscription", b => + { + b.HasOne("LittleShop.Models.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LittleShop.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Customer"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LittleShop.Models.Review", b => + { + b.HasOne("LittleShop.Models.User", "ApprovedByUser") + .WithMany() + .HasForeignKey("ApprovedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("LittleShop.Models.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LittleShop.Models.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LittleShop.Models.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApprovedByUser"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LittleShop.Models.SalesLedger", b => + { + b.HasOne("LittleShop.Models.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LittleShop.Models.Product", "Product") + .WithMany("SalesLedgers") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LittleShop.Models.Bot", b => + { + b.Navigation("Metrics"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("LittleShop.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("LittleShop.Models.Customer", b => + { + b.Navigation("Messages"); + + b.Navigation("Orders"); + }); + + modelBuilder.Entity("LittleShop.Models.CustomerMessage", b => + { + b.Navigation("Replies"); + }); + + modelBuilder.Entity("LittleShop.Models.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("LittleShop.Models.Product", b => + { + b.Navigation("Activities"); + + b.Navigation("MultiBuys"); + + b.Navigation("OrderItems"); + + b.Navigation("Photos"); + + b.Navigation("Reviews"); + + b.Navigation("SalesLedgers"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("LittleShop.Models.ProductMultiBuy", b => + { + b.Navigation("OrderItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LittleShop/Migrations/20251003173458_AddVariantPricing.cs b/LittleShop/Migrations/20251003173458_AddVariantPricing.cs new file mode 100644 index 0000000..40b60f6 --- /dev/null +++ b/LittleShop/Migrations/20251003173458_AddVariantPricing.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LittleShop.Migrations +{ + /// + public partial class AddVariantPricing : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Price", + table: "ProductVariants", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "ProductVariantId", + table: "OrderItems", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_ProductVariantId", + table: "OrderItems", + column: "ProductVariantId"); + + migrationBuilder.AddForeignKey( + name: "FK_OrderItems_ProductVariants_ProductVariantId", + table: "OrderItems", + column: "ProductVariantId", + principalTable: "ProductVariants", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrderItems_ProductVariants_ProductVariantId", + table: "OrderItems"); + + migrationBuilder.DropIndex( + name: "IX_OrderItems_ProductVariantId", + table: "OrderItems"); + + migrationBuilder.DropColumn( + name: "Price", + table: "ProductVariants"); + + migrationBuilder.DropColumn( + name: "ProductVariantId", + table: "OrderItems"); + } + } +} diff --git a/LittleShop/Migrations/LittleShopContextModelSnapshot.cs b/LittleShop/Migrations/LittleShopContextModelSnapshot.cs index 2a97c6a..63a3e63 100644 --- a/LittleShop/Migrations/LittleShopContextModelSnapshot.cs +++ b/LittleShop/Migrations/LittleShopContextModelSnapshot.cs @@ -859,6 +859,9 @@ namespace LittleShop.Migrations b.Property("ProductMultiBuyId") .HasColumnType("TEXT"); + b.Property("ProductVariantId") + .HasColumnType("TEXT"); + b.Property("Quantity") .HasColumnType("INTEGER"); @@ -880,6 +883,8 @@ namespace LittleShop.Migrations b.HasIndex("ProductMultiBuyId"); + b.HasIndex("ProductVariantId"); + b.ToTable("OrderItems"); }); @@ -1041,6 +1046,9 @@ namespace LittleShop.Migrations .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("Price") + .HasColumnType("TEXT"); + b.Property("ProductId") .HasColumnType("TEXT"); @@ -1537,11 +1545,17 @@ namespace LittleShop.Migrations .HasForeignKey("ProductMultiBuyId") .OnDelete(DeleteBehavior.Restrict); + b.HasOne("LittleShop.Models.ProductVariant", "ProductVariant") + .WithMany() + .HasForeignKey("ProductVariantId"); + b.Navigation("Order"); b.Navigation("Product"); b.Navigation("ProductMultiBuy"); + + b.Navigation("ProductVariant"); }); modelBuilder.Entity("LittleShop.Models.Product", b => diff --git a/LittleShop/Models/OrderItem.cs b/LittleShop/Models/OrderItem.cs index 7a69c83..004a1e8 100644 --- a/LittleShop/Models/OrderItem.cs +++ b/LittleShop/Models/OrderItem.cs @@ -14,6 +14,8 @@ public class OrderItem public Guid? ProductMultiBuyId { get; set; } // Nullable for backward compatibility + public Guid? ProductVariantId { get; set; } // Optional: specific variant with potential price override + [StringLength(500)] public string? SelectedVariants { get; set; } // JSON array of variants chosen (e.g., ["Red", "Blue", "Green"] for multi-buy) @@ -29,4 +31,5 @@ public class OrderItem public virtual Order Order { get; set; } = null!; public virtual Product Product { get; set; } = null!; public virtual ProductMultiBuy? ProductMultiBuy { get; set; } + public virtual ProductVariant? ProductVariant { get; set; } } \ No newline at end of file diff --git a/LittleShop/Models/ProductVariant.cs b/LittleShop/Models/ProductVariant.cs index c194723..0c41f74 100644 --- a/LittleShop/Models/ProductVariant.cs +++ b/LittleShop/Models/ProductVariant.cs @@ -27,6 +27,8 @@ public class ProductVariant public ProductWeightUnit? WeightUnit { get; set; } // Optional: override product weight unit for this variant + public decimal? Price { get; set; } // Optional: override product price for this variant (if null, uses product.Price) + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; diff --git a/LittleShop/Services/OrderService.cs b/LittleShop/Services/OrderService.cs index 7f2f832..02fdeb8 100644 --- a/LittleShop/Services/OrderService.cs +++ b/LittleShop/Services/OrderService.cs @@ -78,6 +78,8 @@ public class OrderService : IOrderService .ThenInclude(oi => oi.Product) .Include(o => o.Items) .ThenInclude(oi => oi.ProductMultiBuy) + .Include(o => o.Items) + .ThenInclude(oi => oi.ProductVariant) .Include(o => o.Payments) .FirstOrDefaultAsync(o => o.Id == id); @@ -147,8 +149,25 @@ public class OrderService : IOrderService } ProductMultiBuy? multiBuy = null; + ProductVariant? variant = null; decimal unitPrice = product.Price; + // Check for variant price override (highest priority after multi-buy) + if (itemDto.ProductVariantId.HasValue) + { + variant = await _context.ProductVariants.FindAsync(itemDto.ProductVariantId.Value); + if (variant == null || !variant.IsActive || variant.ProductId != itemDto.ProductId) + { + throw new ArgumentException($"Product variant {itemDto.ProductVariantId} not found, inactive, or doesn't belong to product {itemDto.ProductId}"); + } + + // Use variant price if it has an override, otherwise use product base price + if (variant.Price.HasValue) + { + unitPrice = variant.Price.Value; + } + } + if (itemDto.ProductMultiBuyId.HasValue) { multiBuy = await _context.ProductMultiBuys.FindAsync(itemDto.ProductMultiBuyId.Value); @@ -168,6 +187,7 @@ public class OrderService : IOrderService OrderId = order.Id, ProductId = itemDto.ProductId, ProductMultiBuyId = itemDto.ProductMultiBuyId, + ProductVariantId = itemDto.ProductVariantId, Quantity = itemDto.Quantity, UnitPrice = unitPrice, TotalPrice = unitPrice * itemDto.Quantity @@ -331,8 +351,10 @@ public class OrderService : IOrderService Id = oi.Id, ProductId = oi.ProductId, ProductMultiBuyId = oi.ProductMultiBuyId, + ProductVariantId = oi.ProductVariantId, ProductName = oi.Product.Name, ProductMultiBuyName = oi.ProductMultiBuy?.Name, + ProductVariantName = oi.ProductVariant?.Name, Quantity = oi.Quantity, UnitPrice = oi.UnitPrice, TotalPrice = oi.TotalPrice diff --git a/LittleShop/Services/ProductService.cs b/LittleShop/Services/ProductService.cs index 9257428..bc61f88 100644 --- a/LittleShop/Services/ProductService.cs +++ b/LittleShop/Services/ProductService.cs @@ -556,6 +556,7 @@ public class ProductService : IProductService VariantType = createVariantDto.VariantType, SortOrder = createVariantDto.SortOrder, StockLevel = createVariantDto.StockLevel, + Price = createVariantDto.Price, IsActive = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow @@ -572,6 +573,7 @@ public class ProductService : IProductService VariantType = variant.VariantType, SortOrder = variant.SortOrder, StockLevel = variant.StockLevel, + Price = variant.Price, IsActive = variant.IsActive, CreatedAt = variant.CreatedAt, UpdatedAt = variant.UpdatedAt @@ -595,6 +597,9 @@ public class ProductService : IProductService if (updateVariantDto.StockLevel.HasValue) variant.StockLevel = updateVariantDto.StockLevel.Value; + if (updateVariantDto.Price.HasValue) + variant.Price = updateVariantDto.Price.Value; + if (updateVariantDto.IsActive.HasValue) variant.IsActive = updateVariantDto.IsActive.Value; @@ -627,6 +632,7 @@ public class ProductService : IProductService VariantType = v.VariantType, SortOrder = v.SortOrder, StockLevel = v.StockLevel, + Price = v.Price, IsActive = v.IsActive, CreatedAt = v.CreatedAt, UpdatedAt = v.UpdatedAt @@ -647,6 +653,7 @@ public class ProductService : IProductService VariantType = variant.VariantType, SortOrder = variant.SortOrder, StockLevel = variant.StockLevel, + Price = variant.Price, IsActive = variant.IsActive, CreatedAt = variant.CreatedAt, UpdatedAt = variant.UpdatedAt