Implement product multi-buys and variants system

Major restructuring of product variations:
- Renamed ProductVariation to ProductMultiBuy for quantity-based pricing (e.g., "3 for £25")
- Added new ProductVariant model for string-based options (colors, flavors)
- Complete separation of multi-buy pricing from variant selection

Features implemented:
- Multi-buy deals with automatic price-per-unit calculation
- Product variants for colors/flavors/sizes with stock tracking
- TeleBot checkout supports both multi-buys and variant selection
- Shopping cart correctly calculates multi-buy bundle prices
- Order system tracks selected variants and multi-buy choices
- Real-time bot activity monitoring with SignalR
- Public bot directory page with QR codes for Telegram launch
- Admin dashboard shows multi-buy and variant metrics

Technical changes:
- Updated all DTOs, services, and controllers
- Fixed cart total calculation for multi-buy bundles
- Comprehensive test coverage for new functionality
- All existing tests passing with new features

Database changes:
- Migrated ProductVariations to ProductMultiBuys
- Added ProductVariants table
- Updated OrderItems to track variants

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-21 00:30:12 +01:00
parent 7683b7dfe5
commit 034b8facee
46 changed files with 3190 additions and 332 deletions

View File

@@ -13,7 +13,9 @@ public class LittleShopContext : DbContext
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<ProductPhoto> ProductPhotos { get; set; }
public DbSet<ProductVariation> ProductVariations { get; set; }
public DbSet<ProductMultiBuy> ProductMultiBuys { get; set; }
public DbSet<ProductVariant> ProductVariants { get; set; }
public DbSet<BotActivity> BotActivities { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
public DbSet<CryptoPayment> CryptoPayments { get; set; }
@@ -54,30 +56,72 @@ public class LittleShopContext : DbContext
.HasForeignKey(pp => pp.ProductId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasMany(p => p.Variations)
entity.HasMany(p => p.MultiBuys)
.WithOne(pmb => pmb.Product)
.HasForeignKey(pmb => pmb.ProductId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasMany(p => p.Variants)
.WithOne(pv => pv.Product)
.HasForeignKey(pv => pv.ProductId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasMany(p => p.Activities)
.WithOne(ba => ba.Product)
.HasForeignKey(ba => ba.ProductId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasMany(p => p.OrderItems)
.WithOne(oi => oi.Product)
.HasForeignKey(oi => oi.ProductId)
.OnDelete(DeleteBehavior.Restrict);
});
// ProductVariation entity
modelBuilder.Entity<ProductVariation>(entity =>
// ProductMultiBuy entity
modelBuilder.Entity<ProductMultiBuy>(entity =>
{
entity.HasIndex(e => new { e.ProductId, e.Quantity }).IsUnique(); // One variation per quantity per product
entity.HasIndex(e => new { e.ProductId, e.Quantity }).IsUnique(); // One multi-buy per quantity per product
entity.HasIndex(e => new { e.ProductId, e.SortOrder });
entity.HasIndex(e => e.IsActive);
entity.HasMany(pv => pv.OrderItems)
.WithOne(oi => oi.ProductVariation)
.HasForeignKey(oi => oi.ProductVariationId)
entity.HasMany(pmb => pmb.OrderItems)
.WithOne(oi => oi.ProductMultiBuy)
.HasForeignKey(oi => oi.ProductMultiBuyId)
.OnDelete(DeleteBehavior.Restrict);
});
// ProductVariant entity
modelBuilder.Entity<ProductVariant>(entity =>
{
entity.HasIndex(e => new { e.ProductId, e.Name }).IsUnique(); // Unique variant names per product
entity.HasIndex(e => new { e.ProductId, e.SortOrder });
entity.HasIndex(e => e.IsActive);
});
// BotActivity entity
modelBuilder.Entity<BotActivity>(entity =>
{
entity.HasOne(ba => ba.Bot)
.WithMany()
.HasForeignKey(ba => ba.BotId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(ba => ba.Product)
.WithMany(p => p.Activities)
.HasForeignKey(ba => ba.ProductId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(ba => ba.Order)
.WithMany()
.HasForeignKey(ba => ba.OrderId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasIndex(e => new { e.BotId, e.Timestamp });
entity.HasIndex(e => e.SessionIdentifier);
entity.HasIndex(e => e.ActivityType);
entity.HasIndex(e => e.Timestamp);
});
// Order entity
modelBuilder.Entity<Order>(entity =>
{