From f7101800cdf7b6406e6c51a6e08f2a2f9643ec70 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Fri, 21 Aug 2026 14:10:40 +0530 Subject: [PATCH] Add scheme data model and migrations --- Distribution/Data/AppDBContext.cs | 49 ++++ .../20260821120000_AddPromotionsSchemes.cs | 129 ++++++++++ ...dSchemeFreeGoodsRuleAndBundleComponents.cs | 170 +++++++++++++ .../Migrations/AppDBContextModelSnapshot.cs | 225 ++++++++++++++++++ Distribution/Models/Item.cs | 3 + Distribution/Models/Scheme.cs | 62 +++++ Distribution/Models/SchemeBundleComponent.cs | 20 ++ Distribution/Models/SchemeRedemption.cs | 36 +++ 8 files changed, 694 insertions(+) create mode 100644 Distribution/Migrations/20260821120000_AddPromotionsSchemes.cs create mode 100644 Distribution/Migrations/20260821130000_AddSchemeFreeGoodsRuleAndBundleComponents.cs create mode 100644 Distribution/Models/Scheme.cs create mode 100644 Distribution/Models/SchemeBundleComponent.cs create mode 100644 Distribution/Models/SchemeRedemption.cs diff --git a/Distribution/Data/AppDBContext.cs b/Distribution/Data/AppDBContext.cs index deda8b6..4398ce2 100644 --- a/Distribution/Data/AppDBContext.cs +++ b/Distribution/Data/AppDBContext.cs @@ -36,6 +36,9 @@ public class AppDBContext(DbContextOptions options) : DbContext(op public DbSet DriverProfiles { get; set; } public DbSet DriverComplianceConfigs { get; set; } public DbSet RideCancelRequests { get; set; } + public DbSet Schemes { get; set; } + public DbSet SchemeRedemptions { get; set; } + public DbSet SchemeBundleComponents { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -90,6 +93,52 @@ public class AppDBContext(DbContextOptions options) : DbContext(op .HasForeignKey(r => r.FK_DriverProfileId) .OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity() + .HasOne(r => r.Scheme) + .WithMany(s => s.Redemptions) + .HasForeignKey(r => r.FK_SchemeId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(r => r.Order) + .WithMany() + .HasForeignKey(r => r.FK_OrderId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(r => r.FreeItem) + .WithMany() + .HasForeignKey(r => r.FK_FreeItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(s => s.TriggerItem) + .WithMany() + .HasForeignKey(s => s.FK_TriggerItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(s => s.FreeItem) + .WithMany() + .HasForeignKey(s => s.FK_FreeItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(c => c.Scheme) + .WithMany(s => s.BundleComponents) + .HasForeignKey(c => c.FK_SchemeId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(c => c.Item) + .WithMany() + .HasForeignKey(c => c.FK_ItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(c => new { c.FK_SchemeId, c.FK_ItemId }) + .IsUnique(); + Seed.RoleAndPermissionSeed.Apply(modelBuilder); } } diff --git a/Distribution/Migrations/20260821120000_AddPromotionsSchemes.cs b/Distribution/Migrations/20260821120000_AddPromotionsSchemes.cs new file mode 100644 index 0000000..72ed49c --- /dev/null +++ b/Distribution/Migrations/20260821120000_AddPromotionsSchemes.cs @@ -0,0 +1,129 @@ +using System; +using Distribution.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + [DbContext(typeof(AppDBContext))] + [Migration("20260821120000_AddPromotionsSchemes")] + /// + public partial class AddPromotionsSchemes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CostPrice", + table: "Items", + type: "decimal(65,30)", + nullable: true); + + migrationBuilder.CreateTable( + name: "Schemes", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + RuleType = table.Column(type: "int", nullable: false), + RuleConfig = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + FK_RegionId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + EligibleSegment = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + DateRangeFrom = table.Column(type: "datetime(6)", nullable: false), + DateRangeTo = table.Column(type: "datetime(6)", nullable: false), + AgentLimitExempt = table.Column(type: "tinyint(1)", nullable: false), + BaselineExpectedSales = table.Column(type: "decimal(65,30)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Schemes", x => x.Id); + table.ForeignKey( + name: "FK_Schemes_Regions_FK_RegionId", + column: x => x.FK_RegionId, + principalTable: "Regions", + principalColumn: "Id"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "SchemeRedemptions", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_SchemeId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_OrderId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + FieldInvoiceId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + DiscountAmount = table.Column(type: "decimal(65,30)", nullable: true), + FK_FreeItemId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + FreeQty = table.Column(type: "decimal(65,30)", nullable: true), + FreeItemCostValue = table.Column(type: "decimal(65,30)", nullable: true), + RedeemedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SchemeRedemptions", x => x.Id); + table.ForeignKey( + name: "FK_SchemeRedemptions_Schemes_FK_SchemeId", + column: x => x.FK_SchemeId, + principalTable: "Schemes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SchemeRedemptions_Orders_FK_OrderId", + column: x => x.FK_OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SchemeRedemptions_Items_FK_FreeItemId", + column: x => x.FK_FreeItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Schemes_FK_RegionId", + table: "Schemes", + column: "FK_RegionId"); + + migrationBuilder.CreateIndex( + name: "IX_SchemeRedemptions_FK_SchemeId", + table: "SchemeRedemptions", + column: "FK_SchemeId"); + + migrationBuilder.CreateIndex( + name: "IX_SchemeRedemptions_FK_OrderId", + table: "SchemeRedemptions", + column: "FK_OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_SchemeRedemptions_FK_FreeItemId", + table: "SchemeRedemptions", + column: "FK_FreeItemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SchemeRedemptions"); + + migrationBuilder.DropTable( + name: "Schemes"); + + migrationBuilder.DropColumn( + name: "CostPrice", + table: "Items"); + } + } +} diff --git a/Distribution/Migrations/20260821130000_AddSchemeFreeGoodsRuleAndBundleComponents.cs b/Distribution/Migrations/20260821130000_AddSchemeFreeGoodsRuleAndBundleComponents.cs new file mode 100644 index 0000000..4c3457e --- /dev/null +++ b/Distribution/Migrations/20260821130000_AddSchemeFreeGoodsRuleAndBundleComponents.cs @@ -0,0 +1,170 @@ +using System; +using Distribution.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + [DbContext(typeof(AppDBContext))] + [Migration("20260821130000_AddSchemeFreeGoodsRuleAndBundleComponents")] + /// + public partial class AddSchemeFreeGoodsRuleAndBundleComponents : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "RuleConfig", + table: "Schemes", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "FK_TriggerItemId", + table: "Schemes", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "TriggerQty", + table: "Schemes", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "FK_FreeItemId", + table: "Schemes", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "FreeQty", + table: "Schemes", + type: "decimal(65,30)", + nullable: true); + + migrationBuilder.CreateTable( + name: "SchemeBundleComponents", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_SchemeId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_ItemId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Qty = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SchemeBundleComponents", x => x.Id); + table.ForeignKey( + name: "FK_SchemeBundleComponents_Schemes_FK_SchemeId", + column: x => x.FK_SchemeId, + principalTable: "Schemes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SchemeBundleComponents_Items_FK_ItemId", + column: x => x.FK_ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Schemes_FK_TriggerItemId", + table: "Schemes", + column: "FK_TriggerItemId"); + + migrationBuilder.CreateIndex( + name: "IX_Schemes_FK_FreeItemId", + table: "Schemes", + column: "FK_FreeItemId"); + + migrationBuilder.CreateIndex( + name: "IX_SchemeBundleComponents_FK_SchemeId_FK_ItemId", + table: "SchemeBundleComponents", + columns: new[] { "FK_SchemeId", "FK_ItemId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SchemeBundleComponents_FK_ItemId", + table: "SchemeBundleComponents", + column: "FK_ItemId"); + + migrationBuilder.AddForeignKey( + name: "FK_Schemes_Items_FK_TriggerItemId", + table: "Schemes", + column: "FK_TriggerItemId", + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Schemes_Items_FK_FreeItemId", + table: "Schemes", + column: "FK_FreeItemId", + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SchemeBundleComponents"); + + migrationBuilder.DropForeignKey( + name: "FK_Schemes_Items_FK_TriggerItemId", + table: "Schemes"); + + migrationBuilder.DropForeignKey( + name: "FK_Schemes_Items_FK_FreeItemId", + table: "Schemes"); + + migrationBuilder.DropIndex( + name: "IX_Schemes_FK_TriggerItemId", + table: "Schemes"); + + migrationBuilder.DropIndex( + name: "IX_Schemes_FK_FreeItemId", + table: "Schemes"); + + migrationBuilder.DropColumn( + name: "FK_TriggerItemId", + table: "Schemes"); + + migrationBuilder.DropColumn( + name: "TriggerQty", + table: "Schemes"); + + migrationBuilder.DropColumn( + name: "FK_FreeItemId", + table: "Schemes"); + + migrationBuilder.DropColumn( + name: "FreeQty", + table: "Schemes"); + + migrationBuilder.AlterColumn( + name: "RuleConfig", + table: "Schemes", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/Distribution/Migrations/AppDBContextModelSnapshot.cs b/Distribution/Migrations/AppDBContextModelSnapshot.cs index fe07a1d..1ba6d4d 100644 --- a/Distribution/Migrations/AppDBContextModelSnapshot.cs +++ b/Distribution/Migrations/AppDBContextModelSnapshot.cs @@ -196,6 +196,9 @@ namespace Distribution.Migrations b.Property("CreatedAt") .HasColumnType("datetime(6)"); + b.Property("CostPrice") + .HasColumnType("decimal(65,30)"); + b.Property("Description") .HasColumnType("longtext"); @@ -819,6 +822,135 @@ namespace Distribution.Migrations b.ToTable("Regions"); }); + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AgentLimitExempt") + .HasColumnType("tinyint(1)"); + + b.Property("BaselineExpectedSales") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateRangeFrom") + .HasColumnType("datetime(6)"); + + b.Property("DateRangeTo") + .HasColumnType("datetime(6)"); + + b.Property("EligibleSegment") + .HasColumnType("longtext"); + + b.Property("FK_FreeItemId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("FK_TriggerItemId") + .HasColumnType("char(36)"); + + b.Property("FreeQty") + .HasColumnType("decimal(65,30)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RuleConfig") + .HasColumnType("longtext"); + + b.Property("RuleType") + .HasColumnType("int"); + + b.Property("TriggerQty") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_FreeItemId"); + + b.HasIndex("FK_RegionId"); + + b.HasIndex("FK_TriggerItemId"); + + b.ToTable("Schemes"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeBundleComponent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("Qty") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SchemeId", "FK_ItemId") + .IsUnique(); + + b.ToTable("SchemeBundleComponents"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DiscountAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("FK_FreeItemId") + .HasColumnType("char(36)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("FieldInvoiceId") + .HasColumnType("char(36)"); + + b.Property("FreeItemCostValue") + .HasColumnType("decimal(65,30)"); + + b.Property("FreeQty") + .HasColumnType("decimal(65,30)"); + + b.Property("RedeemedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_FreeItemId"); + + b.HasIndex("FK_OrderId"); + + b.HasIndex("FK_SchemeId"); + + b.ToTable("SchemeRedemptions"); + }); + modelBuilder.Entity("Distribution.Models.RideCancelRequest", b => { b.Property("Id") @@ -2939,6 +3071,25 @@ namespace Distribution.Migrations b.Navigation("Role"); }); + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.PriceList", "PriceList") + .WithMany("Items") + .HasForeignKey("FK_PriceListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PriceList"); + }); + modelBuilder.Entity("Distribution.Models.Route", b => { b.HasOne("Distribution.Models.Region", "Region") @@ -2950,6 +3101,73 @@ namespace Distribution.Migrations b.Navigation("Region"); }); + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.HasOne("Distribution.Models.Item", "FreeItem") + .WithMany() + .HasForeignKey("FK_FreeItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId"); + + b.HasOne("Distribution.Models.Item", "TriggerItem") + .WithMany() + .HasForeignKey("FK_TriggerItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("FreeItem"); + + b.Navigation("Region"); + + b.Navigation("TriggerItem"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeBundleComponent", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Scheme", "Scheme") + .WithMany("BundleComponents") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Scheme"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeRedemption", b => + { + b.HasOne("Distribution.Models.Item", "FreeItem") + .WithMany() + .HasForeignKey("FK_FreeItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Scheme", "Scheme") + .WithMany("Redemptions") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("FreeItem"); + + b.Navigation("Order"); + + b.Navigation("Scheme"); + }); + modelBuilder.Entity("Distribution.Models.StockBalance", b => { b.HasOne("Distribution.Models.Item", "Item") @@ -3135,6 +3353,13 @@ namespace Distribution.Migrations { b.Navigation("Items"); }); + + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.Navigation("BundleComponents"); + + b.Navigation("Redemptions"); + }); #pragma warning restore 612, 618 } } diff --git a/Distribution/Models/Item.cs b/Distribution/Models/Item.cs index 289d948..5aeecd9 100644 --- a/Distribution/Models/Item.cs +++ b/Distribution/Models/Item.cs @@ -26,6 +26,9 @@ namespace Distribution.Models public decimal? SalePrice { get; set; } + /// Cost/purchase price, used to value free-goods scheme redemptions for getSchemeROI. + public decimal? CostPrice { get; set; } + public SyncSource SyncSource { get; set; } = SyncSource.Manual; public int? ExternalErpItemId { get; set; } public DateTime? LastSyncedAt { get; set; } diff --git a/Distribution/Models/Scheme.cs b/Distribution/Models/Scheme.cs new file mode 100644 index 0000000..c00b30a --- /dev/null +++ b/Distribution/Models/Scheme.cs @@ -0,0 +1,62 @@ +namespace Distribution.Models +{ + public enum SchemeRuleType + { + BuyXGetY, + SlabDiscount, + FlatPercent, + FreeBundle + } + + public class Scheme : Template + { + public string Name { get; set; } + public SchemeRuleType RuleType { get; set; } + + /// + /// JSON payload for percentage-based rules only (SlabDiscount tiers, FlatPercent). Free-goods + /// rules (BuyXGetY / FreeBundle) are structured fields/child rows below, not JSON, so they can + /// be managed through their own CRUD endpoints instead of a hand-edited blob. + /// + public string? RuleConfig { get; set; } + + // ── BuyXGetY: trigger item/qty; shares FK_FreeItemId/FreeQty below as the payout. ────────── + + public Guid? FK_TriggerItemId { get; set; } + public Item? TriggerItem { get; set; } + public int? TriggerQty { get; set; } + + // ── Free-goods payout, used by both BuyXGetY and FreeBundle. ───────────────────────────── + + public Guid? FK_FreeItemId { get; set; } + public Item? FreeItem { get; set; } + public decimal? FreeQty { get; set; } + + /// Required component items for a FreeBundle scheme — managed via its own CRUD. + public ICollection BundleComponents { get; set; } = []; + + public Guid? FK_RegionId { get; set; } + public Region? Region { get; set; } + + /// Store segment this scheme applies to (Gold/Silver/Key Account/...); null = all segments. + public string? EligibleSegment { get; set; } + + public DateTime DateRangeFrom { get; set; } + public DateTime DateRangeTo { get; set; } + + /// + /// When true, this scheme's discount is exempt from the per-role discount ceiling in + /// 08_sales_agent_controls.md and stacks on top of it unconditionally. + /// + public bool AgentLimitExempt { get; set; } + + /// + /// Manual baseline (expected sales with no scheme running) used by getSchemeROI. Per the + /// open question in docs/17_promotions_schemes.md, this is entered at scheme creation + /// rather than computed from a trailing-period average. + /// + public decimal? BaselineExpectedSales { get; set; } + + public ICollection Redemptions { get; set; } = []; + } +} diff --git a/Distribution/Models/SchemeBundleComponent.cs b/Distribution/Models/SchemeBundleComponent.cs new file mode 100644 index 0000000..0a446b8 --- /dev/null +++ b/Distribution/Models/SchemeBundleComponent.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + /// A required item+qty line of a FreeBundle scheme (e.g. "buy A + B, get C free"). + public class SchemeBundleComponent + { + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid FK_SchemeId { get; set; } + [ForeignKey("FK_SchemeId")] + public Scheme Scheme { get; set; } + + public Guid FK_ItemId { get; set; } + [ForeignKey("FK_ItemId")] + public Item Item { get; set; } + + public int Qty { get; set; } + } +} diff --git a/Distribution/Models/SchemeRedemption.cs b/Distribution/Models/SchemeRedemption.cs new file mode 100644 index 0000000..78dbf7b --- /dev/null +++ b/Distribution/Models/SchemeRedemption.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class SchemeRedemption + { + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid FK_SchemeId { get; set; } + [ForeignKey("FK_SchemeId")] + public Scheme Scheme { get; set; } + + public Guid? FK_OrderId { get; set; } + [ForeignKey("FK_OrderId")] + public Order? Order { get; set; } + + /// Placeholder for the future field-invoice module — no FieldInvoice entity exists yet. + public Guid? FieldInvoiceId { get; set; } + + public decimal? DiscountAmount { get; set; } + + public Guid? FK_FreeItemId { get; set; } + [ForeignKey("FK_FreeItemId")] + public Item? FreeItem { get; set; } + + public decimal? FreeQty { get; set; } + + /// + /// Cost price of the free item snapshotted at redemption time, so getSchemeROI stays + /// accurate even after the item's cost price later changes. + /// + public decimal? FreeItemCostValue { get; set; } + + public DateTime RedeemedAt { get; set; } = DateTime.Now; + } +}