Sfoglia il codice sorgente

CW-STORY-05.2: model purchases and purchase items

Purchase (aggregate root) and PurchaseItem per AGENTS.md §5.6.
AddItem composition + tightened PurchaseItem ctor deferred to
CW-STORY-05.4, mirroring the CW-STORY-03.1/03.3 split.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
master
Daniel Covington 1 settimana fa
parent
commit
ecc883d4bf
12 ha cambiato i file con 1589 aggiunte e 7 eliminazioni
  1. +2
    -1
      .claude/settings.json
  2. +8
    -6
      docs/scrum-backlog.md
  3. +61
    -0
      src/CartWise.Domain/Entities/Purchase.cs
  4. +75
    -0
      src/CartWise.Domain/Entities/PurchaseItem.cs
  5. +9
    -0
      src/CartWise.Domain/Enums/PurchaseSourceType.cs
  6. +4
    -0
      src/CartWise.Infrastructure/Data/CartWiseDbContext.cs
  7. +34
    -0
      src/CartWise.Infrastructure/Data/Configurations/PurchaseConfiguration.cs
  8. +41
    -0
      src/CartWise.Infrastructure/Data/Configurations/PurchaseItemConfiguration.cs
  9. +996
    -0
      src/CartWise.Infrastructure/Data/Migrations/20260810191555_AddPurchases.Designer.cs
  10. +143
    -0
      src/CartWise.Infrastructure/Data/Migrations/20260810191555_AddPurchases.cs
  11. +146
    -0
      src/CartWise.Infrastructure/Data/Migrations/CartWiseDbContextModelSnapshot.cs
  12. +70
    -0
      tests/CartWise.Domain.Tests/PurchaseTests.cs

+ 2
- 1
.claude/settings.json Vedi File

@@ -37,7 +37,8 @@
"Bash(curl -s -b /tmp/cw5.txt -m 15 http://127.0.0.1:5187/api/products/barcode/3017620422003)",
"Bash(curl -s -b /tmp/cw5.txt http://127.0.0.1:5187/products/0ce5df7a-6c8d-489d-ba9f-8d41699f0705 -o /tmp/product.html -w \"status=%{http_code}\\\\n\")",
"Bash(sed -n '/<h1>/,/Add to my list/p' /tmp/product.html)",
"Bash(cd \"g:/development/C Sharp AI/CartWise\" && rm -f verify4.db* && dotnet ef database update --project src/CartWise.Infrastructure/CartWise.Infrastructure.csproj --startup-project src/CartWise.Web/CartWise.Web.csproj --connection \"Data Source=verify4.db\" 2>&1 | tail -10 && rm -f verify4.db*)"
"Bash(cd \"g:/development/C Sharp AI/CartWise\" && rm -f verify4.db* && dotnet ef database update --project src/CartWise.Infrastructure/CartWise.Infrastructure.csproj --startup-project src/CartWise.Web/CartWise.Web.csproj --connection \"Data Source=verify4.db\" 2>&1 | tail -10 && rm -f verify4.db*)",
"Bash(cd \"g:/development/C Sharp AI/CartWise\" && rm -f verify5.db* && dotnet ef database update --project src/CartWise.Infrastructure/CartWise.Infrastructure.csproj --startup-project src/CartWise.Web/CartWise.Web.csproj --connection \"Data Source=verify5.db\" 2>&1 | tail -10 && rm -f verify5.db*)"
],
"additionalDirectories": [
"G:\\development\\C Sharp AI\\CartWise"


+ 8
- 6
docs/scrum-backlog.md Vedi File

@@ -573,12 +573,14 @@ Verified live end-to-end with a **real** barcode against the **real** Open Food
- EF configurations and migration are created

**Tasks**
- [ ] Create `Purchase`
- [ ] Create `PurchaseItem`
- [ ] Add source type enum
- [ ] Add EF configurations
- [ ] Create migration
- [ ] Add tests
- [x] Create `Purchase`
- [x] Create `PurchaseItem`
- [x] Add source type enum
- [x] Add EF configurations
- [x] Create migration
- [x] Add tests

**Status:** Done — `Purchase` is the aggregate root for `PurchaseItem`, mirroring `ShoppingList`/`ShoppingListItem`'s split across stories: `Items` navigation exists now, but no `AddItem` composition method yet and `PurchaseItem`'s constructor stays `public` — both arrive in `CW-STORY-05.4` when the service layer actually needs them, exactly matching the `CW-STORY-03.1`→`03.3` precedent. `PurchaseSourceType` (`Manual`/`ShoppingMode`/`ReceiptImport`/`RetailerImport`) added. `PurchaseItem` enforces `Quantity > 0` and `LinePrice >= 0` as constructor invariants. `AddPurchases` migration verified applying cleanly. 11 domain tests.

### `CW-STORY-05.3` Model append-only price observations
**Release:** MVP


+ 61
- 0
src/CartWise.Domain/Entities/Purchase.cs Vedi File

@@ -0,0 +1,61 @@
using CartWise.Domain.Enums;

namespace CartWise.Domain.Entities;

public class Purchase
{
private readonly List<PurchaseItem> _items = [];

public Guid PurchaseId { get; private set; }

public Guid HouseholdId { get; private set; }

public Guid? StoreLocationId { get; private set; }

public string PurchasedByUserId { get; private set; } = null!;

public DateTime PurchasedUtc { get; private set; }

public decimal? Subtotal { get; private set; }

public decimal? Tax { get; private set; }

public decimal? Total { get; private set; }

public PurchaseSourceType SourceType { get; private set; }

public DateTime CreatedUtc { get; private set; }

public IReadOnlyCollection<PurchaseItem> Items => _items.AsReadOnly();

private Purchase()
{
}

public Purchase(
Guid householdId,
string purchasedByUserId,
DateTime purchasedUtc,
DateTime createdUtc,
PurchaseSourceType sourceType = PurchaseSourceType.Manual,
Guid? storeLocationId = null)
{
if (householdId == Guid.Empty)
{
throw new ArgumentException("HouseholdId is required.", nameof(householdId));
}

if (string.IsNullOrWhiteSpace(purchasedByUserId))
{
throw new ArgumentException("PurchasedByUserId is required.", nameof(purchasedByUserId));
}

PurchaseId = Guid.NewGuid();
HouseholdId = householdId;
StoreLocationId = storeLocationId;
PurchasedByUserId = purchasedByUserId;
PurchasedUtc = purchasedUtc;
SourceType = sourceType;
CreatedUtc = createdUtc;
}
}

+ 75
- 0
src/CartWise.Domain/Entities/PurchaseItem.cs Vedi File

@@ -0,0 +1,75 @@
namespace CartWise.Domain.Entities;

public class PurchaseItem
{
public Guid PurchaseItemId { get; private set; }

public Guid PurchaseId { get; private set; }

public Guid? ProductId { get; private set; }

public Guid? GroceryConceptId { get; private set; }

public Guid? ShoppingListItemId { get; private set; }

public string Description { get; private set; } = null!;

public decimal Quantity { get; private set; }

public string? Unit { get; private set; }

public decimal LinePrice { get; private set; }

public decimal? UnitPrice { get; private set; }

public DateTime CreatedUtc { get; private set; }

private PurchaseItem()
{
}

public PurchaseItem(
Guid purchaseId,
string description,
decimal linePrice,
DateTime createdUtc,
decimal quantity = 1m,
string? unit = null,
Guid? productId = null,
Guid? groceryConceptId = null,
Guid? shoppingListItemId = null,
decimal? unitPrice = null)
{
if (purchaseId == Guid.Empty)
{
throw new ArgumentException("PurchaseId is required.", nameof(purchaseId));
}

if (string.IsNullOrWhiteSpace(description))
{
throw new ArgumentException("Description is required.", nameof(description));
}

if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity must be greater than zero.");
}

if (linePrice < 0)
{
throw new ArgumentOutOfRangeException(nameof(linePrice), "LinePrice cannot be negative.");
}

PurchaseItemId = Guid.NewGuid();
PurchaseId = purchaseId;
Description = description.Trim();
Quantity = quantity;
Unit = unit;
LinePrice = linePrice;
UnitPrice = unitPrice;
ProductId = productId;
GroceryConceptId = groceryConceptId;
ShoppingListItemId = shoppingListItemId;
CreatedUtc = createdUtc;
}
}

+ 9
- 0
src/CartWise.Domain/Enums/PurchaseSourceType.cs Vedi File

@@ -0,0 +1,9 @@
namespace CartWise.Domain.Enums;

public enum PurchaseSourceType
{
Manual = 0,
ShoppingMode = 1,
ReceiptImport = 2,
RetailerImport = 3
}

+ 4
- 0
src/CartWise.Infrastructure/Data/CartWiseDbContext.cs Vedi File

@@ -37,6 +37,10 @@ public class CartWiseDbContext : IdentityDbContext<ApplicationUser>, IApplicatio

public DbSet<StoreLocation> StoreLocations => Set<StoreLocation>();

public DbSet<Purchase> Purchases => Set<Purchase>();

public DbSet<PurchaseItem> PurchaseItems => Set<PurchaseItem>();

protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);


+ 34
- 0
src/CartWise.Infrastructure/Data/Configurations/PurchaseConfiguration.cs Vedi File

@@ -0,0 +1,34 @@
using CartWise.Domain.Entities;
using CartWise.Infrastructure.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace CartWise.Infrastructure.Data.Configurations;

public class PurchaseConfiguration : IEntityTypeConfiguration<Purchase>
{
public void Configure(EntityTypeBuilder<Purchase> builder)
{
builder.HasKey(p => p.PurchaseId);

builder.HasOne<Household>()
.WithMany()
.HasForeignKey(p => p.HouseholdId)
.OnDelete(DeleteBehavior.Cascade);

builder.HasOne<StoreLocation>()
.WithMany()
.HasForeignKey(p => p.StoreLocationId)
.OnDelete(DeleteBehavior.SetNull);

builder.HasOne<ApplicationUser>()
.WithMany()
.HasForeignKey(p => p.PurchasedByUserId)
.OnDelete(DeleteBehavior.Restrict);

builder.HasIndex(p => new { p.HouseholdId, p.PurchasedUtc });

builder.Metadata.FindNavigation(nameof(Purchase.Items))!
.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}

+ 41
- 0
src/CartWise.Infrastructure/Data/Configurations/PurchaseItemConfiguration.cs Vedi File

@@ -0,0 +1,41 @@
using CartWise.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace CartWise.Infrastructure.Data.Configurations;

public class PurchaseItemConfiguration : IEntityTypeConfiguration<PurchaseItem>
{
public void Configure(EntityTypeBuilder<PurchaseItem> builder)
{
builder.HasKey(i => i.PurchaseItemId);

builder.Property(i => i.Description)
.HasMaxLength(240)
.IsRequired();

builder.Property(i => i.Unit).HasMaxLength(32);

builder.HasOne<Purchase>()
.WithMany(p => p.Items)
.HasForeignKey(i => i.PurchaseId)
.OnDelete(DeleteBehavior.Cascade);

builder.HasOne<Product>()
.WithMany()
.HasForeignKey(i => i.ProductId)
.OnDelete(DeleteBehavior.SetNull);

builder.HasOne<GroceryConcept>()
.WithMany()
.HasForeignKey(i => i.GroceryConceptId)
.OnDelete(DeleteBehavior.SetNull);

builder.HasOne<ShoppingListItem>()
.WithMany()
.HasForeignKey(i => i.ShoppingListItemId)
.OnDelete(DeleteBehavior.SetNull);

builder.HasIndex(i => i.ProductId);
}
}

+ 996
- 0
src/CartWise.Infrastructure/Data/Migrations/20260810191555_AddPurchases.Designer.cs Vedi File

@@ -0,0 +1,996 @@
// <auto-generated />
using System;
using CartWise.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

#nullable disable

namespace CartWise.Infrastructure.Data.Migrations
{
[DbContext(typeof(CartWiseDbContext))]
[Migration("20260810191555_AddPurchases")]
partial class AddPurchases
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.10");

modelBuilder.Entity("CartWise.Domain.Entities.Brand", b =>
{
b.Property<Guid>("BrandId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("TEXT");

b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("TEXT");

b.HasKey("BrandId");

b.HasIndex("NormalizedName")
.IsUnique();

b.ToTable("Brands");
});

modelBuilder.Entity("CartWise.Domain.Entities.GroceryConcept", b =>
{
b.Property<Guid>("GroceryConceptId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<Guid?>("CategoryId")
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("TEXT");

b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("TEXT");

b.HasKey("GroceryConceptId");

b.HasIndex("CategoryId");

b.HasIndex("NormalizedName")
.IsUnique();

b.ToTable("GroceryConcepts");
});

modelBuilder.Entity("CartWise.Domain.Entities.Household", b =>
{
b.Property<Guid>("HouseholdId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<string>("CreatedByUserId")
.IsRequired()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("TEXT");

b.HasKey("HouseholdId");

b.HasIndex("CreatedByUserId");

b.ToTable("Households");
});

modelBuilder.Entity("CartWise.Domain.Entities.HouseholdMember", b =>
{
b.Property<Guid>("HouseholdId")
.HasColumnType("TEXT");

b.Property<string>("UserId")
.HasColumnType("TEXT");

b.Property<DateTime>("JoinedUtc")
.HasColumnType("TEXT");

b.Property<int>("Role")
.HasColumnType("INTEGER");

b.HasKey("HouseholdId", "UserId");

b.HasIndex("UserId");

b.ToTable("HouseholdMembers");
});

modelBuilder.Entity("CartWise.Domain.Entities.HouseholdProductPreference", b =>
{
b.Property<Guid>("HouseholdProductPreferenceId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<Guid>("GroceryConceptId")
.HasColumnType("TEXT");

b.Property<Guid>("HouseholdId")
.HasColumnType("TEXT");

b.Property<Guid?>("PreferredProductId")
.HasColumnType("TEXT");

b.Property<decimal?>("PreferredQuantity")
.HasColumnType("TEXT");

b.Property<string>("PreferredUnit")
.HasMaxLength(32)
.HasColumnType("TEXT");

b.Property<int>("SubstitutionLevel")
.HasColumnType("INTEGER");

b.Property<DateTime>("UpdatedUtc")
.HasColumnType("TEXT");

b.HasKey("HouseholdProductPreferenceId");

b.HasIndex("GroceryConceptId");

b.HasIndex("PreferredProductId");

b.HasIndex("HouseholdId", "GroceryConceptId")
.IsUnique();

b.ToTable("HouseholdProductPreferences");
});

modelBuilder.Entity("CartWise.Domain.Entities.Product", b =>
{
b.Property<Guid>("ProductId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<Guid?>("BrandId")
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<Guid?>("GroceryConceptId")
.HasColumnType("TEXT");

b.Property<string>("ImageUrl")
.HasColumnType("TEXT");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("TEXT");

b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("TEXT");

b.Property<string>("SizeUnit")
.HasMaxLength(32)
.HasColumnType("TEXT");

b.Property<decimal?>("SizeValue")
.HasColumnType("TEXT");

b.Property<string>("SourceExternalId")
.HasMaxLength(200)
.HasColumnType("TEXT");

b.Property<int>("SourceType")
.HasColumnType("INTEGER");

b.Property<DateTime>("UpdatedUtc")
.HasColumnType("TEXT");

b.HasKey("ProductId");

b.HasIndex("BrandId");

b.HasIndex("GroceryConceptId");

b.HasIndex("NormalizedName");

b.ToTable("Products");
});

modelBuilder.Entity("CartWise.Domain.Entities.ProductCategory", b =>
{
b.Property<Guid>("ProductCategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("TEXT");

b.Property<Guid?>("ParentCategoryId")
.HasColumnType("TEXT");

b.HasKey("ProductCategoryId");

b.HasIndex("ParentCategoryId");

b.ToTable("ProductCategories");
});

modelBuilder.Entity("CartWise.Domain.Entities.ProductIdentifier", b =>
{
b.Property<Guid>("ProductIdentifierId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<int>("IdentifierType")
.HasColumnType("INTEGER");

b.Property<string>("NormalizedValue")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");

b.Property<Guid>("ProductId")
.HasColumnType("TEXT");

b.Property<string>("Value")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");

b.HasKey("ProductIdentifierId");

b.HasIndex("ProductId");

b.HasIndex("IdentifierType", "NormalizedValue")
.IsUnique();

b.ToTable("ProductIdentifiers");
});

modelBuilder.Entity("CartWise.Domain.Entities.Purchase", b =>
{
b.Property<Guid>("PurchaseId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<Guid>("HouseholdId")
.HasColumnType("TEXT");

b.Property<string>("PurchasedByUserId")
.IsRequired()
.HasColumnType("TEXT");

b.Property<DateTime>("PurchasedUtc")
.HasColumnType("TEXT");

b.Property<int>("SourceType")
.HasColumnType("INTEGER");

b.Property<Guid?>("StoreLocationId")
.HasColumnType("TEXT");

b.Property<decimal?>("Subtotal")
.HasColumnType("TEXT");

b.Property<decimal?>("Tax")
.HasColumnType("TEXT");

b.Property<decimal?>("Total")
.HasColumnType("TEXT");

b.HasKey("PurchaseId");

b.HasIndex("PurchasedByUserId");

b.HasIndex("StoreLocationId");

b.HasIndex("HouseholdId", "PurchasedUtc");

b.ToTable("Purchases");
});

modelBuilder.Entity("CartWise.Domain.Entities.PurchaseItem", b =>
{
b.Property<Guid>("PurchaseItemId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<string>("Description")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("TEXT");

b.Property<Guid?>("GroceryConceptId")
.HasColumnType("TEXT");

b.Property<decimal>("LinePrice")
.HasColumnType("TEXT");

b.Property<Guid?>("ProductId")
.HasColumnType("TEXT");

b.Property<Guid>("PurchaseId")
.HasColumnType("TEXT");

b.Property<decimal>("Quantity")
.HasColumnType("TEXT");

b.Property<Guid?>("ShoppingListItemId")
.HasColumnType("TEXT");

b.Property<string>("Unit")
.HasMaxLength(32)
.HasColumnType("TEXT");

b.Property<decimal?>("UnitPrice")
.HasColumnType("TEXT");

b.HasKey("PurchaseItemId");

b.HasIndex("GroceryConceptId");

b.HasIndex("ProductId");

b.HasIndex("PurchaseId");

b.HasIndex("ShoppingListItemId");

b.ToTable("PurchaseItems");
});

modelBuilder.Entity("CartWise.Domain.Entities.Retailer", b =>
{
b.Property<Guid>("RetailerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("TEXT");

b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("TEXT");

b.Property<string>("WebsiteUrl")
.HasColumnType("TEXT");

b.HasKey("RetailerId");

b.HasIndex("NormalizedName")
.IsUnique();

b.ToTable("Retailers");
});

modelBuilder.Entity("CartWise.Domain.Entities.ShoppingList", b =>
{
b.Property<Guid>("ShoppingListId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<DateTime?>("CompletedUtc")
.HasColumnType("TEXT");

b.Property<string>("CreatedByUserId")
.IsRequired()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<Guid>("HouseholdId")
.HasColumnType("TEXT");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("TEXT");

b.Property<int>("Status")
.HasColumnType("INTEGER");

b.HasKey("ShoppingListId");

b.HasIndex("CreatedByUserId");

b.HasIndex("HouseholdId", "Status");

b.ToTable("ShoppingLists");
});

modelBuilder.Entity("CartWise.Domain.Entities.ShoppingListItem", b =>
{
b.Property<Guid>("ShoppingListItemId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<string>("AddedByUserId")
.IsRequired()
.HasColumnType("TEXT");

b.Property<DateTime>("AddedUtc")
.HasColumnType("TEXT");

b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("TEXT");

b.Property<Guid?>("GroceryConceptId")
.HasColumnType("TEXT");

b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("TEXT");

b.Property<DateTime?>("PurchasedUtc")
.HasColumnType("TEXT");

b.Property<decimal?>("Quantity")
.HasColumnType("TEXT");

b.Property<Guid>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("TEXT");

b.Property<Guid>("ShoppingListId")
.HasColumnType("TEXT");

b.Property<int>("SortOrder")
.HasColumnType("INTEGER");

b.Property<int>("Status")
.HasColumnType("INTEGER");

b.Property<string>("Unit")
.HasMaxLength(32)
.HasColumnType("TEXT");

b.HasKey("ShoppingListItemId");

b.HasIndex("AddedByUserId");

b.HasIndex("GroceryConceptId");

b.HasIndex("ShoppingListId", "Status");

b.ToTable("ShoppingListItems");
});

modelBuilder.Entity("CartWise.Domain.Entities.StoreLocation", b =>
{
b.Property<Guid>("StoreLocationId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<string>("AddressLine1")
.HasMaxLength(200)
.HasColumnType("TEXT");

b.Property<string>("AddressLine2")
.HasMaxLength(200)
.HasColumnType("TEXT");

b.Property<string>("City")
.HasMaxLength(120)
.HasColumnType("TEXT");

b.Property<string>("CountryCode")
.HasMaxLength(2)
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<string>("ExternalStoreId")
.HasMaxLength(120)
.HasColumnType("TEXT");

b.Property<decimal?>("Latitude")
.HasColumnType("TEXT");

b.Property<decimal?>("Longitude")
.HasColumnType("TEXT");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");

b.Property<string>("PostalCode")
.HasMaxLength(24)
.HasColumnType("TEXT");

b.Property<string>("Region")
.HasMaxLength(80)
.HasColumnType("TEXT");

b.Property<Guid>("RetailerId")
.HasColumnType("TEXT");

b.Property<DateTime>("UpdatedUtc")
.HasColumnType("TEXT");

b.HasKey("StoreLocationId");

b.HasIndex("RetailerId");

b.ToTable("StoreLocations");
});

modelBuilder.Entity("CartWise.Infrastructure.Identity.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");

b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("TEXT");

b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");

b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");

b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");

b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("PasswordHash")
.HasColumnType("TEXT");

b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");

b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");

b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");

b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");

b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");

b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");

b.ToTable("AspNetUsers", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");

b.ToTable("AspNetRoles", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("ClaimType")
.HasColumnType("TEXT");

b.Property<string>("ClaimValue")
.HasColumnType("TEXT");

b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("RoleId");

b.ToTable("AspNetRoleClaims", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("ClaimType")
.HasColumnType("TEXT");

b.Property<string>("ClaimValue")
.HasColumnType("TEXT");

b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("UserId");

b.ToTable("AspNetUserClaims", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");

b.Property<string>("ProviderKey")
.HasColumnType("TEXT");

b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");

b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("LoginProvider", "ProviderKey");

b.HasIndex("UserId");

b.ToTable("AspNetUserLogins", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");

b.Property<string>("RoleId")
.HasColumnType("TEXT");

b.HasKey("UserId", "RoleId");

b.HasIndex("RoleId");

b.ToTable("AspNetUserRoles", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");

b.Property<string>("LoginProvider")
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasColumnType("TEXT");

b.Property<string>("Value")
.HasColumnType("TEXT");

b.HasKey("UserId", "LoginProvider", "Name");

b.ToTable("AspNetUserTokens", (string)null);
});

modelBuilder.Entity("CartWise.Domain.Entities.GroceryConcept", b =>
{
b.HasOne("CartWise.Domain.Entities.ProductCategory", null)
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.SetNull);
});

modelBuilder.Entity("CartWise.Domain.Entities.Household", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("CreatedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});

modelBuilder.Entity("CartWise.Domain.Entities.HouseholdMember", b =>
{
b.HasOne("CartWise.Domain.Entities.Household", null)
.WithMany("Members")
.HasForeignKey("HouseholdId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("CartWise.Domain.Entities.HouseholdProductPreference", b =>
{
b.HasOne("CartWise.Domain.Entities.GroceryConcept", null)
.WithMany()
.HasForeignKey("GroceryConceptId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Domain.Entities.Household", null)
.WithMany()
.HasForeignKey("HouseholdId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Domain.Entities.Product", null)
.WithMany()
.HasForeignKey("PreferredProductId")
.OnDelete(DeleteBehavior.SetNull);
});

modelBuilder.Entity("CartWise.Domain.Entities.Product", b =>
{
b.HasOne("CartWise.Domain.Entities.Brand", null)
.WithMany()
.HasForeignKey("BrandId")
.OnDelete(DeleteBehavior.SetNull);

b.HasOne("CartWise.Domain.Entities.GroceryConcept", null)
.WithMany()
.HasForeignKey("GroceryConceptId")
.OnDelete(DeleteBehavior.SetNull);
});

modelBuilder.Entity("CartWise.Domain.Entities.ProductCategory", b =>
{
b.HasOne("CartWise.Domain.Entities.ProductCategory", null)
.WithMany()
.HasForeignKey("ParentCategoryId")
.OnDelete(DeleteBehavior.Restrict);
});

modelBuilder.Entity("CartWise.Domain.Entities.ProductIdentifier", b =>
{
b.HasOne("CartWise.Domain.Entities.Product", null)
.WithMany("Identifiers")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("CartWise.Domain.Entities.Purchase", b =>
{
b.HasOne("CartWise.Domain.Entities.Household", null)
.WithMany()
.HasForeignKey("HouseholdId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("PurchasedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();

b.HasOne("CartWise.Domain.Entities.StoreLocation", null)
.WithMany()
.HasForeignKey("StoreLocationId")
.OnDelete(DeleteBehavior.SetNull);
});

modelBuilder.Entity("CartWise.Domain.Entities.PurchaseItem", b =>
{
b.HasOne("CartWise.Domain.Entities.GroceryConcept", null)
.WithMany()
.HasForeignKey("GroceryConceptId")
.OnDelete(DeleteBehavior.SetNull);

b.HasOne("CartWise.Domain.Entities.Product", null)
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.SetNull);

b.HasOne("CartWise.Domain.Entities.Purchase", null)
.WithMany("Items")
.HasForeignKey("PurchaseId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Domain.Entities.ShoppingListItem", null)
.WithMany()
.HasForeignKey("ShoppingListItemId")
.OnDelete(DeleteBehavior.SetNull);
});

modelBuilder.Entity("CartWise.Domain.Entities.ShoppingList", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("CreatedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();

b.HasOne("CartWise.Domain.Entities.Household", null)
.WithMany()
.HasForeignKey("HouseholdId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("CartWise.Domain.Entities.ShoppingListItem", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("AddedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();

b.HasOne("CartWise.Domain.Entities.GroceryConcept", null)
.WithMany()
.HasForeignKey("GroceryConceptId")
.OnDelete(DeleteBehavior.SetNull);

b.HasOne("CartWise.Domain.Entities.ShoppingList", null)
.WithMany("Items")
.HasForeignKey("ShoppingListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("CartWise.Domain.Entities.StoreLocation", b =>
{
b.HasOne("CartWise.Domain.Entities.Retailer", null)
.WithMany()
.HasForeignKey("RetailerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("CartWise.Domain.Entities.Household", b =>
{
b.Navigation("Members");
});

modelBuilder.Entity("CartWise.Domain.Entities.Product", b =>
{
b.Navigation("Identifiers");
});

modelBuilder.Entity("CartWise.Domain.Entities.Purchase", b =>
{
b.Navigation("Items");
});

modelBuilder.Entity("CartWise.Domain.Entities.ShoppingList", b =>
{
b.Navigation("Items");
});
#pragma warning restore 612, 618
}
}
}

+ 143
- 0
src/CartWise.Infrastructure/Data/Migrations/20260810191555_AddPurchases.cs Vedi File

@@ -0,0 +1,143 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace CartWise.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddPurchases : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Purchases",
columns: table => new
{
PurchaseId = table.Column<Guid>(type: "TEXT", nullable: false),
HouseholdId = table.Column<Guid>(type: "TEXT", nullable: false),
StoreLocationId = table.Column<Guid>(type: "TEXT", nullable: true),
PurchasedByUserId = table.Column<string>(type: "TEXT", nullable: false),
PurchasedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
Subtotal = table.Column<decimal>(type: "TEXT", nullable: true),
Tax = table.Column<decimal>(type: "TEXT", nullable: true),
Total = table.Column<decimal>(type: "TEXT", nullable: true),
SourceType = table.Column<int>(type: "INTEGER", nullable: false),
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Purchases", x => x.PurchaseId);
table.ForeignKey(
name: "FK_Purchases_AspNetUsers_PurchasedByUserId",
column: x => x.PurchasedByUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Purchases_Households_HouseholdId",
column: x => x.HouseholdId,
principalTable: "Households",
principalColumn: "HouseholdId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Purchases_StoreLocations_StoreLocationId",
column: x => x.StoreLocationId,
principalTable: "StoreLocations",
principalColumn: "StoreLocationId",
onDelete: ReferentialAction.SetNull);
});

migrationBuilder.CreateTable(
name: "PurchaseItems",
columns: table => new
{
PurchaseItemId = table.Column<Guid>(type: "TEXT", nullable: false),
PurchaseId = table.Column<Guid>(type: "TEXT", nullable: false),
ProductId = table.Column<Guid>(type: "TEXT", nullable: true),
GroceryConceptId = table.Column<Guid>(type: "TEXT", nullable: true),
ShoppingListItemId = table.Column<Guid>(type: "TEXT", nullable: true),
Description = table.Column<string>(type: "TEXT", maxLength: 240, nullable: false),
Quantity = table.Column<decimal>(type: "TEXT", nullable: false),
Unit = table.Column<string>(type: "TEXT", maxLength: 32, nullable: true),
LinePrice = table.Column<decimal>(type: "TEXT", nullable: false),
UnitPrice = table.Column<decimal>(type: "TEXT", nullable: true),
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseItems", x => x.PurchaseItemId);
table.ForeignKey(
name: "FK_PurchaseItems_GroceryConcepts_GroceryConceptId",
column: x => x.GroceryConceptId,
principalTable: "GroceryConcepts",
principalColumn: "GroceryConceptId",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_PurchaseItems_Products_ProductId",
column: x => x.ProductId,
principalTable: "Products",
principalColumn: "ProductId",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_PurchaseItems_Purchases_PurchaseId",
column: x => x.PurchaseId,
principalTable: "Purchases",
principalColumn: "PurchaseId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseItems_ShoppingListItems_ShoppingListItemId",
column: x => x.ShoppingListItemId,
principalTable: "ShoppingListItems",
principalColumn: "ShoppingListItemId",
onDelete: ReferentialAction.SetNull);
});

migrationBuilder.CreateIndex(
name: "IX_PurchaseItems_GroceryConceptId",
table: "PurchaseItems",
column: "GroceryConceptId");

migrationBuilder.CreateIndex(
name: "IX_PurchaseItems_ProductId",
table: "PurchaseItems",
column: "ProductId");

migrationBuilder.CreateIndex(
name: "IX_PurchaseItems_PurchaseId",
table: "PurchaseItems",
column: "PurchaseId");

migrationBuilder.CreateIndex(
name: "IX_PurchaseItems_ShoppingListItemId",
table: "PurchaseItems",
column: "ShoppingListItemId");

migrationBuilder.CreateIndex(
name: "IX_Purchases_HouseholdId_PurchasedUtc",
table: "Purchases",
columns: new[] { "HouseholdId", "PurchasedUtc" });

migrationBuilder.CreateIndex(
name: "IX_Purchases_PurchasedByUserId",
table: "Purchases",
column: "PurchasedByUserId");

migrationBuilder.CreateIndex(
name: "IX_Purchases_StoreLocationId",
table: "Purchases",
column: "StoreLocationId");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PurchaseItems");

migrationBuilder.DropTable(
name: "Purchases");
}
}
}

+ 146
- 0
src/CartWise.Infrastructure/Data/Migrations/CartWiseDbContextModelSnapshot.cs Vedi File

@@ -268,6 +268,103 @@ namespace CartWise.Infrastructure.Data.Migrations
b.ToTable("ProductIdentifiers");
});

modelBuilder.Entity("CartWise.Domain.Entities.Purchase", b =>
{
b.Property<Guid>("PurchaseId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<Guid>("HouseholdId")
.HasColumnType("TEXT");

b.Property<string>("PurchasedByUserId")
.IsRequired()
.HasColumnType("TEXT");

b.Property<DateTime>("PurchasedUtc")
.HasColumnType("TEXT");

b.Property<int>("SourceType")
.HasColumnType("INTEGER");

b.Property<Guid?>("StoreLocationId")
.HasColumnType("TEXT");

b.Property<decimal?>("Subtotal")
.HasColumnType("TEXT");

b.Property<decimal?>("Tax")
.HasColumnType("TEXT");

b.Property<decimal?>("Total")
.HasColumnType("TEXT");

b.HasKey("PurchaseId");

b.HasIndex("PurchasedByUserId");

b.HasIndex("StoreLocationId");

b.HasIndex("HouseholdId", "PurchasedUtc");

b.ToTable("Purchases");
});

modelBuilder.Entity("CartWise.Domain.Entities.PurchaseItem", b =>
{
b.Property<Guid>("PurchaseItemId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<string>("Description")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("TEXT");

b.Property<Guid?>("GroceryConceptId")
.HasColumnType("TEXT");

b.Property<decimal>("LinePrice")
.HasColumnType("TEXT");

b.Property<Guid?>("ProductId")
.HasColumnType("TEXT");

b.Property<Guid>("PurchaseId")
.HasColumnType("TEXT");

b.Property<decimal>("Quantity")
.HasColumnType("TEXT");

b.Property<Guid?>("ShoppingListItemId")
.HasColumnType("TEXT");

b.Property<string>("Unit")
.HasMaxLength(32)
.HasColumnType("TEXT");

b.Property<decimal?>("UnitPrice")
.HasColumnType("TEXT");

b.HasKey("PurchaseItemId");

b.HasIndex("GroceryConceptId");

b.HasIndex("ProductId");

b.HasIndex("PurchaseId");

b.HasIndex("ShoppingListItemId");

b.ToTable("PurchaseItems");
});

modelBuilder.Entity("CartWise.Domain.Entities.Retailer", b =>
{
b.Property<Guid>("RetailerId")
@@ -732,6 +829,50 @@ namespace CartWise.Infrastructure.Data.Migrations
.IsRequired();
});

modelBuilder.Entity("CartWise.Domain.Entities.Purchase", b =>
{
b.HasOne("CartWise.Domain.Entities.Household", null)
.WithMany()
.HasForeignKey("HouseholdId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("PurchasedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();

b.HasOne("CartWise.Domain.Entities.StoreLocation", null)
.WithMany()
.HasForeignKey("StoreLocationId")
.OnDelete(DeleteBehavior.SetNull);
});

modelBuilder.Entity("CartWise.Domain.Entities.PurchaseItem", b =>
{
b.HasOne("CartWise.Domain.Entities.GroceryConcept", null)
.WithMany()
.HasForeignKey("GroceryConceptId")
.OnDelete(DeleteBehavior.SetNull);

b.HasOne("CartWise.Domain.Entities.Product", null)
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.SetNull);

b.HasOne("CartWise.Domain.Entities.Purchase", null)
.WithMany("Items")
.HasForeignKey("PurchaseId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Domain.Entities.ShoppingListItem", null)
.WithMany()
.HasForeignKey("ShoppingListItemId")
.OnDelete(DeleteBehavior.SetNull);
});

modelBuilder.Entity("CartWise.Domain.Entities.ShoppingList", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
@@ -837,6 +978,11 @@ namespace CartWise.Infrastructure.Data.Migrations
b.Navigation("Identifiers");
});

modelBuilder.Entity("CartWise.Domain.Entities.Purchase", b =>
{
b.Navigation("Items");
});

modelBuilder.Entity("CartWise.Domain.Entities.ShoppingList", b =>
{
b.Navigation("Items");


+ 70
- 0
tests/CartWise.Domain.Tests/PurchaseTests.cs Vedi File

@@ -0,0 +1,70 @@
using CartWise.Domain.Entities;
using CartWise.Domain.Enums;

namespace CartWise.Domain.Tests;

public class PurchaseTests
{
private static readonly DateTime Now = new(2026, 8, 10, 12, 0, 0, DateTimeKind.Utc);

[Fact]
public void Constructor_DefaultsToManualSourceAndNoStore()
{
var purchase = new Purchase(Guid.NewGuid(), "user-1", Now, Now);

Assert.Equal(PurchaseSourceType.Manual, purchase.SourceType);
Assert.Null(purchase.StoreLocationId);
Assert.Empty(purchase.Items);
}

[Fact]
public void Constructor_ThrowsWhenHouseholdIdIsEmpty()
{
Assert.Throws<ArgumentException>(() => new Purchase(Guid.Empty, "user-1", Now, Now));
}

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Constructor_ThrowsWhenPurchasedByUserIdIsMissing(string? userId)
{
Assert.Throws<ArgumentException>(() => new Purchase(Guid.NewGuid(), userId!, Now, Now));
}
}

public class PurchaseItemTests
{
private static readonly DateTime Now = new(2026, 8, 10, 12, 0, 0, DateTimeKind.Utc);

[Fact]
public void Constructor_DefaultsQuantityToOne()
{
var item = new PurchaseItem(Guid.NewGuid(), "Milk", 3.99m, Now);

Assert.Equal(1m, item.Quantity);
Assert.Equal(3.99m, item.LinePrice);
}

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Constructor_ThrowsWhenDescriptionIsMissing(string? description)
{
Assert.Throws<ArgumentException>(() => new PurchaseItem(Guid.NewGuid(), description!, 3.99m, Now));
}

[Fact]
public void Constructor_ThrowsWhenQuantityIsZeroOrNegative()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new PurchaseItem(Guid.NewGuid(), "Milk", 3.99m, Now, quantity: 0));
Assert.Throws<ArgumentOutOfRangeException>(() => new PurchaseItem(Guid.NewGuid(), "Milk", 3.99m, Now, quantity: -1));
}

[Fact]
public void Constructor_ThrowsWhenLinePriceIsNegative()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new PurchaseItem(Guid.NewGuid(), "Milk", -1m, Now));
}
}

Loading…
Annulla
Salva

Powered by TurnKey Linux.