diff --git a/Imprink.sln b/Imprink.sln
new file mode 100644
index 0000000..d906648
--- /dev/null
+++ b/Imprink.sln
@@ -0,0 +1,139 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imprink.WebApi", "src\Imprink.WebApi\Imprink.WebApi.csproj", "{659FA9DF-3AAF-42B0-9613-1EB79BB84053}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{06B9A385-FCDC-4052-B0AC-2B20316CC67F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imprink.Domain", "src\Imprink.Domain\Imprink.Domain.csproj", "{45A42670-C3A2-4B21-B3C8-1AD71EE41348}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imprink.Infrastructure", "src\Imprink.Infrastructure\Imprink.Infrastructure.csproj", "{5566AAE7-444D-41F8-BC6D-59C341EBF6F3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imprink.Application", "src\Imprink.Application\Imprink.Application.csproj", "{379021C7-7B82-4643-A1D6-B789CEDE9BDE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imprink.Application.Tests", "tests\Imprink.Application.Tests\Imprink.Application.Tests.csproj", "{E858950C-69E0-426B-A679-CD1B944C2F8F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imprink.Domain.Tests", "tests\Imprink.Domain.Tests\Imprink.Domain.Tests.csproj", "{8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imprink.Infrastructure.Tests", "tests\Imprink.Infrastructure.Tests\Imprink.Infrastructure.Tests.csproj", "{E683C824-15F9-4FD3-9947-3756E48934A4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imprink.WebApi.Tests", "tests\Imprink.WebApi.Tests\Imprink.WebApi.Tests.csproj", "{0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Debug|x64.Build.0 = Debug|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Debug|x86.Build.0 = Debug|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Release|Any CPU.Build.0 = Release|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Release|x64.ActiveCfg = Release|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Release|x64.Build.0 = Release|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Release|x86.ActiveCfg = Release|Any CPU
+ {659FA9DF-3AAF-42B0-9613-1EB79BB84053}.Release|x86.Build.0 = Release|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Debug|x64.Build.0 = Debug|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Debug|x86.Build.0 = Debug|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Release|Any CPU.Build.0 = Release|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Release|x64.ActiveCfg = Release|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Release|x64.Build.0 = Release|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Release|x86.ActiveCfg = Release|Any CPU
+ {45A42670-C3A2-4B21-B3C8-1AD71EE41348}.Release|x86.Build.0 = Release|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Debug|x64.Build.0 = Debug|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Debug|x86.Build.0 = Debug|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Release|x64.ActiveCfg = Release|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Release|x64.Build.0 = Release|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Release|x86.ActiveCfg = Release|Any CPU
+ {5566AAE7-444D-41F8-BC6D-59C341EBF6F3}.Release|x86.Build.0 = Release|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Debug|x64.Build.0 = Debug|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Debug|x86.Build.0 = Debug|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Release|x64.ActiveCfg = Release|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Release|x64.Build.0 = Release|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Release|x86.ActiveCfg = Release|Any CPU
+ {379021C7-7B82-4643-A1D6-B789CEDE9BDE}.Release|x86.Build.0 = Release|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Debug|x64.Build.0 = Debug|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Debug|x86.Build.0 = Debug|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Release|x64.ActiveCfg = Release|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Release|x64.Build.0 = Release|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Release|x86.ActiveCfg = Release|Any CPU
+ {E858950C-69E0-426B-A679-CD1B944C2F8F}.Release|x86.Build.0 = Release|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Debug|x64.Build.0 = Debug|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Debug|x86.Build.0 = Debug|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Release|x64.ActiveCfg = Release|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Release|x64.Build.0 = Release|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Release|x86.ActiveCfg = Release|Any CPU
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E}.Release|x86.Build.0 = Release|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Debug|x64.Build.0 = Debug|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Debug|x86.Build.0 = Debug|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Release|x64.ActiveCfg = Release|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Release|x64.Build.0 = Release|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Release|x86.ActiveCfg = Release|Any CPU
+ {E683C824-15F9-4FD3-9947-3756E48934A4}.Release|x86.Build.0 = Release|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Debug|x64.Build.0 = Debug|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Debug|x86.Build.0 = Debug|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Release|x64.ActiveCfg = Release|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Release|x64.Build.0 = Release|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Release|x86.ActiveCfg = Release|Any CPU
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {E858950C-69E0-426B-A679-CD1B944C2F8F} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {8CB5B0C8-ED1B-486F-9A02-ED507F533D0E} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {E683C824-15F9-4FD3-9947-3756E48934A4} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {0EF1EBCE-22E2-4511-B845-EA3EC792DFE3} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Imprink.Application/IUnitOfWork.cs b/src/Imprink.Application/IUnitOfWork.cs
new file mode 100644
index 0000000..fbc2f86
--- /dev/null
+++ b/src/Imprink.Application/IUnitOfWork.cs
@@ -0,0 +1,15 @@
+using Imprink.Domain.Repositories;
+
+namespace Imprink.Application;
+
+public interface IUnitOfWork
+{
+ public IProductRepository ProductRepository { get; }
+ public ICategoryRepository CategoryRepository { get; }
+ public IProductVariantRepository ProductVariantRepository { get; }
+
+ Task SaveAsync(CancellationToken cancellationToken = default);
+ Task BeginTransactionAsync(CancellationToken cancellationToken = default);
+ Task CommitTransactionAsync(CancellationToken cancellationToken = default);
+ Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Imprink.Application.csproj b/src/Imprink.Application/Imprink.Application.csproj
new file mode 100644
index 0000000..7d1c2ec
--- /dev/null
+++ b/src/Imprink.Application/Imprink.Application.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Imprink.Application/Products/Commands/CreateCategoryCommand.cs b/src/Imprink.Application/Products/Commands/CreateCategoryCommand.cs
new file mode 100644
index 0000000..3ffbc28
--- /dev/null
+++ b/src/Imprink.Application/Products/Commands/CreateCategoryCommand.cs
@@ -0,0 +1,14 @@
+using Imprink.Application.Products.Dtos;
+using MediatR;
+
+namespace Imprink.Application.Products.Commands;
+
+public class CreateCategoryCommand : IRequest
+{
+ public string Name { get; set; } = null!;
+ public string Description { get; set; } = null!;
+ public string? ImageUrl { get; set; }
+ public int SortOrder { get; set; }
+ public bool IsActive { get; set; } = true;
+ public Guid? ParentCategoryId { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Commands/CreateProductCommand.cs b/src/Imprink.Application/Products/Commands/CreateProductCommand.cs
new file mode 100644
index 0000000..4233044
--- /dev/null
+++ b/src/Imprink.Application/Products/Commands/CreateProductCommand.cs
@@ -0,0 +1,15 @@
+using Imprink.Application.Products.Dtos;
+using MediatR;
+
+namespace Imprink.Application.Products.Commands;
+
+public class CreateProductCommand : IRequest
+{
+ public string Name { get; set; } = null!;
+ public string? Description { get; set; }
+ public decimal BasePrice { get; set; }
+ public bool IsCustomizable { get; set; }
+ public bool IsActive { get; set; } = true;
+ public string? ImageUrl { get; set; }
+ public Guid? CategoryId { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Commands/CreateProductVariantCommand.cs b/src/Imprink.Application/Products/Commands/CreateProductVariantCommand.cs
new file mode 100644
index 0000000..f6edcfd
--- /dev/null
+++ b/src/Imprink.Application/Products/Commands/CreateProductVariantCommand.cs
@@ -0,0 +1,16 @@
+using Imprink.Application.Products.Dtos;
+using MediatR;
+
+namespace Imprink.Application.Products.Commands;
+
+public class CreateProductVariantCommand : IRequest
+{
+ public Guid ProductId { get; set; }
+ public string Size { get; set; } = null!;
+ public string? Color { get; set; }
+ public decimal Price { get; set; }
+ public string? ImageUrl { get; set; }
+ public string Sku { get; set; } = null!;
+ public int StockQuantity { get; set; }
+ public bool IsActive { get; set; } = true;
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Commands/DeleteCategoryCommand.cs b/src/Imprink.Application/Products/Commands/DeleteCategoryCommand.cs
new file mode 100644
index 0000000..28f3694
--- /dev/null
+++ b/src/Imprink.Application/Products/Commands/DeleteCategoryCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace Imprink.Application.Products.Commands;
+
+public class DeleteCategoryCommand : IRequest
+{
+ public Guid Id { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Commands/DeleteProductCommand.cs b/src/Imprink.Application/Products/Commands/DeleteProductCommand.cs
new file mode 100644
index 0000000..a40687f
--- /dev/null
+++ b/src/Imprink.Application/Products/Commands/DeleteProductCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace Imprink.Application.Products.Commands;
+
+public class DeleteProductCommand : IRequest
+{
+ public Guid Id { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Commands/DeleteProductVariantCommand.cs b/src/Imprink.Application/Products/Commands/DeleteProductVariantCommand.cs
new file mode 100644
index 0000000..df13945
--- /dev/null
+++ b/src/Imprink.Application/Products/Commands/DeleteProductVariantCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace Imprink.Application.Products.Commands;
+
+public class DeleteProductVariantCommand : IRequest
+{
+ public Guid Id { get; set; }
+}
diff --git a/src/Imprink.Application/Products/Dtos/CategoryDto.cs b/src/Imprink.Application/Products/Dtos/CategoryDto.cs
new file mode 100644
index 0000000..fbc701b
--- /dev/null
+++ b/src/Imprink.Application/Products/Dtos/CategoryDto.cs
@@ -0,0 +1,14 @@
+namespace Imprink.Application.Products.Dtos;
+
+public class CategoryDto
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; } = null!;
+ public string Description { get; set; } = null!;
+ public string? ImageUrl { get; set; }
+ public int SortOrder { get; set; }
+ public bool IsActive { get; set; }
+ public Guid? ParentCategoryId { get; set; }
+ public DateTime? CreatedAt { get; set; }
+ public DateTime? ModifiedAt { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Dtos/PagedResultDto.cs b/src/Imprink.Application/Products/Dtos/PagedResultDto.cs
new file mode 100644
index 0000000..f654054
--- /dev/null
+++ b/src/Imprink.Application/Products/Dtos/PagedResultDto.cs
@@ -0,0 +1,12 @@
+namespace Imprink.Application.Products.Dtos;
+
+public class PagedResultDto
+{
+ public IEnumerable Items { get; set; } = new List();
+ public int TotalCount { get; set; }
+ public int PageNumber { get; set; }
+ public int PageSize { get; set; }
+ public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
+ public bool HasPreviousPage => PageNumber > 1;
+ public bool HasNextPage => PageNumber < TotalPages;
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Dtos/ProductDto.cs b/src/Imprink.Application/Products/Dtos/ProductDto.cs
new file mode 100644
index 0000000..abb8403
--- /dev/null
+++ b/src/Imprink.Application/Products/Dtos/ProductDto.cs
@@ -0,0 +1,16 @@
+namespace Imprink.Application.Products.Dtos;
+
+public class ProductDto
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; } = null!;
+ public string? Description { get; set; }
+ public decimal BasePrice { get; set; }
+ public bool IsCustomizable { get; set; }
+ public bool IsActive { get; set; }
+ public string? ImageUrl { get; set; }
+ public Guid? CategoryId { get; set; }
+ public CategoryDto? Category { get; set; }
+ public DateTime? CreatedAt { get; set; }
+ public DateTime? ModifiedAt { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Dtos/ProductVariantDto.cs b/src/Imprink.Application/Products/Dtos/ProductVariantDto.cs
new file mode 100644
index 0000000..881c54d
--- /dev/null
+++ b/src/Imprink.Application/Products/Dtos/ProductVariantDto.cs
@@ -0,0 +1,17 @@
+namespace Imprink.Application.Products.Dtos;
+
+public class ProductVariantDto
+{
+ public Guid Id { get; set; }
+ public Guid ProductId { get; set; }
+ public string Size { get; set; } = null!;
+ public string? Color { get; set; }
+ public decimal Price { get; set; }
+ public string? ImageUrl { get; set; }
+ public string Sku { get; set; } = null!;
+ public int StockQuantity { get; set; }
+ public bool IsActive { get; set; }
+ public ProductDto? Product { get; set; }
+ public DateTime? CreatedAt { get; set; }
+ public DateTime? ModifiedAt { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/CreateCategoryHandler.cs b/src/Imprink.Application/Products/Handlers/CreateCategoryHandler.cs
new file mode 100644
index 0000000..1d3c136
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/CreateCategoryHandler.cs
@@ -0,0 +1,48 @@
+using Imprink.Application.Products.Commands;
+using Imprink.Application.Products.Dtos;
+using Imprink.Domain.Entities.Product;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class CreateCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler
+{
+ public async Task Handle(CreateCategoryCommand request, CancellationToken cancellationToken)
+ {
+ await unitOfWork.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ var category = new Category
+ {
+ Name = request.Name,
+ Description = request.Description,
+ ImageUrl = request.ImageUrl,
+ SortOrder = request.SortOrder,
+ IsActive = request.IsActive,
+ ParentCategoryId = request.ParentCategoryId
+ };
+
+ var createdCategory = await unitOfWork.CategoryRepository.AddAsync(category, cancellationToken);
+ await unitOfWork.CommitTransactionAsync(cancellationToken);
+
+ return new CategoryDto
+ {
+ Id = createdCategory.Id,
+ Name = createdCategory.Name,
+ Description = createdCategory.Description,
+ ImageUrl = createdCategory.ImageUrl,
+ SortOrder = createdCategory.SortOrder,
+ IsActive = createdCategory.IsActive,
+ ParentCategoryId = createdCategory.ParentCategoryId,
+ CreatedAt = createdCategory.CreatedAt,
+ ModifiedAt = createdCategory.ModifiedAt
+ };
+ }
+ catch
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/CreateProductHandler.cs b/src/Imprink.Application/Products/Handlers/CreateProductHandler.cs
new file mode 100644
index 0000000..ba353eb
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/CreateProductHandler.cs
@@ -0,0 +1,66 @@
+using Imprink.Application.Products.Commands;
+using Imprink.Application.Products.Dtos;
+using Imprink.Domain.Entities.Product;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class CreateProductHandler(IUnitOfWork unitOfWork) : IRequestHandler
+{
+ public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken)
+ {
+ await unitOfWork.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ var product = new Product
+ {
+ Name = request.Name,
+ Description = request.Description,
+ BasePrice = request.BasePrice,
+ IsCustomizable = request.IsCustomizable,
+ IsActive = request.IsActive,
+ ImageUrl = request.ImageUrl,
+ CategoryId = request.CategoryId,
+ Category = null!
+ };
+
+ var createdProduct = await unitOfWork.ProductRepository.AddAsync(product, cancellationToken);
+
+ var categoryDto = new CategoryDto
+ {
+ Id = createdProduct.Category.Id,
+ Name = createdProduct.Category.Name,
+ Description = createdProduct.Category.Description,
+ ImageUrl = createdProduct.Category.ImageUrl,
+ SortOrder = createdProduct.Category.SortOrder,
+ IsActive = createdProduct.Category.IsActive,
+ ParentCategoryId = createdProduct.Category.ParentCategoryId,
+ CreatedAt = createdProduct.Category.CreatedAt,
+ ModifiedAt = createdProduct.Category.ModifiedAt
+ };
+
+ await unitOfWork.CommitTransactionAsync(cancellationToken);
+
+ return new ProductDto
+ {
+ Id = createdProduct.Id,
+ Name = createdProduct.Name,
+ Description = createdProduct.Description,
+ BasePrice = createdProduct.BasePrice,
+ IsCustomizable = createdProduct.IsCustomizable,
+ IsActive = createdProduct.IsActive,
+ ImageUrl = createdProduct.ImageUrl,
+ CategoryId = createdProduct.CategoryId,
+ Category = categoryDto,
+ CreatedAt = createdProduct.CreatedAt,
+ ModifiedAt = createdProduct.ModifiedAt
+ };
+ }
+ catch
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/CreateProductVariantHandler.cs b/src/Imprink.Application/Products/Handlers/CreateProductVariantHandler.cs
new file mode 100644
index 0000000..f02bc88
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/CreateProductVariantHandler.cs
@@ -0,0 +1,54 @@
+using Imprink.Application.Products.Commands;
+using Imprink.Application.Products.Dtos;
+using Imprink.Domain.Entities.Product;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class CreateProductVariantHandler(IUnitOfWork unitOfWork)
+ : IRequestHandler
+{
+ public async Task Handle(CreateProductVariantCommand request, CancellationToken cancellationToken)
+ {
+ await unitOfWork.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ var productVariant = new ProductVariant
+ {
+ ProductId = request.ProductId,
+ Size = request.Size,
+ Color = request.Color,
+ Price = request.Price,
+ ImageUrl = request.ImageUrl,
+ Sku = request.Sku,
+ StockQuantity = request.StockQuantity,
+ IsActive = request.IsActive,
+ Product = null!
+ };
+
+ var createdVariant = await unitOfWork.ProductVariantRepository.AddAsync(productVariant, cancellationToken);
+ await unitOfWork.CommitTransactionAsync(cancellationToken);
+
+ return new ProductVariantDto
+ {
+ Id = createdVariant.Id,
+ ProductId = createdVariant.ProductId,
+ Size = createdVariant.Size,
+ Color = createdVariant.Color,
+ Price = createdVariant.Price,
+ ImageUrl = createdVariant.ImageUrl,
+ Sku = createdVariant.Sku,
+ StockQuantity = createdVariant.StockQuantity,
+ IsActive = createdVariant.IsActive,
+ CreatedAt = createdVariant.CreatedAt,
+ ModifiedAt = createdVariant.ModifiedAt
+ };
+ }
+ catch
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/DeleteCategoryHandler.cs b/src/Imprink.Application/Products/Handlers/DeleteCategoryHandler.cs
new file mode 100644
index 0000000..4b93917
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/DeleteCategoryHandler.cs
@@ -0,0 +1,31 @@
+using Imprink.Application.Products.Commands;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class DeleteCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler
+{
+ public async Task Handle(DeleteCategoryCommand request, CancellationToken cancellationToken)
+ {
+ await unitOfWork.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ var exists = await unitOfWork.CategoryRepository.ExistsAsync(request.Id, cancellationToken);
+ if (!exists)
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ return false;
+ }
+
+ await unitOfWork.CategoryRepository.DeleteAsync(request.Id, cancellationToken);
+ await unitOfWork.CommitTransactionAsync(cancellationToken);
+ return true;
+ }
+ catch
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/DeleteProductHandler.cs b/src/Imprink.Application/Products/Handlers/DeleteProductHandler.cs
new file mode 100644
index 0000000..29de5c2
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/DeleteProductHandler.cs
@@ -0,0 +1,31 @@
+using Imprink.Application.Products.Commands;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class DeleteProductHandler(IUnitOfWork unitOfWork) : IRequestHandler
+{
+ public async Task Handle(DeleteProductCommand request, CancellationToken cancellationToken)
+ {
+ await unitOfWork.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ var exists = await unitOfWork.ProductRepository.ExistsAsync(request.Id, cancellationToken);
+ if (!exists)
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ return false;
+ }
+
+ await unitOfWork.ProductRepository.DeleteAsync(request.Id, cancellationToken);
+ await unitOfWork.CommitTransactionAsync(cancellationToken);
+ return true;
+ }
+ catch
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/DeleteProductVariantHandler.cs b/src/Imprink.Application/Products/Handlers/DeleteProductVariantHandler.cs
new file mode 100644
index 0000000..d39a6f6
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/DeleteProductVariantHandler.cs
@@ -0,0 +1,31 @@
+using Imprink.Application.Products.Commands;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class DeleteProductVariantHandler(IUnitOfWork unitOfWork) : IRequestHandler
+{
+ public async Task Handle(DeleteProductVariantCommand request, CancellationToken cancellationToken)
+ {
+ await unitOfWork.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ var exists = await unitOfWork.ProductVariantRepository.ExistsAsync(request.Id, cancellationToken);
+ if (!exists)
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ return false;
+ }
+
+ await unitOfWork.ProductVariantRepository.DeleteAsync(request.Id, cancellationToken);
+ await unitOfWork.CommitTransactionAsync(cancellationToken);
+ return true;
+ }
+ catch
+ {
+ await unitOfWork.RollbackTransactionAsync(cancellationToken);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/GetCategoriesHandler.cs b/src/Imprink.Application/Products/Handlers/GetCategoriesHandler.cs
new file mode 100644
index 0000000..8bb0985
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/GetCategoriesHandler.cs
@@ -0,0 +1,41 @@
+using Imprink.Application.Products.Dtos;
+using Imprink.Application.Products.Queries;
+using Imprink.Domain.Entities.Product;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class GetCategoriesHandler(IUnitOfWork unitOfWork)
+ : IRequestHandler>
+{
+ public async Task> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
+ {
+ IEnumerable categories;
+
+ if (request.RootCategoriesOnly)
+ {
+ categories = await unitOfWork.CategoryRepository.GetRootCategoriesAsync(cancellationToken);
+ }
+ else if (request.IsActive.HasValue && request.IsActive.Value)
+ {
+ categories = await unitOfWork.CategoryRepository.GetActiveAsync(cancellationToken);
+ }
+ else
+ {
+ categories = await unitOfWork.CategoryRepository.GetAllAsync(cancellationToken);
+ }
+
+ return categories.Select(c => new CategoryDto
+ {
+ Id = c.Id,
+ Name = c.Name,
+ Description = c.Description,
+ ImageUrl = c.ImageUrl,
+ SortOrder = c.SortOrder,
+ IsActive = c.IsActive,
+ ParentCategoryId = c.ParentCategoryId,
+ CreatedAt = c.CreatedAt,
+ ModifiedAt = c.ModifiedAt
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/GetProductVariantsHandler.cs b/src/Imprink.Application/Products/Handlers/GetProductVariantsHandler.cs
new file mode 100644
index 0000000..cddae77
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/GetProductVariantsHandler.cs
@@ -0,0 +1,63 @@
+using Imprink.Application.Products.Dtos;
+using Imprink.Application.Products.Queries;
+using Imprink.Domain.Entities.Product;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class GetProductVariantsHandler(IUnitOfWork unitOfWork)
+ : IRequestHandler>
+{
+ public async Task> Handle(GetProductVariantsQuery request, CancellationToken cancellationToken)
+ {
+ IEnumerable variants;
+
+ if (request.ProductId.HasValue)
+ {
+ if (request.InStockOnly)
+ {
+ variants = await unitOfWork.ProductVariantRepository.GetInStockByProductIdAsync(request.ProductId.Value, cancellationToken);
+ }
+ else if (request.IsActive.HasValue && request.IsActive.Value)
+ {
+ variants = await unitOfWork.ProductVariantRepository.GetActiveByProductIdAsync(request.ProductId.Value, cancellationToken);
+ }
+ else
+ {
+ variants = await unitOfWork.ProductVariantRepository.GetByProductIdAsync(request.ProductId.Value, cancellationToken);
+ }
+ }
+ else
+ {
+ variants = new List();
+ }
+
+ return variants.Select(pv => new ProductVariantDto
+ {
+ Id = pv.Id,
+ ProductId = pv.ProductId,
+ Size = pv.Size,
+ Color = pv.Color,
+ Price = pv.Price,
+ ImageUrl = pv.ImageUrl,
+ Sku = pv.Sku,
+ StockQuantity = pv.StockQuantity,
+ IsActive = pv.IsActive,
+ Product = new ProductDto
+ {
+ Id = pv.Product.Id,
+ Name = pv.Product.Name,
+ Description = pv.Product.Description,
+ BasePrice = pv.Product.BasePrice,
+ IsCustomizable = pv.Product.IsCustomizable,
+ IsActive = pv.Product.IsActive,
+ ImageUrl = pv.Product.ImageUrl,
+ CategoryId = pv.Product.CategoryId,
+ CreatedAt = pv.Product.CreatedAt,
+ ModifiedAt = pv.Product.ModifiedAt
+ },
+ CreatedAt = pv.CreatedAt,
+ ModifiedAt = pv.ModifiedAt
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Handlers/GetProductsHandler.cs b/src/Imprink.Application/Products/Handlers/GetProductsHandler.cs
new file mode 100644
index 0000000..4116892
--- /dev/null
+++ b/src/Imprink.Application/Products/Handlers/GetProductsHandler.cs
@@ -0,0 +1,47 @@
+using Imprink.Application.Products.Dtos;
+using Imprink.Application.Products.Queries;
+using MediatR;
+
+namespace Imprink.Application.Products.Handlers;
+
+public class GetProductsHandler(IUnitOfWork unitOfWork) : IRequestHandler>
+{
+ public async Task> Handle(GetProductsQuery request, CancellationToken cancellationToken)
+ {
+ var pagedResult = await unitOfWork.ProductRepository.GetPagedAsync(request.FilterParameters, cancellationToken);
+
+ var productDtos = pagedResult.Items.Select(p => new ProductDto
+ {
+ Id = p.Id,
+ Name = p.Name,
+ Description = p.Description,
+ BasePrice = p.BasePrice,
+ IsCustomizable = p.IsCustomizable,
+ IsActive = p.IsActive,
+ ImageUrl = p.ImageUrl,
+ CategoryId = p.CategoryId,
+ Category = new CategoryDto
+ {
+ Id = p.Category.Id,
+ Name = p.Category.Name,
+ Description = p.Category.Description,
+ ImageUrl = p.Category.ImageUrl,
+ SortOrder = p.Category.SortOrder,
+ IsActive = p.Category.IsActive,
+ ParentCategoryId = p.Category.ParentCategoryId,
+ CreatedAt = p.Category.CreatedAt,
+ ModifiedAt = p.Category.ModifiedAt
+ },
+ CreatedAt = p.CreatedAt,
+ ModifiedAt = p.ModifiedAt
+ });
+
+ return new PagedResultDto
+ {
+ Items = productDtos,
+ TotalCount = pagedResult.TotalCount,
+ PageNumber = pagedResult.PageNumber,
+ PageSize = pagedResult.PageSize
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Queries/GetCategoriesQuery.cs b/src/Imprink.Application/Products/Queries/GetCategoriesQuery.cs
new file mode 100644
index 0000000..dff7db6
--- /dev/null
+++ b/src/Imprink.Application/Products/Queries/GetCategoriesQuery.cs
@@ -0,0 +1,10 @@
+using Imprink.Application.Products.Dtos;
+using MediatR;
+
+namespace Imprink.Application.Products.Queries;
+
+public class GetCategoriesQuery : IRequest>
+{
+ public bool? IsActive { get; set; }
+ public bool RootCategoriesOnly { get; set; } = false;
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Queries/GetProductVariantsQuery.cs b/src/Imprink.Application/Products/Queries/GetProductVariantsQuery.cs
new file mode 100644
index 0000000..c150d32
--- /dev/null
+++ b/src/Imprink.Application/Products/Queries/GetProductVariantsQuery.cs
@@ -0,0 +1,11 @@
+using Imprink.Application.Products.Dtos;
+using MediatR;
+
+namespace Imprink.Application.Products.Queries;
+
+public class GetProductVariantsQuery : IRequest>
+{
+ public Guid? ProductId { get; set; }
+ public bool? IsActive { get; set; }
+ public bool InStockOnly { get; set; } = false;
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Products/Queries/GetProductsQuery.cs b/src/Imprink.Application/Products/Queries/GetProductsQuery.cs
new file mode 100644
index 0000000..7329b15
--- /dev/null
+++ b/src/Imprink.Application/Products/Queries/GetProductsQuery.cs
@@ -0,0 +1,10 @@
+using Imprink.Application.Products.Dtos;
+using Imprink.Domain.Common.Models;
+using MediatR;
+
+namespace Imprink.Application.Products.Queries;
+
+public class GetProductsQuery : IRequest>
+{
+ public ProductFilterParameters FilterParameters { get; set; } = new();
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Common/Models/PagedResult.cs b/src/Imprink.Domain/Common/Models/PagedResult.cs
new file mode 100644
index 0000000..cde7b5a
--- /dev/null
+++ b/src/Imprink.Domain/Common/Models/PagedResult.cs
@@ -0,0 +1,12 @@
+namespace Imprink.Domain.Common.Models;
+
+public class PagedResult
+{
+ public IEnumerable Items { get; set; } = new List();
+ public int TotalCount { get; set; }
+ public int PageNumber { get; set; }
+ public int PageSize { get; set; }
+ public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
+ public bool HasPreviousPage => PageNumber > 1;
+ public bool HasNextPage => PageNumber < TotalPages;
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Common/Models/ProductFilterParameters.cs b/src/Imprink.Domain/Common/Models/ProductFilterParameters.cs
new file mode 100644
index 0000000..4631894
--- /dev/null
+++ b/src/Imprink.Domain/Common/Models/ProductFilterParameters.cs
@@ -0,0 +1,15 @@
+namespace Imprink.Domain.Common.Models;
+
+public class ProductFilterParameters
+{
+ public int PageNumber { get; set; } = 1;
+ public int PageSize { get; set; } = 10;
+ public string? SearchTerm { get; set; }
+ public Guid? CategoryId { get; set; }
+ public decimal? MinPrice { get; set; }
+ public decimal? MaxPrice { get; set; }
+ public bool? IsActive { get; set; } = true;
+ public bool? IsCustomizable { get; set; }
+ public string SortBy { get; set; } = "Name";
+ public string SortDirection { get; set; } = "ASC";
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/EntityBase.cs b/src/Imprink.Domain/Entities/EntityBase.cs
new file mode 100644
index 0000000..8511eb2
--- /dev/null
+++ b/src/Imprink.Domain/Entities/EntityBase.cs
@@ -0,0 +1,10 @@
+namespace Imprink.Domain.Entities;
+
+public abstract class EntityBase
+{
+ public Guid Id { get; set; }
+ public DateTime? CreatedAt { get; set; }
+ public DateTime? ModifiedAt { get; set; }
+ public string? CreatedBy { get; set; }
+ public string? ModifiedBy { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Orders/Order.cs b/src/Imprink.Domain/Entities/Orders/Order.cs
new file mode 100644
index 0000000..b9b0c4c
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Orders/Order.cs
@@ -0,0 +1,17 @@
+namespace Imprink.Domain.Entities.Orders;
+
+public class Order : EntityBase
+{
+ public string UserId { get; set; } = null!;
+ public DateTime OrderDate { get; set; }
+ public decimal TotalPrice { get; set; }
+ public int OrderStatusId { get; set; }
+ public int ShippingStatusId { get; set; }
+ public string OrderNumber { get; set; } = null!;
+ public string Notes { get; set; } = null!;
+
+ public OrderStatus OrderStatus { get; set; } = null!;
+ public ShippingStatus ShippingStatus { get; set; } = null!;
+ public OrderAddress OrderAddress { get; set; } = null!;
+ public virtual ICollection OrderItems { get; set; } = new List();
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Orders/OrderAddress.cs b/src/Imprink.Domain/Entities/Orders/OrderAddress.cs
new file mode 100644
index 0000000..1eb44aa
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Orders/OrderAddress.cs
@@ -0,0 +1,13 @@
+namespace Imprink.Domain.Entities.Orders;
+
+public class OrderAddress : EntityBase
+{
+ public Guid OrderId { get; set; }
+ public required string Street { get; set; }
+ public required string City { get; set; }
+ public required string State { get; set; }
+ public required string PostalCode { get; set; }
+ public required string Country { get; set; }
+
+ public virtual required Order Order { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Orders/OrderItem.cs b/src/Imprink.Domain/Entities/Orders/OrderItem.cs
new file mode 100644
index 0000000..4013d51
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Orders/OrderItem.cs
@@ -0,0 +1,19 @@
+using Imprink.Domain.Entities.Product;
+
+namespace Imprink.Domain.Entities.Orders;
+
+public class OrderItem : EntityBase
+{
+ public Guid OrderId { get; set; }
+ public Guid ProductId { get; set; }
+ public Guid? ProductVariantId { get; set; }
+ public int Quantity { get; set; }
+ public decimal UnitPrice { get; set; }
+ public decimal TotalPrice { get; set; }
+ public string CustomizationImageUrl { get; set; } = null!;
+ public string CustomizationDescription { get; set; } = null!;
+
+ public Order Order { get; set; } = null!;
+ public Product.Product Product { get; set; } = null!;
+ public ProductVariant ProductVariant { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Orders/OrderStatus.cs b/src/Imprink.Domain/Entities/Orders/OrderStatus.cs
new file mode 100644
index 0000000..597a0a9
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Orders/OrderStatus.cs
@@ -0,0 +1,9 @@
+namespace Imprink.Domain.Entities.Orders;
+
+public class OrderStatus
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = null!;
+
+ public virtual ICollection Orders { get; set; } = new List();
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Orders/ShippingStatus.cs b/src/Imprink.Domain/Entities/Orders/ShippingStatus.cs
new file mode 100644
index 0000000..eb07c63
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Orders/ShippingStatus.cs
@@ -0,0 +1,9 @@
+namespace Imprink.Domain.Entities.Orders;
+
+public class ShippingStatus
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = null!;
+
+ public virtual ICollection Orders { get; set; } = new List();
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Product/Category.cs b/src/Imprink.Domain/Entities/Product/Category.cs
new file mode 100644
index 0000000..3bbf53c
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Product/Category.cs
@@ -0,0 +1,15 @@
+namespace Imprink.Domain.Entities.Product;
+
+public class Category : EntityBase
+{
+ public string Name { get; set; } = null!;
+ public string Description { get; set; } = null!;
+ public string? ImageUrl { get; set; }
+ public int SortOrder { get; set; }
+ public bool IsActive { get; set; }
+ public Guid? ParentCategoryId { get; set; }
+
+ public virtual Category? ParentCategory { get; set; }
+ public virtual ICollection SubCategories { get; set; } = new List();
+ public virtual ICollection Products { get; set; } = new List();
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Product/Product.cs b/src/Imprink.Domain/Entities/Product/Product.cs
new file mode 100644
index 0000000..d728d5b
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Product/Product.cs
@@ -0,0 +1,18 @@
+using Imprink.Domain.Entities.Orders;
+
+namespace Imprink.Domain.Entities.Product;
+
+public class Product : EntityBase
+{
+ public required string Name { get; set; }
+ public string? Description { get; set; }
+ public required decimal BasePrice { get; set; }
+ public required bool IsCustomizable { get; set; }
+ public required bool IsActive { get; set; }
+ public string? ImageUrl { get; set; }
+ public Guid? CategoryId { get; set; }
+
+ public virtual required Category Category { get; set; }
+ public virtual ICollection ProductVariants { get; set; } = new List();
+ public virtual ICollection OrderItems { get; set; } = new List();
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Product/ProductVariant.cs b/src/Imprink.Domain/Entities/Product/ProductVariant.cs
new file mode 100644
index 0000000..5dd781e
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Product/ProductVariant.cs
@@ -0,0 +1,18 @@
+using Imprink.Domain.Entities.Orders;
+
+namespace Imprink.Domain.Entities.Product;
+
+public class ProductVariant : EntityBase
+{
+ public required Guid ProductId { get; set; }
+ public required string Size { get; set; }
+ public string? Color { get; set; }
+ public required decimal Price { get; set; }
+ public string? ImageUrl { get; set; }
+ public required string Sku { get; set; }
+ public int StockQuantity { get; set; }
+ public bool IsActive { get; set; }
+
+ public virtual required Imprink.Domain.Entities.Product.Product Product { get; set; }
+ public virtual ICollection OrderItems { get; set; } = new List();
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Users/Address.cs b/src/Imprink.Domain/Entities/Users/Address.cs
new file mode 100644
index 0000000..144b8c0
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Users/Address.cs
@@ -0,0 +1,14 @@
+namespace Imprink.Domain.Entities.Users;
+
+public class Address : EntityBase
+{
+ public required string UserId { get; set; }
+ public required string AddressType { get; set; }
+ public required string Street { get; set; }
+ public required string City { get; set; }
+ public required string State { get; set; }
+ public required string PostalCode { get; set; }
+ public required string Country { get; set; }
+ public required bool IsDefault { get; set; }
+ public required bool IsActive { get; set; }
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Users/ApplicationRole.cs b/src/Imprink.Domain/Entities/Users/ApplicationRole.cs
new file mode 100644
index 0000000..dea6dc5
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Users/ApplicationRole.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace Imprink.Domain.Entities.Users;
+
+public class ApplicationRole : IdentityRole
+{
+ public string Description { get; set; } = null!;
+ public DateTime CreatedAt { get; set; }
+ public bool IsActive { get; set; }
+
+ public ApplicationRole()
+ {}
+
+ public ApplicationRole(string roleName) : base(roleName)
+ {}
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Entities/Users/ApplicationUser.cs b/src/Imprink.Domain/Entities/Users/ApplicationUser.cs
new file mode 100644
index 0000000..a0576f5
--- /dev/null
+++ b/src/Imprink.Domain/Entities/Users/ApplicationUser.cs
@@ -0,0 +1,18 @@
+using Imprink.Domain.Entities.Orders;
+using Microsoft.AspNetCore.Identity;
+
+namespace Imprink.Domain.Entities.Users;
+
+public sealed class ApplicationUser : IdentityUser
+{
+ public required string FirstName { get; set; }
+ public required string LastName { get; set; }
+ public DateTime? DateOfBirth { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime LastLoginAt { get; set; }
+ public bool IsActive { get; set; }
+ public string? ProfileImageUrl { get; set; }
+
+ public ICollection Addresses { get; set; } = new List();
+ public ICollection Orders { get; set; } = new List();
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Imprink.Domain.csproj b/src/Imprink.Domain/Imprink.Domain.csproj
new file mode 100644
index 0000000..da1134f
--- /dev/null
+++ b/src/Imprink.Domain/Imprink.Domain.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Imprink.Domain/Repositories/ICategoryRepository.cs b/src/Imprink.Domain/Repositories/ICategoryRepository.cs
new file mode 100644
index 0000000..ef79aad
--- /dev/null
+++ b/src/Imprink.Domain/Repositories/ICategoryRepository.cs
@@ -0,0 +1,20 @@
+using Imprink.Domain.Entities.Product;
+
+namespace Imprink.Domain.Repositories;
+
+public interface ICategoryRepository
+{
+ Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
+ Task GetByIdWithSubCategoriesAsync(Guid id, CancellationToken cancellationToken = default);
+ Task GetByIdWithProductsAsync(Guid id, CancellationToken cancellationToken = default);
+ Task> GetAllAsync(CancellationToken cancellationToken = default);
+ Task> GetActiveAsync(CancellationToken cancellationToken = default);
+ Task> GetRootCategoriesAsync(CancellationToken cancellationToken = default);
+ Task> GetSubCategoriesAsync(Guid parentCategoryId, CancellationToken cancellationToken = default);
+ Task AddAsync(Category category, CancellationToken cancellationToken = default);
+ Task UpdateAsync(Category category, CancellationToken cancellationToken = default);
+ Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
+ Task ExistsAsync(Guid id, CancellationToken cancellationToken = default);
+ Task HasSubCategoriesAsync(Guid categoryId, CancellationToken cancellationToken = default);
+ Task HasProductsAsync(Guid categoryId, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Repositories/IProductRepository.cs b/src/Imprink.Domain/Repositories/IProductRepository.cs
new file mode 100644
index 0000000..38eaee9
--- /dev/null
+++ b/src/Imprink.Domain/Repositories/IProductRepository.cs
@@ -0,0 +1,19 @@
+using Imprink.Domain.Common.Models;
+using Imprink.Domain.Entities.Product;
+
+namespace Imprink.Domain.Repositories;
+
+public interface IProductRepository
+{
+ Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
+ Task GetByIdWithVariantsAsync(Guid id, CancellationToken cancellationToken = default);
+ Task GetByIdWithCategoryAsync(Guid id, CancellationToken cancellationToken = default);
+ Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default);
+ Task> GetPagedAsync(ProductFilterParameters filterParameters, CancellationToken cancellationToken = default);
+ Task> GetByCategoryAsync(Guid categoryId, CancellationToken cancellationToken = default);
+ Task> GetCustomizableAsync(CancellationToken cancellationToken = default);
+ Task AddAsync(Product product, CancellationToken cancellationToken = default);
+ Task UpdateAsync(Product product, CancellationToken cancellationToken = default);
+ Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
+ Task ExistsAsync(Guid id, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Repositories/IProductVariantRepository.cs b/src/Imprink.Domain/Repositories/IProductVariantRepository.cs
new file mode 100644
index 0000000..8b98c45
--- /dev/null
+++ b/src/Imprink.Domain/Repositories/IProductVariantRepository.cs
@@ -0,0 +1,19 @@
+using Imprink.Domain.Entities.Product;
+
+namespace Imprink.Domain.Repositories;
+
+public interface IProductVariantRepository
+{
+ Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
+ Task GetByIdWithProductAsync(Guid id, CancellationToken cancellationToken = default);
+ Task GetBySkuAsync(string sku, CancellationToken cancellationToken = default);
+ Task> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default);
+ Task> GetActiveByProductIdAsync(Guid productId, CancellationToken cancellationToken = default);
+ Task> GetInStockByProductIdAsync(Guid productId, CancellationToken cancellationToken = default);
+ Task AddAsync(ProductVariant productVariant, CancellationToken cancellationToken = default);
+ Task UpdateAsync(ProductVariant productVariant, CancellationToken cancellationToken = default);
+ Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
+ Task ExistsAsync(Guid id, CancellationToken cancellationToken = default);
+ Task SkuExistsAsync(string sku, Guid? excludeId = null, CancellationToken cancellationToken = default);
+ Task UpdateStockQuantityAsync(Guid id, int quantity, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/EntityBaseConfiguration.cs b/src/Imprink.Infrastructure/Configuration/EntityBaseConfiguration.cs
new file mode 100644
index 0000000..6315200
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/EntityBaseConfiguration.cs
@@ -0,0 +1,40 @@
+using Imprink.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration;
+
+public class EntityBaseConfiguration : IEntityTypeConfiguration where T : EntityBase
+{
+ public virtual void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(e => e.Id);
+
+ builder.Property(e => e.Id)
+ .HasDefaultValueSql("NEWID()");
+
+ builder.Property(e => e.CreatedAt)
+ .IsRequired();
+
+ builder.Property(e => e.ModifiedAt)
+ .IsRequired()
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ builder.Property(e => e.CreatedBy)
+ .IsRequired()
+ .HasMaxLength(450);
+
+ builder.Property(e => e.ModifiedBy)
+ .IsRequired()
+ .HasMaxLength(450);
+
+ builder.HasIndex(e => e.CreatedAt)
+ .HasDatabaseName($"IX_{typeof(T).Name}_CreatedAt");
+
+ builder.HasIndex(e => e.ModifiedAt)
+ .HasDatabaseName($"IX_{typeof(T).Name}_ModifiedAt");
+
+ builder.HasIndex(e => e.CreatedBy)
+ .HasDatabaseName($"IX_{typeof(T).Name}_CreatedBy");
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Orders/OrderAddressConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Orders/OrderAddressConfiguration.cs
new file mode 100644
index 0000000..180f7fe
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Orders/OrderAddressConfiguration.cs
@@ -0,0 +1,39 @@
+using Imprink.Domain.Entities.Orders;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Orders;
+
+public class OrderAddressConfiguration : EntityBaseConfiguration
+{
+ public override void Configure(EntityTypeBuilder builder)
+ {
+ base.Configure(builder);
+
+ builder.Property(oa => oa.OrderId)
+ .IsRequired();
+
+ builder.Property(oa => oa.Street)
+ .IsRequired()
+ .HasMaxLength(200);
+
+ builder.Property(oa => oa.City)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ builder.Property(oa => oa.State)
+ .HasMaxLength(100);
+
+ builder.Property(oa => oa.PostalCode)
+ .IsRequired()
+ .HasMaxLength(20);
+
+ builder.Property(oa => oa.Country)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ builder.HasIndex(oa => oa.OrderId)
+ .IsUnique()
+ .HasDatabaseName("IX_OrderAddress_OrderId");
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Orders/OrderConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Orders/OrderConfiguration.cs
new file mode 100644
index 0000000..6c61d53
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Orders/OrderConfiguration.cs
@@ -0,0 +1,77 @@
+using Imprink.Domain.Entities.Orders;
+using Imprink.Domain.Entities.Users;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Orders;
+
+public class OrderConfiguration : EntityBaseConfiguration
+ {
+ public override void Configure(EntityTypeBuilder builder)
+ {
+ base.Configure(builder);
+
+ builder.Property(o => o.UserId)
+ .IsRequired()
+ .HasMaxLength(450);
+
+ builder.Property(o => o.OrderDate)
+ .IsRequired();
+
+ builder.Property(o => o.TotalPrice)
+ .IsRequired()
+ .HasColumnType("decimal(18,2)");
+
+ builder.Property(o => o.OrderStatusId)
+ .IsRequired();
+
+ builder.Property(o => o.ShippingStatusId)
+ .IsRequired();
+
+ builder.Property(o => o.OrderNumber)
+ .IsRequired()
+ .HasMaxLength(50);
+
+ builder.Property(o => o.Notes)
+ .HasMaxLength(1000);
+
+ builder.HasOne()
+ .WithMany(u => u.Orders)
+ .HasForeignKey(o => o.UserId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasOne(o => o.OrderStatus)
+ .WithMany(os => os.Orders)
+ .HasForeignKey(o => o.OrderStatusId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasOne(o => o.ShippingStatus)
+ .WithMany(ss => ss.Orders)
+ .HasForeignKey(o => o.ShippingStatusId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasOne(o => o.OrderAddress)
+ .WithOne(oa => oa.Order)
+ .HasForeignKey(oa => oa.OrderId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasIndex(o => o.UserId)
+ .HasDatabaseName("IX_Order_UserId");
+
+ builder.HasIndex(o => o.OrderNumber)
+ .IsUnique()
+ .HasDatabaseName("IX_Order_OrderNumber");
+
+ builder.HasIndex(o => o.OrderDate)
+ .HasDatabaseName("IX_Order_OrderDate");
+
+ builder.HasIndex(o => o.OrderStatusId)
+ .HasDatabaseName("IX_Order_OrderStatusId");
+
+ builder.HasIndex(o => o.ShippingStatusId)
+ .HasDatabaseName("IX_Order_ShippingStatusId");
+
+ builder.HasIndex(o => new { o.UserId, o.OrderDate })
+ .HasDatabaseName("IX_Order_User_Date");
+ }
+ }
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Orders/OrderItemConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Orders/OrderItemConfiguration.cs
new file mode 100644
index 0000000..7f3e16d
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Orders/OrderItemConfiguration.cs
@@ -0,0 +1,64 @@
+using Imprink.Domain.Entities.Orders;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Orders;
+
+public class OrderItemConfiguration : EntityBaseConfiguration
+ {
+ public override void Configure(EntityTypeBuilder builder)
+ {
+ base.Configure(builder);
+
+ builder.Property(oi => oi.OrderId)
+ .IsRequired();
+
+ builder.Property(oi => oi.ProductId)
+ .IsRequired();
+
+ builder.Property(oi => oi.Quantity)
+ .IsRequired()
+ .HasDefaultValue(1);
+
+ builder.Property(oi => oi.UnitPrice)
+ .IsRequired()
+ .HasColumnType("decimal(18,2)");
+
+ builder.Property(oi => oi.TotalPrice)
+ .IsRequired()
+ .HasColumnType("decimal(18,2)");
+
+ builder.Property(oi => oi.CustomizationImageUrl)
+ .HasMaxLength(500);
+
+ builder.Property(oi => oi.CustomizationDescription)
+ .HasMaxLength(2000);
+
+ builder.HasOne(oi => oi.Order)
+ .WithMany(o => o.OrderItems)
+ .HasForeignKey(oi => oi.OrderId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(oi => oi.Product)
+ .WithMany(p => p.OrderItems)
+ .HasForeignKey(oi => oi.ProductId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasOne(oi => oi.ProductVariant)
+ .WithMany(pv => pv.OrderItems)
+ .HasForeignKey(oi => oi.ProductVariantId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasIndex(oi => oi.OrderId)
+ .HasDatabaseName("IX_OrderItem_OrderId");
+
+ builder.HasIndex(oi => oi.ProductId)
+ .HasDatabaseName("IX_OrderItem_ProductId");
+
+ builder.HasIndex(oi => oi.ProductVariantId)
+ .HasDatabaseName("IX_OrderItem_ProductVariantId");
+
+ builder.HasIndex(oi => new { oi.OrderId, oi.ProductId })
+ .HasDatabaseName("IX_OrderItem_Order_Product");
+ }
+ }
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Orders/OrderStatusConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Orders/OrderStatusConfiguration.cs
new file mode 100644
index 0000000..4cbc230
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Orders/OrderStatusConfiguration.cs
@@ -0,0 +1,31 @@
+using Imprink.Domain.Entities.Orders;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Orders;
+
+public class OrderStatusConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(os => os.Id);
+
+ builder.Property(os => os.Id)
+ .ValueGeneratedNever();
+
+ builder.Property(os => os.Name)
+ .IsRequired()
+ .HasMaxLength(50);
+
+ builder.HasIndex(os => os.Name)
+ .IsUnique()
+ .HasDatabaseName("IX_OrderStatus_Name");
+
+ builder.HasData(
+ new OrderStatus { Id = 0, Name = "Pending" },
+ new OrderStatus { Id = 1, Name = "Processing" },
+ new OrderStatus { Id = 2, Name = "Completed" },
+ new OrderStatus { Id = 3, Name = "Cancelled" }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Orders/ShippingStatusConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Orders/ShippingStatusConfiguration.cs
new file mode 100644
index 0000000..af119df
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Orders/ShippingStatusConfiguration.cs
@@ -0,0 +1,31 @@
+using Imprink.Domain.Entities.Orders;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Orders;
+
+public class ShippingStatusConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(ss => ss.Id);
+
+ builder.Property(ss => ss.Id)
+ .ValueGeneratedNever();
+
+ builder.Property(ss => ss.Name)
+ .IsRequired()
+ .HasMaxLength(50);
+
+ builder.HasIndex(ss => ss.Name)
+ .IsUnique()
+ .HasDatabaseName("IX_ShippingStatus_Name");
+
+ builder.HasData(
+ new ShippingStatus { Id = 0, Name = "Prepping" },
+ new ShippingStatus { Id = 1, Name = "Packaging" },
+ new ShippingStatus { Id = 2, Name = "Shipped" },
+ new ShippingStatus { Id = 3, Name = "Delivered" }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Products/CategoryConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Products/CategoryConfiguration.cs
new file mode 100644
index 0000000..0867cb7
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Products/CategoryConfiguration.cs
@@ -0,0 +1,105 @@
+using Imprink.Domain.Entities.Product;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Products;
+
+public class CategoryConfiguration : EntityBaseConfiguration
+{
+ public override void Configure(EntityTypeBuilder builder)
+ {
+ base.Configure(builder);
+
+ builder.Property(c => c.Name)
+ .IsRequired()
+ .HasMaxLength(200);
+
+ builder.Property(c => c.Description)
+ .HasMaxLength(2000);
+
+ builder.Property(c => c.ImageUrl)
+ .HasMaxLength(500);
+
+ builder.Property(c => c.SortOrder)
+ .IsRequired()
+ .HasDefaultValue(0);
+
+ builder.Property(c => c.IsActive)
+ .IsRequired()
+ .HasDefaultValue(true);
+
+ builder.Property(c => c.ParentCategoryId)
+ .IsRequired(false);
+
+ builder.Property(c => c.CreatedAt)
+ .IsRequired(false);
+
+ builder.Property(c => c.CreatedBy)
+ .IsRequired(false);
+
+ builder.Property(c => c.ModifiedAt)
+ .IsRequired(false);
+
+ builder.Property(c => c.ModifiedBy)
+ .IsRequired(false);
+
+ builder.HasOne(c => c.ParentCategory)
+ .WithMany(c => c.SubCategories)
+ .HasForeignKey(c => c.ParentCategoryId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasIndex(c => c.Name)
+ .HasDatabaseName("IX_Category_Name");
+
+ builder.HasIndex(c => c.IsActive)
+ .HasDatabaseName("IX_Category_IsActive");
+
+ builder.HasIndex(c => c.ParentCategoryId)
+ .HasDatabaseName("IX_Category_ParentCategoryId");
+
+ builder.HasIndex(c => new { c.ParentCategoryId, c.SortOrder })
+ .HasDatabaseName("IX_Category_Parent_SortOrder");
+
+ builder.HasIndex(c => new { c.IsActive, c.SortOrder })
+ .HasDatabaseName("IX_Category_Active_SortOrder");
+
+ builder.HasData(
+ new Category
+ {
+ Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
+ Name = "Textile",
+ Description = "Textile and fabric-based products",
+ IsActive = true,
+ SortOrder = 1,
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ ModifiedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ CreatedBy = "system@printbase.com",
+ ModifiedBy = "system@printbase.com"
+ },
+ new Category
+ {
+ Id = Guid.Parse("22222222-2222-2222-2222-222222222222"),
+ Name = "Hard Surfaces",
+ Description = "Products for hard surface printing",
+ IsActive = true,
+ SortOrder = 2,
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ ModifiedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ CreatedBy = "system@printbase.com",
+ ModifiedBy = "system@printbase.com"
+ },
+ new Category
+ {
+ Id = Guid.Parse("33333333-3333-3333-3333-333333333333"),
+ Name = "Paper",
+ Description = "Paper-based printing products",
+ IsActive = true,
+ SortOrder = 3,
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ ModifiedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ CreatedBy = "system@printbase.com",
+ ModifiedBy = "system@printbase.com"
+ }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Products/ProductConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Products/ProductConfiguration.cs
new file mode 100644
index 0000000..ed3054e
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Products/ProductConfiguration.cs
@@ -0,0 +1,73 @@
+using Imprink.Domain.Entities.Product;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Products;
+
+public class ProductConfiguration : EntityBaseConfiguration
+{
+ public override void Configure(EntityTypeBuilder builder)
+ {
+ base.Configure(builder);
+
+ builder.Property(p => p.Name)
+ .IsRequired()
+ .HasMaxLength(200);
+
+ builder.Property(p => p.Description)
+ .HasMaxLength(2000);
+
+ builder.Property(p => p.BasePrice)
+ .IsRequired()
+ .HasColumnType("decimal(18,2)");
+
+ builder.Property(p => p.IsCustomizable)
+ .IsRequired()
+ .HasDefaultValue(false);
+
+ builder.Property(p => p.IsActive)
+ .IsRequired()
+ .HasDefaultValue(true);
+
+ builder.Property(p => p.ImageUrl)
+ .HasMaxLength(500);
+
+ builder.Property(p => p.CategoryId)
+ .IsRequired(false);
+
+ builder.Property(c => c.CreatedAt)
+ .IsRequired(false);
+
+ builder.Property(c => c.CreatedBy)
+ .IsRequired(false);
+
+ builder.Property(c => c.ModifiedAt)
+ .IsRequired(false);
+
+ builder.Property(c => c.ModifiedBy)
+ .IsRequired(false);
+
+ builder.HasOne(p => p.Category)
+ .WithMany(c => c.Products)
+ .HasForeignKey(p => p.CategoryId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ builder.HasIndex(p => p.Name)
+ .HasDatabaseName("IX_Product_Name");
+
+ builder.HasIndex(p => p.IsActive)
+ .HasDatabaseName("IX_Product_IsActive");
+
+ builder.HasIndex(p => p.IsCustomizable)
+ .HasDatabaseName("IX_Product_IsCustomizable");
+
+ builder.HasIndex(p => p.CategoryId)
+ .HasDatabaseName("IX_Product_CategoryId");
+
+ builder.HasIndex(p => new { p.IsActive, p.IsCustomizable })
+ .HasDatabaseName("IX_Product_Active_Customizable");
+
+ builder.HasIndex(p => new { p.CategoryId, p.IsActive })
+ .HasDatabaseName("IX_Product_Category_Active");
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Products/ProductVariantConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Products/ProductVariantConfiguration.cs
new file mode 100644
index 0000000..7593b6b
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Products/ProductVariantConfiguration.cs
@@ -0,0 +1,72 @@
+using Imprink.Domain.Entities.Product;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Products;
+
+public class ProductVariantConfiguration : EntityBaseConfiguration
+{
+ public override void Configure(EntityTypeBuilder builder)
+ {
+ base.Configure(builder);
+
+ builder.Property(pv => pv.ProductId)
+ .IsRequired();
+
+ builder.Property(pv => pv.Size)
+ .HasMaxLength(50);
+
+ builder.Property(pv => pv.Color)
+ .HasMaxLength(50);
+
+ builder.Property(pv => pv.Price)
+ .IsRequired()
+ .HasColumnType("decimal(18,2)");
+
+ builder.Property(pv => pv.ImageUrl)
+ .HasMaxLength(500);
+
+ builder.Property(pv => pv.Sku)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ builder.Property(pv => pv.StockQuantity)
+ .IsRequired()
+ .HasDefaultValue(0);
+
+ builder.Property(pv => pv.IsActive)
+ .IsRequired()
+ .HasDefaultValue(true);
+
+ builder.Property(c => c.CreatedAt)
+ .IsRequired(false);
+
+ builder.Property(c => c.CreatedBy)
+ .IsRequired(false);
+
+ builder.Property(c => c.ModifiedAt)
+ .IsRequired(false);
+
+ builder.Property(c => c.ModifiedBy)
+ .IsRequired(false);
+
+ builder.HasOne(pv => pv.Product)
+ .WithMany(p => p.ProductVariants)
+ .HasForeignKey(pv => pv.ProductId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasIndex(pv => pv.ProductId)
+ .HasDatabaseName("IX_ProductVariant_ProductId");
+
+ builder.HasIndex(pv => pv.Sku)
+ .IsUnique()
+ .HasDatabaseName("IX_ProductVariant_SKU");
+
+ builder.HasIndex(pv => pv.IsActive)
+ .HasDatabaseName("IX_ProductVariant_IsActive");
+
+ builder.HasIndex(pv => new { pv.ProductId, pv.Size, pv.Color })
+ .IsUnique()
+ .HasDatabaseName("IX_ProductVariant_Product_Size_Color");
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Users/AddressConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Users/AddressConfiguration.cs
new file mode 100644
index 0000000..d47d168
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Users/AddressConfiguration.cs
@@ -0,0 +1,62 @@
+using Imprink.Domain.Entities.Users;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Users;
+
+public class AddressConfiguration : EntityBaseConfiguration
+{
+ public override void Configure(EntityTypeBuilder builder)
+ {
+ base.Configure(builder);
+
+ builder.Property(a => a.UserId)
+ .IsRequired()
+ .HasMaxLength(450);
+
+ builder.Property(a => a.AddressType)
+ .IsRequired()
+ .HasMaxLength(50);
+
+ builder.Property(a => a.Street)
+ .IsRequired()
+ .HasMaxLength(200);
+
+ builder.Property(a => a.City)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ builder.Property(a => a.State)
+ .HasMaxLength(100);
+
+ builder.Property(a => a.PostalCode)
+ .IsRequired()
+ .HasMaxLength(20);
+
+ builder.Property(a => a.Country)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ builder.Property(a => a.IsDefault)
+ .IsRequired()
+ .HasDefaultValue(false);
+
+ builder.Property(a => a.IsActive)
+ .IsRequired()
+ .HasDefaultValue(true);
+
+ builder.HasOne()
+ .WithMany(u => u.Addresses)
+ .HasForeignKey(a => a.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasIndex(a => a.UserId)
+ .HasDatabaseName("IX_Address_UserId");
+
+ builder.HasIndex(a => new { a.UserId, a.AddressType })
+ .HasDatabaseName("IX_Address_User_Type");
+
+ builder.HasIndex(a => new { a.UserId, a.IsDefault })
+ .HasDatabaseName("IX_Address_User_Default");
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Users/ApplicationRoleConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Users/ApplicationRoleConfiguration.cs
new file mode 100644
index 0000000..313ccf0
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Users/ApplicationRoleConfiguration.cs
@@ -0,0 +1,66 @@
+using Imprink.Domain.Entities.Users;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Users;
+
+public class ApplicationRoleConfiguration : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.Property(r => r.Description)
+ .HasMaxLength(500);
+
+ builder.Property(r => r.CreatedAt)
+ .IsRequired()
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ builder.Property(r => r.IsActive)
+ .IsRequired()
+ .HasDefaultValue(true);
+
+ builder.HasIndex(r => r.IsActive)
+ .HasDatabaseName("IX_ApplicationRole_IsActive");
+
+ var seedDate = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ builder.HasData(
+ new ApplicationRole
+ {
+ Id = "1",
+ Name = "Administrator",
+ NormalizedName = "ADMINISTRATOR",
+ Description = "Full system access",
+ CreatedAt = seedDate,
+ IsActive = true
+ },
+ new ApplicationRole
+ {
+ Id = "2",
+ Name = "Customer",
+ NormalizedName = "CUSTOMER",
+ Description = "Standard customer access",
+ CreatedAt = seedDate,
+ IsActive = true
+ },
+ new ApplicationRole
+ {
+ Id = "3",
+ Name = "OrderManager",
+ NormalizedName = "ORDERMANAGER",
+ Description = "Manage orders and fulfillment",
+ CreatedAt = seedDate,
+ IsActive = true
+ },
+ new ApplicationRole
+ {
+ Id = "4",
+ Name = "ProductManager",
+ NormalizedName = "PRODUCTMANAGER",
+ Description = "Manage products and inventory",
+ CreatedAt = seedDate,
+ IsActive = true
+ }
+ );
+ }
+ }
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Configuration/Users/ApplicationUserConfiguration.cs b/src/Imprink.Infrastructure/Configuration/Users/ApplicationUserConfiguration.cs
new file mode 100644
index 0000000..423e1fb
--- /dev/null
+++ b/src/Imprink.Infrastructure/Configuration/Users/ApplicationUserConfiguration.cs
@@ -0,0 +1,30 @@
+using Imprink.Domain.Entities.Users;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Imprink.Infrastructure.Configuration.Users;
+
+public class ApplicationUserConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.Property(u => u.FirstName)
+ .HasMaxLength(100);
+
+ builder.Property(u => u.LastName)
+ .HasMaxLength(100);
+
+ builder.Property(u => u.ProfileImageUrl)
+ .HasMaxLength(500);
+
+ builder.Property(u => u.CreatedAt)
+ .IsRequired()
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ builder.Property(u => u.LastLoginAt)
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ builder.Property(u => u.IsActive)
+ .HasDefaultValue(true);
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Database/ApplicationDbContext.cs b/src/Imprink.Infrastructure/Database/ApplicationDbContext.cs
new file mode 100644
index 0000000..c92304e
--- /dev/null
+++ b/src/Imprink.Infrastructure/Database/ApplicationDbContext.cs
@@ -0,0 +1,42 @@
+using Imprink.Domain.Entities.Orders;
+using Imprink.Domain.Entities.Product;
+using Imprink.Domain.Entities.Users;
+using Imprink.Infrastructure.Configuration.Orders;
+using Imprink.Infrastructure.Configuration.Products;
+using Imprink.Infrastructure.Configuration.Users;
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+
+namespace Imprink.Infrastructure.Database;
+
+public class ApplicationDbContext(DbContextOptions options)
+ : IdentityDbContext(options)
+{
+ public DbSet Products { get; set; }
+ public DbSet ProductVariants { get; set; }
+ public DbSet Orders { get; set; }
+ public DbSet OrderItems { get; set; }
+ public DbSet OrderAddresses { get; set; }
+ public DbSet Addresses { get; set; }
+ public DbSet OrderStatuses { get; set; }
+ public DbSet ShippingStatuses { get; set; }
+ public DbSet Categories { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ modelBuilder.ApplyConfiguration(new ApplicationUserConfiguration());
+ modelBuilder.ApplyConfiguration(new ApplicationRoleConfiguration());
+
+ modelBuilder.ApplyConfiguration(new ProductConfiguration());
+ modelBuilder.ApplyConfiguration(new ProductVariantConfiguration());
+ modelBuilder.ApplyConfiguration(new OrderConfiguration());
+ modelBuilder.ApplyConfiguration(new OrderItemConfiguration());
+ modelBuilder.ApplyConfiguration(new OrderAddressConfiguration());
+ modelBuilder.ApplyConfiguration(new AddressConfiguration());
+ modelBuilder.ApplyConfiguration(new OrderStatusConfiguration());
+ modelBuilder.ApplyConfiguration(new ShippingStatusConfiguration());
+ modelBuilder.ApplyConfiguration(new CategoryConfiguration());
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj b/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj
new file mode 100644
index 0000000..fc58356
--- /dev/null
+++ b/src/Imprink.Infrastructure/Imprink.Infrastructure.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Imprink.Infrastructure/Migrations/20250527103204_InitialSetup.Designer.cs b/src/Imprink.Infrastructure/Migrations/20250527103204_InitialSetup.Designer.cs
new file mode 100644
index 0000000..b04c217
--- /dev/null
+++ b/src/Imprink.Infrastructure/Migrations/20250527103204_InitialSetup.Designer.cs
@@ -0,0 +1,1212 @@
+//
+using System;
+using Imprink.Infrastructure.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Printbase.Infrastructure.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20250527103204_InitialSetup")]
+ partial class InitialSetup
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Orders.Order", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWID()");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ModifiedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property("ModifiedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Notes")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("OrderDate")
+ .HasColumnType("datetime2");
+
+ b.Property("OrderNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("OrderStatusId")
+ .HasColumnType("int");
+
+ b.Property("ShippingStatusId")
+ .HasColumnType("int");
+
+ b.Property("TotalPrice")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt")
+ .HasDatabaseName("IX_Order_CreatedAt");
+
+ b.HasIndex("CreatedBy")
+ .HasDatabaseName("IX_Order_CreatedBy");
+
+ b.HasIndex("ModifiedAt")
+ .HasDatabaseName("IX_Order_ModifiedAt");
+
+ b.HasIndex("OrderDate")
+ .HasDatabaseName("IX_Order_OrderDate");
+
+ b.HasIndex("OrderNumber")
+ .IsUnique()
+ .HasDatabaseName("IX_Order_OrderNumber");
+
+ b.HasIndex("OrderStatusId")
+ .HasDatabaseName("IX_Order_OrderStatusId");
+
+ b.HasIndex("ShippingStatusId")
+ .HasDatabaseName("IX_Order_ShippingStatusId");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("IX_Order_UserId");
+
+ b.HasIndex("UserId", "OrderDate")
+ .HasDatabaseName("IX_Order_User_Date");
+
+ b.ToTable("Orders");
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderAddress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWID()");
+
+ b.Property("City")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Country")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ModifiedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property("ModifiedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("OrderId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PostalCode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("State")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Street")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt")
+ .HasDatabaseName("IX_OrderAddress_CreatedAt");
+
+ b.HasIndex("CreatedBy")
+ .HasDatabaseName("IX_OrderAddress_CreatedBy");
+
+ b.HasIndex("ModifiedAt")
+ .HasDatabaseName("IX_OrderAddress_ModifiedAt");
+
+ b.HasIndex("OrderId")
+ .IsUnique()
+ .HasDatabaseName("IX_OrderAddress_OrderId");
+
+ b.ToTable("OrderAddresses");
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWID()");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("CustomizationDescription")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("CustomizationImageUrl")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("ModifiedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property("ModifiedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("OrderId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ProductId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ProductVariantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Quantity")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(1);
+
+ b.Property("TotalPrice")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("UnitPrice")
+ .HasColumnType("decimal(18,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt")
+ .HasDatabaseName("IX_OrderItem_CreatedAt");
+
+ b.HasIndex("CreatedBy")
+ .HasDatabaseName("IX_OrderItem_CreatedBy");
+
+ b.HasIndex("ModifiedAt")
+ .HasDatabaseName("IX_OrderItem_ModifiedAt");
+
+ b.HasIndex("OrderId")
+ .HasDatabaseName("IX_OrderItem_OrderId");
+
+ b.HasIndex("ProductId")
+ .HasDatabaseName("IX_OrderItem_ProductId");
+
+ b.HasIndex("ProductVariantId")
+ .HasDatabaseName("IX_OrderItem_ProductVariantId");
+
+ b.HasIndex("OrderId", "ProductId")
+ .HasDatabaseName("IX_OrderItem_Order_Product");
+
+ b.ToTable("OrderItems");
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderStatus", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique()
+ .HasDatabaseName("IX_OrderStatus_Name");
+
+ b.ToTable("OrderStatuses");
+
+ b.HasData(
+ new
+ {
+ Id = 0,
+ Name = "Pending"
+ },
+ new
+ {
+ Id = 1,
+ Name = "Processing"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Completed"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Cancelled"
+ });
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Orders.ShippingStatus", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique()
+ .HasDatabaseName("IX_ShippingStatus_Name");
+
+ b.ToTable("ShippingStatuses");
+
+ b.HasData(
+ new
+ {
+ Id = 0,
+ Name = "Prepping"
+ },
+ new
+ {
+ Id = 1,
+ Name = "Packaging"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Shipped"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Delivered"
+ });
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Product.Category", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWID()");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("ImageUrl")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.Property("ModifiedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property("ModifiedBy")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("ParentCategoryId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("SortOrder")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt")
+ .HasDatabaseName("IX_Category_CreatedAt");
+
+ b.HasIndex("CreatedBy")
+ .HasDatabaseName("IX_Category_CreatedBy");
+
+ b.HasIndex("IsActive")
+ .HasDatabaseName("IX_Category_IsActive");
+
+ b.HasIndex("ModifiedAt")
+ .HasDatabaseName("IX_Category_ModifiedAt");
+
+ b.HasIndex("Name")
+ .HasDatabaseName("IX_Category_Name");
+
+ b.HasIndex("ParentCategoryId")
+ .HasDatabaseName("IX_Category_ParentCategoryId");
+
+ b.HasIndex("IsActive", "SortOrder")
+ .HasDatabaseName("IX_Category_Active_SortOrder");
+
+ b.HasIndex("ParentCategoryId", "SortOrder")
+ .HasDatabaseName("IX_Category_Parent_SortOrder");
+
+ b.ToTable("Categories");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("11111111-1111-1111-1111-111111111111"),
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ CreatedBy = "system@printbase.com",
+ Description = "Textile and fabric-based products",
+ IsActive = true,
+ ModifiedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ ModifiedBy = "system@printbase.com",
+ Name = "Textile",
+ SortOrder = 1
+ },
+ new
+ {
+ Id = new Guid("22222222-2222-2222-2222-222222222222"),
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ CreatedBy = "system@printbase.com",
+ Description = "Products for hard surface printing",
+ IsActive = true,
+ ModifiedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ ModifiedBy = "system@printbase.com",
+ Name = "Hard Surfaces",
+ SortOrder = 2
+ },
+ new
+ {
+ Id = new Guid("33333333-3333-3333-3333-333333333333"),
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ CreatedBy = "system@printbase.com",
+ Description = "Paper-based printing products",
+ IsActive = true,
+ ModifiedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ ModifiedBy = "system@printbase.com",
+ Name = "Paper",
+ SortOrder = 3
+ });
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Product.Product", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWID()");
+
+ b.Property("BasePrice")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("CategoryId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Description")
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("ImageUrl")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.Property("IsCustomizable")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false);
+
+ b.Property("ModifiedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property("ModifiedBy")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("IX_Product_CategoryId");
+
+ b.HasIndex("CreatedAt")
+ .HasDatabaseName("IX_Product_CreatedAt");
+
+ b.HasIndex("CreatedBy")
+ .HasDatabaseName("IX_Product_CreatedBy");
+
+ b.HasIndex("IsActive")
+ .HasDatabaseName("IX_Product_IsActive");
+
+ b.HasIndex("IsCustomizable")
+ .HasDatabaseName("IX_Product_IsCustomizable");
+
+ b.HasIndex("ModifiedAt")
+ .HasDatabaseName("IX_Product_ModifiedAt");
+
+ b.HasIndex("Name")
+ .HasDatabaseName("IX_Product_Name");
+
+ b.HasIndex("CategoryId", "IsActive")
+ .HasDatabaseName("IX_Product_Category_Active");
+
+ b.HasIndex("IsActive", "IsCustomizable")
+ .HasDatabaseName("IX_Product_Active_Customizable");
+
+ b.ToTable("Products");
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Product.ProductVariant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWID()");
+
+ b.Property("Color")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ImageUrl")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.Property("ModifiedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property("ModifiedBy")
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Price")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("ProductId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Size")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Sku")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("StockQuantity")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt")
+ .HasDatabaseName("IX_ProductVariant_CreatedAt");
+
+ b.HasIndex("CreatedBy")
+ .HasDatabaseName("IX_ProductVariant_CreatedBy");
+
+ b.HasIndex("IsActive")
+ .HasDatabaseName("IX_ProductVariant_IsActive");
+
+ b.HasIndex("ModifiedAt")
+ .HasDatabaseName("IX_ProductVariant_ModifiedAt");
+
+ b.HasIndex("ProductId")
+ .HasDatabaseName("IX_ProductVariant_ProductId");
+
+ b.HasIndex("Sku")
+ .IsUnique()
+ .HasDatabaseName("IX_ProductVariant_SKU");
+
+ b.HasIndex("ProductId", "Size", "Color")
+ .IsUnique()
+ .HasDatabaseName("IX_ProductVariant_Product_Size_Color")
+ .HasFilter("[Color] IS NOT NULL");
+
+ b.ToTable("ProductVariants");
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Users.Address", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWID()");
+
+ b.Property("AddressType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("City")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Country")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.Property("IsDefault")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false);
+
+ b.Property("ModifiedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property("ModifiedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("PostalCode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("State")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Street")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt")
+ .HasDatabaseName("IX_Address_CreatedAt");
+
+ b.HasIndex("CreatedBy")
+ .HasDatabaseName("IX_Address_CreatedBy");
+
+ b.HasIndex("ModifiedAt")
+ .HasDatabaseName("IX_Address_ModifiedAt");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("IX_Address_UserId");
+
+ b.HasIndex("UserId", "AddressType")
+ .HasDatabaseName("IX_Address_User_Type");
+
+ b.HasIndex("UserId", "IsDefault")
+ .HasDatabaseName("IX_Address_User_Default");
+
+ b.ToTable("Addresses");
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Users.ApplicationRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IsActive")
+ .HasDatabaseName("IX_ApplicationRole_IsActive");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = "1",
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ Description = "Full system access",
+ IsActive = true,
+ Name = "Administrator",
+ NormalizedName = "ADMINISTRATOR"
+ },
+ new
+ {
+ Id = "2",
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ Description = "Standard customer access",
+ IsActive = true,
+ Name = "Customer",
+ NormalizedName = "CUSTOMER"
+ },
+ new
+ {
+ Id = "3",
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ Description = "Manage orders and fulfillment",
+ IsActive = true,
+ Name = "OrderManager",
+ NormalizedName = "ORDERMANAGER"
+ },
+ new
+ {
+ Id = "4",
+ CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ Description = "Manage products and inventory",
+ IsActive = true,
+ Name = "ProductManager",
+ NormalizedName = "PRODUCTMANAGER"
+ });
+ });
+
+ modelBuilder.Entity("Printbase.Domain.Entities.Users.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("GETUTCDATE()");
+
+ b.Property