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("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("LastLoginAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("ProfileImageUrl") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.Order", b => + { + b.HasOne("Printbase.Domain.Entities.Orders.OrderStatus", "OrderStatus") + .WithMany("Orders") + .HasForeignKey("OrderStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Orders.ShippingStatus", "ShippingStatus") + .WithMany("Orders") + .HasForeignKey("ShippingStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OrderStatus"); + + b.Navigation("ShippingStatus"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderAddress", b => + { + b.HasOne("Printbase.Domain.Entities.Orders.Order", "Order") + .WithOne("OrderAddress") + .HasForeignKey("Printbase.Domain.Entities.Orders.OrderAddress", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderItem", b => + { + b.HasOne("Printbase.Domain.Entities.Orders.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Product.Product", "Product") + .WithMany("OrderItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Product.ProductVariant", "ProductVariant") + .WithMany("OrderItems") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Order"); + + b.Navigation("Product"); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.Category", b => + { + b.HasOne("Printbase.Domain.Entities.Product.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.Product", b => + { + b.HasOne("Printbase.Domain.Entities.Product.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.ProductVariant", b => + { + b.HasOne("Printbase.Domain.Entities.Product.Product", "Product") + .WithMany("ProductVariants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Users.Address", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany("Addresses") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.Order", b => + { + b.Navigation("OrderAddress") + .IsRequired(); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderStatus", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.ShippingStatus", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.Product", b => + { + b.Navigation("OrderItems"); + + b.Navigation("ProductVariants"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.ProductVariant", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Users.ApplicationUser", b => + { + b.Navigation("Addresses"); + + b.Navigation("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Imprink.Infrastructure/Migrations/20250527103204_InitialSetup.cs b/src/Imprink.Infrastructure/Migrations/20250527103204_InitialSetup.cs new file mode 100644 index 0000000..63d3e63 --- /dev/null +++ b/src/Imprink.Infrastructure/Migrations/20250527103204_InitialSetup.cs @@ -0,0 +1,824 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Printbase.Infrastructure.Migrations +{ + /// + public partial class InitialSetup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + FirstName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + LastName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + DateOfBirth = table.Column(type: "datetime2", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + LastLoginAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + ProfileImageUrl = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: false), + ImageUrl = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + SortOrder = table.Column(type: "int", nullable: false, defaultValue: 0), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + ParentCategoryId = table.Column(type: "uniqueidentifier", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: true), + ModifiedAt = table.Column(type: "datetime2", nullable: true, defaultValueSql: "GETUTCDATE()"), + CreatedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), + ModifiedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + table.ForeignKey( + name: "FK_Categories_Categories_ParentCategoryId", + column: x => x.ParentCategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OrderStatuses", + columns: table => new + { + Id = table.Column(type: "int", nullable: false), + Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderStatuses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ShippingStatuses", + columns: table => new + { + Id = table.Column(type: "int", nullable: false), + Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShippingStatuses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"), + UserId = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + AddressType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Street = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + City = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + State = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + PostalCode = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + Country = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + IsDefault = table.Column(type: "bit", nullable: false, defaultValue: false), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ModifiedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + CreatedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + ModifiedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + table.ForeignKey( + name: "FK_Addresses_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + BasePrice = table.Column(type: "decimal(18,2)", nullable: false), + IsCustomizable = table.Column(type: "bit", nullable: false, defaultValue: false), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + ImageUrl = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + CategoryId = table.Column(type: "uniqueidentifier", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: true), + ModifiedAt = table.Column(type: "datetime2", nullable: true, defaultValueSql: "GETUTCDATE()"), + CreatedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), + ModifiedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + table.ForeignKey( + name: "FK_Products_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"), + UserId = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + OrderDate = table.Column(type: "datetime2", nullable: false), + TotalPrice = table.Column(type: "decimal(18,2)", nullable: false), + OrderStatusId = table.Column(type: "int", nullable: false), + ShippingStatusId = table.Column(type: "int", nullable: false), + OrderNumber = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Notes = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ModifiedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + CreatedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + ModifiedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Orders_OrderStatuses_OrderStatusId", + column: x => x.OrderStatusId, + principalTable: "OrderStatuses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Orders_ShippingStatuses_ShippingStatusId", + column: x => x.ShippingStatusId, + principalTable: "ShippingStatuses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ProductVariants", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"), + ProductId = table.Column(type: "uniqueidentifier", nullable: false), + Size = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Color = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Price = table.Column(type: "decimal(18,2)", nullable: false), + ImageUrl = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Sku = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + StockQuantity = table.Column(type: "int", nullable: false, defaultValue: 0), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + CreatedAt = table.Column(type: "datetime2", nullable: true), + ModifiedAt = table.Column(type: "datetime2", nullable: true, defaultValueSql: "GETUTCDATE()"), + CreatedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), + ModifiedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductVariants", x => x.Id); + table.ForeignKey( + name: "FK_ProductVariants_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrderAddresses", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"), + OrderId = table.Column(type: "uniqueidentifier", nullable: false), + Street = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + City = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + State = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + PostalCode = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + Country = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ModifiedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + CreatedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + ModifiedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderAddresses", x => x.Id); + table.ForeignKey( + name: "FK_OrderAddresses_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrderItems", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"), + OrderId = table.Column(type: "uniqueidentifier", nullable: false), + ProductId = table.Column(type: "uniqueidentifier", nullable: false), + ProductVariantId = table.Column(type: "uniqueidentifier", nullable: true), + Quantity = table.Column(type: "int", nullable: false, defaultValue: 1), + UnitPrice = table.Column(type: "decimal(18,2)", nullable: false), + TotalPrice = table.Column(type: "decimal(18,2)", nullable: false), + CustomizationImageUrl = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + CustomizationDescription = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ModifiedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"), + CreatedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + ModifiedBy = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderItems", x => x.Id); + table.ForeignKey( + name: "FK_OrderItems_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderItems_ProductVariants_ProductVariantId", + column: x => x.ProductVariantId, + principalTable: "ProductVariants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrderItems_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.InsertData( + table: "AspNetRoles", + columns: new[] { "Id", "ConcurrencyStamp", "CreatedAt", "Description", "IsActive", "Name", "NormalizedName" }, + values: new object[,] + { + { "1", null, new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "Full system access", true, "Administrator", "ADMINISTRATOR" }, + { "2", null, new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "Standard customer access", true, "Customer", "CUSTOMER" }, + { "3", null, new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "Manage orders and fulfillment", true, "OrderManager", "ORDERMANAGER" }, + { "4", null, new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "Manage products and inventory", true, "ProductManager", "PRODUCTMANAGER" } + }); + + migrationBuilder.InsertData( + table: "Categories", + columns: new[] { "Id", "CreatedAt", "CreatedBy", "Description", "ImageUrl", "IsActive", "ModifiedAt", "ModifiedBy", "Name", "ParentCategoryId", "SortOrder" }, + values: new object[,] + { + { new Guid("11111111-1111-1111-1111-111111111111"), new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "system@printbase.com", "Textile and fabric-based products", null, true, new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "system@printbase.com", "Textile", null, 1 }, + { new Guid("22222222-2222-2222-2222-222222222222"), new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "system@printbase.com", "Products for hard surface printing", null, true, new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "system@printbase.com", "Hard Surfaces", null, 2 }, + { new Guid("33333333-3333-3333-3333-333333333333"), new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "system@printbase.com", "Paper-based printing products", null, true, new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), "system@printbase.com", "Paper", null, 3 } + }); + + migrationBuilder.InsertData( + table: "OrderStatuses", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 0, "Pending" }, + { 1, "Processing" }, + { 2, "Completed" }, + { 3, "Cancelled" } + }); + + migrationBuilder.InsertData( + table: "ShippingStatuses", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 0, "Prepping" }, + { 1, "Packaging" }, + { 2, "Shipped" }, + { 3, "Delivered" } + }); + + migrationBuilder.CreateIndex( + name: "IX_Address_CreatedAt", + table: "Addresses", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Address_CreatedBy", + table: "Addresses", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Address_ModifiedAt", + table: "Addresses", + column: "ModifiedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Address_User_Default", + table: "Addresses", + columns: new[] { "UserId", "IsDefault" }); + + migrationBuilder.CreateIndex( + name: "IX_Address_User_Type", + table: "Addresses", + columns: new[] { "UserId", "AddressType" }); + + migrationBuilder.CreateIndex( + name: "IX_Address_UserId", + table: "Addresses", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_ApplicationRole_IsActive", + table: "AspNetRoles", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Category_Active_SortOrder", + table: "Categories", + columns: new[] { "IsActive", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_Category_CreatedAt", + table: "Categories", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Category_CreatedBy", + table: "Categories", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Category_IsActive", + table: "Categories", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_Category_ModifiedAt", + table: "Categories", + column: "ModifiedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Category_Name", + table: "Categories", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_Category_Parent_SortOrder", + table: "Categories", + columns: new[] { "ParentCategoryId", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_Category_ParentCategoryId", + table: "Categories", + column: "ParentCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderAddress_CreatedAt", + table: "OrderAddresses", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_OrderAddress_CreatedBy", + table: "OrderAddresses", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_OrderAddress_ModifiedAt", + table: "OrderAddresses", + column: "ModifiedAt"); + + migrationBuilder.CreateIndex( + name: "IX_OrderAddress_OrderId", + table: "OrderAddresses", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrderItem_CreatedAt", + table: "OrderItems", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItem_CreatedBy", + table: "OrderItems", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItem_ModifiedAt", + table: "OrderItems", + column: "ModifiedAt"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItem_Order_Product", + table: "OrderItems", + columns: new[] { "OrderId", "ProductId" }); + + migrationBuilder.CreateIndex( + name: "IX_OrderItem_OrderId", + table: "OrderItems", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItem_ProductId", + table: "OrderItems", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItem_ProductVariantId", + table: "OrderItems", + column: "ProductVariantId"); + + migrationBuilder.CreateIndex( + name: "IX_Order_CreatedAt", + table: "Orders", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Order_CreatedBy", + table: "Orders", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Order_ModifiedAt", + table: "Orders", + column: "ModifiedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Order_OrderDate", + table: "Orders", + column: "OrderDate"); + + migrationBuilder.CreateIndex( + name: "IX_Order_OrderNumber", + table: "Orders", + column: "OrderNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Order_OrderStatusId", + table: "Orders", + column: "OrderStatusId"); + + migrationBuilder.CreateIndex( + name: "IX_Order_ShippingStatusId", + table: "Orders", + column: "ShippingStatusId"); + + migrationBuilder.CreateIndex( + name: "IX_Order_User_Date", + table: "Orders", + columns: new[] { "UserId", "OrderDate" }); + + migrationBuilder.CreateIndex( + name: "IX_Order_UserId", + table: "Orders", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderStatus_Name", + table: "OrderStatuses", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Product_Active_Customizable", + table: "Products", + columns: new[] { "IsActive", "IsCustomizable" }); + + migrationBuilder.CreateIndex( + name: "IX_Product_Category_Active", + table: "Products", + columns: new[] { "CategoryId", "IsActive" }); + + migrationBuilder.CreateIndex( + name: "IX_Product_CategoryId", + table: "Products", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Product_CreatedAt", + table: "Products", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Product_CreatedBy", + table: "Products", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Product_IsActive", + table: "Products", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_Product_IsCustomizable", + table: "Products", + column: "IsCustomizable"); + + migrationBuilder.CreateIndex( + name: "IX_Product_ModifiedAt", + table: "Products", + column: "ModifiedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Product_Name", + table: "Products", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariant_CreatedAt", + table: "ProductVariants", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariant_CreatedBy", + table: "ProductVariants", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariant_IsActive", + table: "ProductVariants", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariant_ModifiedAt", + table: "ProductVariants", + column: "ModifiedAt"); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariant_Product_Size_Color", + table: "ProductVariants", + columns: new[] { "ProductId", "Size", "Color" }, + unique: true, + filter: "[Color] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariant_ProductId", + table: "ProductVariants", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariant_SKU", + table: "ProductVariants", + column: "Sku", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShippingStatus_Name", + table: "ShippingStatuses", + column: "Name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Addresses"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "OrderAddresses"); + + migrationBuilder.DropTable( + name: "OrderItems"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "ProductVariants"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "OrderStatuses"); + + migrationBuilder.DropTable( + name: "ShippingStatuses"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/src/Imprink.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Imprink.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..42f0f1e --- /dev/null +++ b/src/Imprink.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,1209 @@ +// +using System; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Printbase.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(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("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("LastLoginAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("ProfileImageUrl") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.Order", b => + { + b.HasOne("Printbase.Domain.Entities.Orders.OrderStatus", "OrderStatus") + .WithMany("Orders") + .HasForeignKey("OrderStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Orders.ShippingStatus", "ShippingStatus") + .WithMany("Orders") + .HasForeignKey("ShippingStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OrderStatus"); + + b.Navigation("ShippingStatus"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderAddress", b => + { + b.HasOne("Printbase.Domain.Entities.Orders.Order", "Order") + .WithOne("OrderAddress") + .HasForeignKey("Printbase.Domain.Entities.Orders.OrderAddress", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderItem", b => + { + b.HasOne("Printbase.Domain.Entities.Orders.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Product.Product", "Product") + .WithMany("OrderItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Printbase.Domain.Entities.Product.ProductVariant", "ProductVariant") + .WithMany("OrderItems") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Order"); + + b.Navigation("Product"); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.Category", b => + { + b.HasOne("Printbase.Domain.Entities.Product.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.Product", b => + { + b.HasOne("Printbase.Domain.Entities.Product.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.ProductVariant", b => + { + b.HasOne("Printbase.Domain.Entities.Product.Product", "Product") + .WithMany("ProductVariants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Users.Address", b => + { + b.HasOne("Printbase.Domain.Entities.Users.ApplicationUser", null) + .WithMany("Addresses") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.Order", b => + { + b.Navigation("OrderAddress") + .IsRequired(); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.OrderStatus", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Orders.ShippingStatus", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.Product", b => + { + b.Navigation("OrderItems"); + + b.Navigation("ProductVariants"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Product.ProductVariant", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Printbase.Domain.Entities.Users.ApplicationUser", b => + { + b.Navigation("Addresses"); + + b.Navigation("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Imprink.Infrastructure/Repositories/CategoryRepository.cs b/src/Imprink.Infrastructure/Repositories/CategoryRepository.cs new file mode 100644 index 0000000..c71678f --- /dev/null +++ b/src/Imprink.Infrastructure/Repositories/CategoryRepository.cs @@ -0,0 +1,111 @@ +using Imprink.Domain.Entities.Product; +using Imprink.Domain.Repositories; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.Infrastructure.Repositories; + +public class CategoryRepository(ApplicationDbContext context) : ICategoryRepository +{ + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Categories + .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + } + + public async Task GetByIdWithSubCategoriesAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Categories + .Include(c => c.SubCategories.Where(sc => sc.IsActive)) + .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + } + + public async Task GetByIdWithProductsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Categories + .Include(c => c.Products.Where(p => p.IsActive)) + .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await context.Categories + .OrderBy(c => c.SortOrder) + .ThenBy(c => c.Name) + .ToListAsync(cancellationToken); + } + + public async Task> GetActiveAsync(CancellationToken cancellationToken = default) + { + return await context.Categories + .Where(c => c.IsActive) + .OrderBy(c => c.SortOrder) + .ThenBy(c => c.Name) + .ToListAsync(cancellationToken); + } + + public async Task> GetRootCategoriesAsync(CancellationToken cancellationToken = default) + { + return await context.Categories + .Where(c => c.ParentCategoryId == null && c.IsActive) + .OrderBy(c => c.SortOrder) + .ThenBy(c => c.Name) + .ToListAsync(cancellationToken); + } + + public async Task> GetSubCategoriesAsync(Guid parentCategoryId, CancellationToken cancellationToken = default) + { + return await context.Categories + .Where(c => c.ParentCategoryId == parentCategoryId && c.IsActive) + .OrderBy(c => c.SortOrder) + .ThenBy(c => c.Name) + .ToListAsync(cancellationToken); + } + + public async Task AddAsync(Category category, CancellationToken cancellationToken = default) + { + category.Id = Guid.NewGuid(); + category.CreatedAt = DateTime.UtcNow; + category.ModifiedAt = DateTime.UtcNow; + + context.Categories.Add(category); + await context.SaveChangesAsync(cancellationToken); + return category; + } + + public async Task UpdateAsync(Category category, CancellationToken cancellationToken = default) + { + category.ModifiedAt = DateTime.UtcNow; + context.Categories.Update(category); + await context.SaveChangesAsync(cancellationToken); + return category; + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var category = await GetByIdAsync(id, cancellationToken); + if (category != null) + { + context.Categories.Remove(category); + await context.SaveChangesAsync(cancellationToken); + } + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Categories + .AnyAsync(c => c.Id == id, cancellationToken); + } + + public async Task HasSubCategoriesAsync(Guid categoryId, CancellationToken cancellationToken = default) + { + return await context.Categories + .AnyAsync(c => c.ParentCategoryId == categoryId, cancellationToken); + } + + public async Task HasProductsAsync(Guid categoryId, CancellationToken cancellationToken = default) + { + return await context.Products + .AnyAsync(p => p.CategoryId == categoryId, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Repositories/ProductRepository.cs b/src/Imprink.Infrastructure/Repositories/ProductRepository.cs new file mode 100644 index 0000000..742184e --- /dev/null +++ b/src/Imprink.Infrastructure/Repositories/ProductRepository.cs @@ -0,0 +1,165 @@ +using Imprink.Domain.Common.Models; +using Imprink.Domain.Entities.Product; +using Imprink.Domain.Repositories; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.Infrastructure.Repositories; + +public class ProductRepository(ApplicationDbContext context) : IProductRepository +{ + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Products + .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); + } + + public async Task GetByIdWithVariantsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Products + .Include(p => p.ProductVariants.Where(pv => pv.IsActive)) + .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); + } + + public async Task GetByIdWithCategoryAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Products + .Include(p => p.Category) + .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); + } + + public async Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Products + .Include(p => p.Category) + .Include(p => p.ProductVariants.Where(pv => pv.IsActive)) + .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); + } + + public async Task> GetPagedAsync(ProductFilterParameters filterParameters, CancellationToken cancellationToken = default) + { + var query = context.Products + .Include(p => p.Category) + .AsQueryable(); + + if (filterParameters.IsActive.HasValue) + { + query = query.Where(p => p.IsActive == filterParameters.IsActive.Value); + } + + if (!string.IsNullOrEmpty(filterParameters.SearchTerm)) + { + query = query.Where(p => p.Name.Contains(filterParameters.SearchTerm) || + (p.Description != null && p.Description.Contains(filterParameters.SearchTerm))); + } + + if (filterParameters.CategoryId.HasValue) + { + query = query.Where(p => p.CategoryId == filterParameters.CategoryId.Value); + } + + if (filterParameters.MinPrice.HasValue) + { + query = query.Where(p => p.BasePrice >= filterParameters.MinPrice.Value); + } + + if (filterParameters.MaxPrice.HasValue) + { + query = query.Where(p => p.BasePrice <= filterParameters.MaxPrice.Value); + } + + if (filterParameters.IsCustomizable.HasValue) + { + query = query.Where(p => p.IsCustomizable == filterParameters.IsCustomizable.Value); + } + + query = filterParameters.SortBy.ToLower() switch + { + "price" => filterParameters.SortDirection.Equals("DESC" + , StringComparison.CurrentCultureIgnoreCase) + ? query.OrderByDescending(p => p.BasePrice) + : query.OrderBy(p => p.BasePrice), + "name" => filterParameters.SortDirection.Equals("DESC" + , StringComparison.CurrentCultureIgnoreCase) + ? query.OrderByDescending(p => p.Name) + : query.OrderBy(p => p.Name), + _ => query.OrderBy(p => p.Name) + }; + + var totalCount = await query.CountAsync(cancellationToken); + + var items = await query + .Skip((filterParameters.PageNumber - 1) * filterParameters.PageSize) + .Take(filterParameters.PageSize) + .ToListAsync(cancellationToken); + + return new PagedResult + { + Items = items, + TotalCount = totalCount, + PageNumber = filterParameters.PageNumber, + PageSize = filterParameters.PageSize + }; + } + + public async Task> GetByCategoryAsync(Guid categoryId, CancellationToken cancellationToken = default) + { + return await context.Products + .Include(p => p.Category) + .Where(p => p.CategoryId == categoryId && p.IsActive) + .OrderBy(p => p.Name) + .ToListAsync(cancellationToken); + } + + public async Task> GetCustomizableAsync(CancellationToken cancellationToken = default) + { + return await context.Products + .Include(p => p.Category) + .Where(p => p.IsCustomizable && p.IsActive) + .OrderBy(p => p.Name) + .ToListAsync(cancellationToken); + } + + public async Task AddAsync(Product product, CancellationToken cancellationToken = default) + { + product.Id = Guid.NewGuid(); + product.CreatedAt = DateTime.UtcNow; + product.ModifiedAt = DateTime.UtcNow; + + context.Products.Add(product); + await context.SaveChangesAsync(cancellationToken); + + if (product.CategoryId.HasValue) + { + await context.Entry(product) + .Reference(p => p.Category) + .LoadAsync(cancellationToken); + } + + return product; + } + + public async Task UpdateAsync(Product product, CancellationToken cancellationToken = default) + { + product.ModifiedAt = DateTime.UtcNow; + context.Products.Update(product); + await context.SaveChangesAsync(cancellationToken); + return product; + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var product = await GetByIdAsync(id, cancellationToken); + if (product != null) + { + context.Products.Remove(product); + await context.SaveChangesAsync(cancellationToken); + } + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Products + .AnyAsync(p => p.Id == id, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Repositories/ProductVariantRepository.cs b/src/Imprink.Infrastructure/Repositories/ProductVariantRepository.cs new file mode 100644 index 0000000..a7255cc --- /dev/null +++ b/src/Imprink.Infrastructure/Repositories/ProductVariantRepository.cs @@ -0,0 +1,118 @@ +using Imprink.Domain.Entities.Product; +using Imprink.Domain.Repositories; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.Infrastructure.Repositories; + +public class ProductVariantRepository(ApplicationDbContext context) : IProductVariantRepository +{ + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.ProductVariants + .FirstOrDefaultAsync(pv => pv.Id == id, cancellationToken); + } + + public async Task GetByIdWithProductAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.ProductVariants + .Include(pv => pv.Product) + .ThenInclude(p => p.Category) + .FirstOrDefaultAsync(pv => pv.Id == id, cancellationToken); + } + + public async Task GetBySkuAsync(string sku, CancellationToken cancellationToken = default) + { + return await context.ProductVariants + .Include(pv => pv.Product) + .FirstOrDefaultAsync(pv => pv.Sku == sku, cancellationToken); + } + + public async Task> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default) + { + return await context.ProductVariants + .Include(pv => pv.Product) + .Where(pv => pv.ProductId == productId) + .OrderBy(pv => pv.Size) + .ThenBy(pv => pv.Color) + .ToListAsync(cancellationToken); + } + + public async Task> GetActiveByProductIdAsync(Guid productId, CancellationToken cancellationToken = default) + { + return await context.ProductVariants + .Include(pv => pv.Product) + .Where(pv => pv.ProductId == productId && pv.IsActive) + .OrderBy(pv => pv.Size) + .ThenBy(pv => pv.Color) + .ToListAsync(cancellationToken); + } + + public async Task> GetInStockByProductIdAsync(Guid productId, CancellationToken cancellationToken = default) + { + return await context.ProductVariants + .Include(pv => pv.Product) + .Where(pv => pv.ProductId == productId && pv.IsActive && pv.StockQuantity > 0) + .OrderBy(pv => pv.Size) + .ThenBy(pv => pv.Color) + .ToListAsync(cancellationToken); + } + + public async Task AddAsync(ProductVariant productVariant, CancellationToken cancellationToken = default) + { + productVariant.Id = Guid.NewGuid(); + productVariant.CreatedAt = DateTime.UtcNow; + productVariant.ModifiedAt = DateTime.UtcNow; + + context.ProductVariants.Add(productVariant); + await context.SaveChangesAsync(cancellationToken); + return productVariant; + } + + public async Task UpdateAsync(ProductVariant productVariant, CancellationToken cancellationToken = default) + { + productVariant.ModifiedAt = DateTime.UtcNow; + context.ProductVariants.Update(productVariant); + await context.SaveChangesAsync(cancellationToken); + return productVariant; + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var productVariant = await GetByIdAsync(id, cancellationToken); + if (productVariant != null) + { + context.ProductVariants.Remove(productVariant); + await context.SaveChangesAsync(cancellationToken); + } + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.ProductVariants + .AnyAsync(pv => pv.Id == id, cancellationToken); + } + + public async Task SkuExistsAsync(string sku, Guid? excludeId = null, CancellationToken cancellationToken = default) + { + var query = context.ProductVariants.Where(pv => pv.Sku == sku); + + if (excludeId.HasValue) + { + query = query.Where(pv => pv.Id != excludeId.Value); + } + + return await query.AnyAsync(cancellationToken); + } + + public async Task UpdateStockQuantityAsync(Guid id, int quantity, CancellationToken cancellationToken = default) + { + var productVariant = await GetByIdAsync(id, cancellationToken); + if (productVariant != null) + { + productVariant.StockQuantity = quantity; + productVariant.ModifiedAt = DateTime.UtcNow; + await context.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/UnitOfWork.cs b/src/Imprink.Infrastructure/UnitOfWork.cs new file mode 100644 index 0000000..3386e7c --- /dev/null +++ b/src/Imprink.Infrastructure/UnitOfWork.cs @@ -0,0 +1,36 @@ +using Imprink.Application; +using Imprink.Domain.Repositories; +using Imprink.Infrastructure.Database; + +namespace Imprink.Infrastructure; + +public class UnitOfWork( + ApplicationDbContext context, + IProductRepository productRepository, + IProductVariantRepository productVariantRepository, + ICategoryRepository categoryRepository) : IUnitOfWork +{ + public IProductRepository ProductRepository => productRepository; + public IProductVariantRepository ProductVariantRepository => productVariantRepository; + public ICategoryRepository CategoryRepository => categoryRepository; + + public async Task SaveAsync(CancellationToken cancellationToken = default) + { + await context.SaveChangesAsync(cancellationToken); + } + + public Task BeginTransactionAsync(CancellationToken cancellationToken = default) + { + return context.Database.BeginTransactionAsync(cancellationToken); + } + + public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + await context.Database.CommitTransactionAsync(cancellationToken); + } + + public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + { + await context.Database.RollbackTransactionAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/CategoriesController.cs b/src/Imprink.WebApi/Controllers/CategoriesController.cs new file mode 100644 index 0000000..557f8e2 --- /dev/null +++ b/src/Imprink.WebApi/Controllers/CategoriesController.cs @@ -0,0 +1,46 @@ +using Imprink.Application.Products.Commands; +using Imprink.Application.Products.Dtos; +using Imprink.Application.Products.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace Imprink.WebApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CategoriesController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task>> GetCategories( + [FromQuery] bool? isActive = null, + [FromQuery] bool rootCategoriesOnly = false) + { + var query = new GetCategoriesQuery + { + IsActive = isActive, + RootCategoriesOnly = rootCategoriesOnly + }; + + var result = await mediator.Send(query); + return Ok(result); + } + + [HttpPost] + public async Task> CreateCategory([FromBody] CreateCategoryCommand command) + { + var result = await mediator.Send(command); + return CreatedAtAction(nameof(CreateCategory), new { id = result.Id }, result); + } + + [HttpDelete("{id:guid}")] + public async Task DeleteCategory(Guid id) + { + var command = new DeleteCategoryCommand { Id = id }; + var result = await mediator.Send(command); + + if (!result) + return NotFound(); + + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/ProductVariantsController.cs b/src/Imprink.WebApi/Controllers/ProductVariantsController.cs new file mode 100644 index 0000000..1454e3e --- /dev/null +++ b/src/Imprink.WebApi/Controllers/ProductVariantsController.cs @@ -0,0 +1,48 @@ +using Imprink.Application.Products.Commands; +using Imprink.Application.Products.Dtos; +using Imprink.Application.Products.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace Imprink.WebApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ProductVariantsController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task>> GetProductVariants( + [FromQuery] Guid? productId = null, + [FromQuery] bool? isActive = null, + [FromQuery] bool inStockOnly = false) + { + var query = new GetProductVariantsQuery + { + ProductId = productId, + IsActive = isActive, + InStockOnly = inStockOnly + }; + + var result = await mediator.Send(query); + return Ok(result); + } + + [HttpPost] + public async Task> CreateProductVariant([FromBody] CreateProductVariantCommand command) + { + var result = await mediator.Send(command); + return CreatedAtAction(nameof(CreateProductVariant), new { id = result.Id }, result); + } + + [HttpDelete("{id:guid}")] + public async Task DeleteProductVariant(Guid id) + { + var command = new DeleteProductVariantCommand { Id = id }; + var result = await mediator.Send(command); + + if (!result) + return NotFound(); + + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/ProductsController.cs b/src/Imprink.WebApi/Controllers/ProductsController.cs new file mode 100644 index 0000000..5a7d74b --- /dev/null +++ b/src/Imprink.WebApi/Controllers/ProductsController.cs @@ -0,0 +1,65 @@ +using Imprink.Application.Products.Commands; +using Imprink.Application.Products.Dtos; +using Imprink.Application.Products.Queries; +using Imprink.Domain.Common.Models; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace Imprink.WebApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ProductsController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task>> GetProducts( + [FromQuery] int pageNumber = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? searchTerm = null, + [FromQuery] Guid? categoryId = null, + [FromQuery] decimal? minPrice = null, + [FromQuery] decimal? maxPrice = null, + [FromQuery] bool? isActive = true, + [FromQuery] bool? isCustomizable = null, + [FromQuery] string sortBy = "Name", + [FromQuery] string sortDirection = "ASC") + { + var filterParameters = new ProductFilterParameters + { + PageNumber = pageNumber, + PageSize = pageSize, + SearchTerm = searchTerm, + CategoryId = categoryId, + MinPrice = minPrice, + MaxPrice = maxPrice, + IsActive = isActive, + IsCustomizable = isCustomizable, + SortBy = sortBy, + SortDirection = sortDirection + }; + + var query = new GetProductsQuery { FilterParameters = filterParameters }; + var result = await mediator.Send(query); + + return Ok(result); + } + + [HttpPost] + public async Task> CreateProduct([FromBody] CreateProductCommand command) + { + var result = await mediator.Send(command); + return CreatedAtAction(nameof(CreateProduct), new { id = result.Id }, result); + } + + [HttpDelete("{id:guid}")] + public async Task DeleteProduct(Guid id) + { + var command = new DeleteProductCommand { Id = id }; + var result = await mediator.Send(command); + + if (!result) + return NotFound(); + + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Imprink.WebApi.csproj b/src/Imprink.WebApi/Imprink.WebApi.csproj new file mode 100644 index 0000000..e57cd4d --- /dev/null +++ b/src/Imprink.WebApi/Imprink.WebApi.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/src/Imprink.WebApi/Program.cs b/src/Imprink.WebApi/Program.cs new file mode 100644 index 0000000..492b1c9 --- /dev/null +++ b/src/Imprink.WebApi/Program.cs @@ -0,0 +1,11 @@ +using Imprink.WebApi; + +var builder = WebApplication.CreateBuilder(args); + +Startup.ConfigureServices(builder); + +var app = builder.Build(); + +Startup.Configure(app, app.Environment); + +app.Run(); \ No newline at end of file diff --git a/src/Imprink.WebApi/Properties/launchSettings.json b/src/Imprink.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..eb0c887 --- /dev/null +++ b/src/Imprink.WebApi/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5220", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7277;http://localhost:5220", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Imprink.WebApi/Startup.cs b/src/Imprink.WebApi/Startup.cs new file mode 100644 index 0000000..b925f68 --- /dev/null +++ b/src/Imprink.WebApi/Startup.cs @@ -0,0 +1,128 @@ +using Imprink.Application; +using Imprink.Application.Products.Handlers; +using Imprink.Domain.Entities.Users; +using Imprink.Domain.Repositories; +using Imprink.Infrastructure; +using Imprink.Infrastructure.Database; +using Imprink.Infrastructure.Repositories; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.WebApi; + +public static class Startup +{ + public static void ConfigureServices(WebApplicationBuilder builder) + { + var services = builder.Services; + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddDbContext(options => + options.UseSqlServer( + builder.Configuration.GetConnectionString("DefaultConnection"), + b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName))); + + services.AddMediatR(cfg => + { + cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly); + }); + + services.AddIdentity(options => + { + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireUppercase = true; + options.Password.RequireNonAlphanumeric = true; + options.Password.RequiredLength = 8; + options.Password.RequiredUniqueChars = 1; + + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = true; + + options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + options.User.RequireUniqueEmail = true; + + options.SignIn.RequireConfirmedEmail = true; + options.SignIn.RequireConfirmedPhoneNumber = false; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + services.ConfigureApplicationCookie(options => + { + options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromMinutes(60); + options.LoginPath = "/Account/Login"; + options.AccessDeniedPath = "/Account/AccessDenied"; + options.SlidingExpiration = true; + }); + + services.Configure(options => + { + options.TokenLifespan = TimeSpan.FromHours(24); + }); + + services.AddAuthorizationBuilder() + .AddPolicy("AdminPolicy", policy => + policy.RequireRole("Administrator")) + .AddPolicy("OrderManagementPolicy", policy => + policy.RequireRole("Administrator", "OrderManager")) + .AddPolicy("ProductManagementPolicy", policy => + policy.RequireRole("Administrator", "ProductManager")) + .AddPolicy("CustomerPolicy", policy => + policy.RequireRole("Customer", "Administrator", "OrderManager", "ProductManager")); + + services.AddControllers(); + services.AddSwaggerGen(); + } + + public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (app is WebApplication application) + { + using var scope = application.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + try + { + dbContext.Database.Migrate(); + Console.WriteLine("Database migrations applied successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred while applying migrations: {ex.Message}"); + } + } + + // if (env.IsDevelopment()) + // { + // app.UseSwagger(); + // app.UseSwaggerUI(); + // app.UseDeveloperExceptionPage(); + // } + // else + // { + // app.UseExceptionHandler("/Error"); + // app.UseHsts(); + // app.UseHttpsRedirection(); + // } + + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseDeveloperExceptionPage(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/appsettings.Development.json b/src/Imprink.WebApi/appsettings.Development.json new file mode 100644 index 0000000..e0a33a4 --- /dev/null +++ b/src/Imprink.WebApi/appsettings.Development.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Debug", + "Microsoft.EntityFrameworkCore": "Information" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=Printbase;Encrypt=false;Trusted_Connection=True;TrustServerCertificate=true;MultipleActiveResultSets=true;" + }, + "DatabaseOptions": { + "ApplyMigrationsAtStartup": true + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/appsettings.json b/src/Imprink.WebApi/appsettings.json new file mode 100644 index 0000000..f215cd6 --- /dev/null +++ b/src/Imprink.WebApi/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Debug", + "Microsoft.EntityFrameworkCore": "Information" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=Printbase;Encrypt=false;TrustServerCertificate=true;Trusted_Connection=True;MultipleActiveResultSets=true;" + }, + "DatabaseOptions": { + "ApplyMigrationsAtStartup": true + } +} \ No newline at end of file diff --git a/tests/Imprink.Application.Tests/Imprink.Application.Tests.csproj b/tests/Imprink.Application.Tests/Imprink.Application.Tests.csproj new file mode 100644 index 0000000..42b41d6 --- /dev/null +++ b/tests/Imprink.Application.Tests/Imprink.Application.Tests.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/tests/Imprink.Domain.Tests/Imprink.Domain.Tests.csproj b/tests/Imprink.Domain.Tests/Imprink.Domain.Tests.csproj new file mode 100644 index 0000000..e99f38d --- /dev/null +++ b/tests/Imprink.Domain.Tests/Imprink.Domain.Tests.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + diff --git a/tests/Imprink.Infrastructure.Tests/Imprink.Infrastructure.Tests.csproj b/tests/Imprink.Infrastructure.Tests/Imprink.Infrastructure.Tests.csproj new file mode 100644 index 0000000..38fa3f2 --- /dev/null +++ b/tests/Imprink.Infrastructure.Tests/Imprink.Infrastructure.Tests.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/tests/Imprink.WebApi.Tests/Imprink.WebApi.Tests.csproj b/tests/Imprink.WebApi.Tests/Imprink.WebApi.Tests.csproj new file mode 100644 index 0000000..e99f38d --- /dev/null +++ b/tests/Imprink.WebApi.Tests/Imprink.WebApi.Tests.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + +