diff --git a/src/Printbase.Application/Class1.cs b/src/Printbase.Application/Class1.cs
deleted file mode 100644
index fff6417..0000000
--- a/src/Printbase.Application/Class1.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Printbase.Application;
-
-public class Class1
-{
-}
\ No newline at end of file
diff --git a/src/Printbase.Application/Printbase.Application.csproj b/src/Printbase.Application/Printbase.Application.csproj
index 17b910f..d096b39 100644
--- a/src/Printbase.Application/Printbase.Application.csproj
+++ b/src/Printbase.Application/Printbase.Application.csproj
@@ -6,4 +6,8 @@
enable
+
+
+
+
diff --git a/src/Printbase.Domain/Class1.cs b/src/Printbase.Domain/Class1.cs
deleted file mode 100644
index faafe41..0000000
--- a/src/Printbase.Domain/Class1.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Printbase.Domain;
-
-public class Class1
-{
-}
\ No newline at end of file
diff --git a/src/Printbase.Domain/Entities/Products/Product.cs b/src/Printbase.Domain/Entities/Products/Product.cs
new file mode 100644
index 0000000..bd7dbdf
--- /dev/null
+++ b/src/Printbase.Domain/Entities/Products/Product.cs
@@ -0,0 +1,80 @@
+namespace Printbase.Domain.Entities.Products;
+
+public class Product
+{
+ public Guid Id { get; private set; }
+ public string Name { get; private set; }
+ public string? Description { get; private set; }
+ public decimal? Discount { get; private set; }
+ public Guid TypeId { get; private set; }
+ public IReadOnlyCollection Variants => _variants.AsReadOnly();
+
+ private readonly List _variants = [];
+
+ private Product() { }
+
+ public Product(Guid id, string name, Guid typeId, string? description = null, decimal? discount = null)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Product name cannot be empty", nameof(name));
+
+ if (typeId == Guid.Empty)
+ throw new ArgumentException("Type ID cannot be empty", nameof(typeId));
+
+ Id = id;
+ Name = name;
+ TypeId = typeId;
+ Description = description;
+ Discount = discount;
+ }
+
+ public void AddVariant(ProductVariant variant)
+ {
+ ArgumentNullException.ThrowIfNull(variant);
+
+ _variants.Add(variant);
+ }
+
+ public void RemoveVariant(Guid variantId)
+ {
+ var variant = _variants.Find(v => v.Id == variantId);
+ if (variant != null)
+ {
+ _variants.Remove(variant);
+ }
+ }
+
+ public void UpdateName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Product name cannot be empty", nameof(name));
+
+ Name = name;
+ }
+
+ public void UpdateDescription(string? description)
+ {
+ Description = description;
+ }
+
+ public void UpdateDiscount(decimal? discount)
+ {
+ if (discount.HasValue && (discount.Value < 0 || discount.Value > 100))
+ throw new ArgumentException("Discount must be between 0 and 100", nameof(discount));
+
+ Discount = discount;
+ }
+
+ public void UpdateType(Guid typeId)
+ {
+ if (typeId == Guid.Empty)
+ throw new ArgumentException("Type ID cannot be empty", nameof(typeId));
+
+ TypeId = typeId;
+ }
+
+ public decimal? GetEffectiveDiscount()
+ {
+ return Discount;
+ }
+}
\ No newline at end of file
diff --git a/src/Printbase.Domain/Entities/Products/ProductGroup.cs b/src/Printbase.Domain/Entities/Products/ProductGroup.cs
new file mode 100644
index 0000000..8241e7a
--- /dev/null
+++ b/src/Printbase.Domain/Entities/Products/ProductGroup.cs
@@ -0,0 +1,49 @@
+namespace Printbase.Domain.Entities.Products;
+
+public class ProductGroup
+{
+ public Guid Id { get; private set; }
+ public string Name { get; private set; }
+ public string? Description { get; private set; }
+ public IReadOnlyCollection Types => _types.AsReadOnly();
+
+ private readonly List _types = new();
+
+ private ProductGroup() { }
+
+ public ProductGroup(Guid id, string name, string? description = null)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Group name cannot be empty", nameof(name));
+
+ Id = id;
+ Name = name;
+ Description = description;
+ }
+
+ public void AddType(ProductType type)
+ {
+ ArgumentNullException.ThrowIfNull(type);
+
+ _types.Add(type);
+ }
+
+ public void RemoveType(Guid typeId)
+ {
+ var type = _types.Find(t => t.Id == typeId);
+ if (type != null) _types.Remove(type);
+ }
+
+ public void UpdateName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Group name cannot be empty", nameof(name));
+
+ Name = name;
+ }
+
+ public void UpdateDescription(string? description)
+ {
+ Description = description;
+ }
+}
\ No newline at end of file
diff --git a/src/Printbase.Domain/Entities/Products/ProductType.cs b/src/Printbase.Domain/Entities/Products/ProductType.cs
new file mode 100644
index 0000000..1658608
--- /dev/null
+++ b/src/Printbase.Domain/Entities/Products/ProductType.cs
@@ -0,0 +1,38 @@
+namespace Printbase.Domain.Entities.Products;
+
+public class ProductType
+{
+ public Guid Id { get; private set; }
+ public string Name { get; private set; }
+ public string? Description { get; private set; }
+ public Guid GroupId { get; private set; }
+
+ private ProductType() { }
+
+ public ProductType(Guid id, string name, Guid groupId, string? description = null)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Type name cannot be empty", nameof(name));
+
+ if (groupId == Guid.Empty)
+ throw new ArgumentException("Group ID cannot be empty", nameof(groupId));
+
+ Id = id;
+ Name = name;
+ GroupId = groupId;
+ Description = description;
+ }
+
+ public void UpdateName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Type name cannot be empty", nameof(name));
+
+ Name = name;
+ }
+
+ public void UpdateDescription(string? description)
+ {
+ Description = description;
+ }
+}
\ No newline at end of file
diff --git a/src/Printbase.Domain/Entities/Products/ProductVariant.cs b/src/Printbase.Domain/Entities/Products/ProductVariant.cs
new file mode 100644
index 0000000..078f16e
--- /dev/null
+++ b/src/Printbase.Domain/Entities/Products/ProductVariant.cs
@@ -0,0 +1,97 @@
+namespace Printbase.Domain.Entities.Products;
+
+public class ProductVariant
+{
+ public Guid Id { get; private set; }
+ public Guid ProductId { get; private set; }
+ public string? Color { get; private set; }
+ public string? Size { get; private set; }
+ public decimal Price { get; private set; }
+ public decimal? Discount { get; private set; }
+ public int Stock { get; private set; }
+
+ private ProductVariant() { }
+
+ public ProductVariant(Guid id, Guid productId, decimal price, string? color = null, string? size = null,
+ decimal? discount = null, int stock = 0)
+ {
+ if (price < 0)
+ throw new ArgumentException("Price cannot be negative", nameof(price));
+
+ Id = id;
+ ProductId = productId;
+ Color = color;
+ Size = size;
+ Price = price;
+ Discount = discount;
+ Stock = stock;
+ }
+
+ public void UpdateColor(string? color)
+ {
+ Color = color;
+ }
+
+ public void UpdateSize(string? size)
+ {
+ Size = size;
+ }
+
+ public void UpdatePrice(decimal price)
+ {
+ if (price < 0) throw new ArgumentException("Price cannot be negative", nameof(price));
+
+ Price = price;
+ }
+
+ public void UpdateDiscount(decimal? discount)
+ {
+ if (discount is < 0 or > 100)
+ throw new ArgumentException("Discount must be between 0 and 100", nameof(discount));
+
+ Discount = discount;
+ }
+
+ public void UpdateStock(int quantity)
+ {
+ if (quantity < 0)
+ throw new ArgumentException("Stock quantity cannot be negative", nameof(quantity));
+
+ Stock = quantity;
+ }
+
+ public void AddStock(int quantity)
+ {
+ if (quantity <= 0)
+ throw new ArgumentException("Quantity to add must be positive", nameof(quantity));
+
+ Stock += quantity;
+ }
+
+ public bool RemoveStock(int quantity)
+ {
+ if (quantity <= 0)
+ throw new ArgumentException("Quantity to remove must be positive", nameof(quantity));
+
+ if (Stock < quantity) return false;
+
+ Stock -= quantity;
+ return true;
+ }
+
+ public decimal GetEffectivePrice(decimal? productDiscount = null)
+ {
+ var effectivePrice = Price;
+
+ var discountToApply = Discount ?? productDiscount;
+
+ if (discountToApply is > 0) effectivePrice -= (effectivePrice * discountToApply.Value / 100);
+
+ return Math.Round(effectivePrice, 2);
+ }
+
+ public decimal? GetEffectiveDiscount(decimal? productDiscount = null)
+ {
+ return Discount ?? productDiscount;
+ }
+}
\ No newline at end of file
diff --git a/src/Printbase.Domain/Printbase.Domain.csproj b/src/Printbase.Domain/Printbase.Domain.csproj
index 17b910f..93e8640 100644
--- a/src/Printbase.Domain/Printbase.Domain.csproj
+++ b/src/Printbase.Domain/Printbase.Domain.csproj
@@ -6,4 +6,8 @@
enable
+
+
+
+
diff --git a/src/Printbase.Infrastructure/Class1.cs b/src/Printbase.Infrastructure/Class1.cs
deleted file mode 100644
index 73d80a3..0000000
--- a/src/Printbase.Infrastructure/Class1.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Printbase.Infrastructure;
-
-public class Class1
-{
-}
\ No newline at end of file
diff --git a/src/Printbase.Infrastructure/DbEntities/Products/ProductDbEntity.cs b/src/Printbase.Infrastructure/DbEntities/Products/ProductDbEntity.cs
new file mode 100644
index 0000000..a0d870b
--- /dev/null
+++ b/src/Printbase.Infrastructure/DbEntities/Products/ProductDbEntity.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Printbase.Infrastructure.DbEntities.Products;
+
+[Table("Products")]
+public class ProductDbEntity
+{
+ [Key, Required]
+ public Guid Id { get; set; }
+
+ [MaxLength(50), Required]
+ public required string Name { get; set; }
+
+ [MaxLength(1000)]
+ public string? Description { get; set; }
+
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal? Discount { get; set; }
+
+ [Required]
+ public Guid TypeId { get; set; }
+
+ [ForeignKey(nameof(TypeId)), Required]
+ public required ProductTypeDbEntity Type { get; set; }
+
+ [InverseProperty(nameof(ProductVariantDbEntity.Product)), Required]
+ public required ICollection Variants { get; set; }
+}
\ No newline at end of file
diff --git a/src/Printbase.Infrastructure/DbEntities/Products/ProductGroupDbEntity.cs b/src/Printbase.Infrastructure/DbEntities/Products/ProductGroupDbEntity.cs
new file mode 100644
index 0000000..08bacd0
--- /dev/null
+++ b/src/Printbase.Infrastructure/DbEntities/Products/ProductGroupDbEntity.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Printbase.Infrastructure.DbEntities.Products;
+
+[Table("ProductGroups")]
+public class ProductGroupDbEntity
+{
+ [Key, Required]
+ public Guid Id { get; set; }
+
+ [MaxLength(50), Required]
+ public required string Name { get; set; }
+
+ [MaxLength(255)]
+ public string? Description { get; set; }
+
+ [InverseProperty(nameof(ProductTypeDbEntity.Group)), Required]
+ public required ICollection Types { get; set; }
+}
\ No newline at end of file
diff --git a/src/Printbase.Infrastructure/DbEntities/Products/ProductTypeDbEntity.cs b/src/Printbase.Infrastructure/DbEntities/Products/ProductTypeDbEntity.cs
new file mode 100644
index 0000000..127b334
--- /dev/null
+++ b/src/Printbase.Infrastructure/DbEntities/Products/ProductTypeDbEntity.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Printbase.Infrastructure.DbEntities.Products;
+
+[Table("ProductTypes")]
+public class ProductTypeDbEntity
+{
+ [Key, Required]
+ public Guid Id { get; set; }
+
+ [MaxLength(50), Required]
+ public required string Name { get; set; }
+
+ [MaxLength(255)]
+ public string? Description { get; set; }
+
+ [Required]
+ public Guid GroupId { get; set; }
+
+ [ForeignKey(nameof(GroupId)), Required]
+ public required ProductGroupDbEntity Group { get; set; }
+
+ [InverseProperty(nameof(ProductDbEntity.Type))]
+ public ICollection? Products { get; set; }
+}
\ No newline at end of file
diff --git a/src/Printbase.Infrastructure/DbEntities/Products/ProductVariantDbEntity.cs b/src/Printbase.Infrastructure/DbEntities/Products/ProductVariantDbEntity.cs
new file mode 100644
index 0000000..e158e3a
--- /dev/null
+++ b/src/Printbase.Infrastructure/DbEntities/Products/ProductVariantDbEntity.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Printbase.Infrastructure.DbEntities.Products;
+
+[Table("ProductVariants")]
+public class ProductVariantDbEntity
+{
+ [Key, Required]
+ public Guid Id { get; set; }
+
+ [Required]
+ public Guid ProductId { get; set; }
+
+ [MaxLength(50)]
+ public string? Color { get; set; }
+
+ [MaxLength(20)]
+ public string? Size { get; set; }
+
+ [Column(TypeName = "decimal(18,2)"), Required]
+ public decimal Price { get; set; }
+
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal? Discount { get; set; }
+
+ [Required]
+ public int Stock { get; set; }
+
+ [ForeignKey(nameof(ProductId)), Required]
+ public required ProductDbEntity Product { get; set; }
+}
\ No newline at end of file