From bcebac18e75efe35c29c78c78551afb8e51c0567 Mon Sep 17 00:00:00 2001 From: Said Date: Wed, 5 Aug 2026 23:58:33 +0300 Subject: [PATCH] Initial commit - ECommerce project --- .gitignore | 30 ++ ECommerce.API/Config/Authentication.cs | 52 +++ ECommerce.API/Config/Authorization.cs | 22 ++ .../OrderOwnerOrAdminHandler.cs | 55 ++++ ECommerce.API/Config/Cors.cs | 47 +++ ECommerce.API/Config/FluentEmail.cs | 27 ++ ECommerce.API/Config/RegisteringServices.cs | 21 ++ ECommerce.API/Config/Swagger.cs | 29 ++ ECommerce.API/Dockerfile | 57 ++++ ECommerce.API/ECommerce.API.csproj | 35 ++ ECommerce.API/Endpoints/RegisterEndpoints.cs | 23 ++ ECommerce.API/Endpoints/v1/CategoryMap.cs | 90 +++++ ECommerce.API/Endpoints/v1/ProductMap.cs | 77 +++++ ECommerce.API/Program.cs | 103 ++++++ ECommerce.API/Properties/launchSettings.json | 41 +++ ECommerce.API/api_output.log | 56 ++++ ECommerce.API/appsettings.Development.json | 32 ++ ECommerce.API/appsettings.json | 33 ++ ECommerce.API/watch_output.log | 0 ECommerce.Contracts/Constants/AppCon.cs | 11 + ECommerce.Contracts/Constants/AppEnum.cs | 63 ++++ ECommerce.Contracts/DTOs/CategoryFilter.cs | 10 + ECommerce.Contracts/DTOs/CategoryVM.cs | 25 ++ ECommerce.Contracts/DTOs/ProductFilter.cs | 14 + ECommerce.Contracts/DTOs/ProductVM.cs | 38 +++ ECommerce.Contracts/DTOs/TechnicalDetail.cs | 9 + .../ECommerce.Contracts.csproj | 18 + .../Exceptions/NotFoundException.cs | 13 + ECommerce.Contracts/Options/JWT.cs | 13 + .../Validators/CategoryVMValidator.cs | 12 + .../Validators/ProductVMValidator.cs | 14 + ECommerce.Core.Tests/CategoryServiceTests.cs | 68 ++++ .../ECommerce.Core.Tests.csproj | 28 ++ .../LocalizationMappingTests.cs | 153 +++++++++ ECommerce.Core.Tests/UnitTest1.cs | 10 + ECommerce.Core/ECommerce.Core.csproj | 28 ++ ECommerce.Core/Services/CategoryService.cs | 236 +++++++++++++ ECommerce.Core/Services/ProductService.cs | 138 ++++++++ ECommerce.Core/Utility/FilterExtension.cs | 126 +++++++ .../Utility/IEnumerableExtensions.cs | 27 ++ ECommerce.Core/Utility/Pagination.cs | 19 ++ ECommerce.Core/Utility/PaginationExtension.cs | 21 ++ ECommerce.Core/Utility/QueryableExtensions.cs | 31 ++ ECommerce.Core/Utility/SortExt.cs | 45 +++ ECommerce.Domain/Context.cs | 70 ++++ ECommerce.Domain/ECommerce.Domain.csproj | 30 ++ ECommerce.Domain/Entities/Category.cs | 108 ++++++ ECommerce.Domain/Entities/Product.cs | 135 ++++++++ .../20260105142147_InitialCreate.Designer.cs | 153 +++++++++ .../20260105142147_InitialCreate.cs | 99 ++++++ .../20260110093153_ModelUpdate.Designer.cs | 153 +++++++++ .../Migrations/20260110093153_ModelUpdate.cs | 100 ++++++ ...119105451_MultiLanguageSupport.Designer.cs | 152 +++++++++ .../20260119105451_MultiLanguageSupport.cs | 73 +++++ ..._TechnicalDetailsForCategories.Designer.cs | 171 ++++++++++ ...119121446_TechnicalDetailsForCategories.cs | 134 ++++++++ ...8_MakeTechnicalDetailsNullable.Designer.cs | 169 ++++++++++ ...0119180718_MakeTechnicalDetailsNullable.cs | 54 +++ ...60120122747_NestedTranslations.Designer.cs | 155 +++++++++ .../20260120122747_NestedTranslations.cs | 118 +++++++ ...20132648_QueryableTranslations.Designer.cs | 309 ++++++++++++++++++ .../20260120132648_QueryableTranslations.cs | 54 +++ .../Migrations/ContextModelSnapshot.cs | 306 +++++++++++++++++ 63 files changed, 4543 insertions(+) create mode 100644 .gitignore create mode 100755 ECommerce.API/Config/Authentication.cs create mode 100755 ECommerce.API/Config/Authorization.cs create mode 100644 ECommerce.API/Config/AuthorizeHandlers/OrderOwnerOrAdminHandler.cs create mode 100755 ECommerce.API/Config/Cors.cs create mode 100755 ECommerce.API/Config/FluentEmail.cs create mode 100755 ECommerce.API/Config/RegisteringServices.cs create mode 100755 ECommerce.API/Config/Swagger.cs create mode 100755 ECommerce.API/Dockerfile create mode 100644 ECommerce.API/ECommerce.API.csproj create mode 100644 ECommerce.API/Endpoints/RegisterEndpoints.cs create mode 100644 ECommerce.API/Endpoints/v1/CategoryMap.cs create mode 100644 ECommerce.API/Endpoints/v1/ProductMap.cs create mode 100644 ECommerce.API/Program.cs create mode 100644 ECommerce.API/Properties/launchSettings.json create mode 100644 ECommerce.API/api_output.log create mode 100644 ECommerce.API/appsettings.Development.json create mode 100644 ECommerce.API/appsettings.json create mode 100644 ECommerce.API/watch_output.log create mode 100755 ECommerce.Contracts/Constants/AppCon.cs create mode 100755 ECommerce.Contracts/Constants/AppEnum.cs create mode 100644 ECommerce.Contracts/DTOs/CategoryFilter.cs create mode 100644 ECommerce.Contracts/DTOs/CategoryVM.cs create mode 100644 ECommerce.Contracts/DTOs/ProductFilter.cs create mode 100644 ECommerce.Contracts/DTOs/ProductVM.cs create mode 100644 ECommerce.Contracts/DTOs/TechnicalDetail.cs create mode 100755 ECommerce.Contracts/ECommerce.Contracts.csproj create mode 100755 ECommerce.Contracts/Exceptions/NotFoundException.cs create mode 100755 ECommerce.Contracts/Options/JWT.cs create mode 100644 ECommerce.Contracts/Validators/CategoryVMValidator.cs create mode 100644 ECommerce.Contracts/Validators/ProductVMValidator.cs create mode 100644 ECommerce.Core.Tests/CategoryServiceTests.cs create mode 100644 ECommerce.Core.Tests/ECommerce.Core.Tests.csproj create mode 100644 ECommerce.Core.Tests/LocalizationMappingTests.cs create mode 100644 ECommerce.Core.Tests/UnitTest1.cs create mode 100755 ECommerce.Core/ECommerce.Core.csproj create mode 100644 ECommerce.Core/Services/CategoryService.cs create mode 100644 ECommerce.Core/Services/ProductService.cs create mode 100644 ECommerce.Core/Utility/FilterExtension.cs create mode 100644 ECommerce.Core/Utility/IEnumerableExtensions.cs create mode 100755 ECommerce.Core/Utility/Pagination.cs create mode 100644 ECommerce.Core/Utility/PaginationExtension.cs create mode 100644 ECommerce.Core/Utility/QueryableExtensions.cs create mode 100644 ECommerce.Core/Utility/SortExt.cs create mode 100755 ECommerce.Domain/Context.cs create mode 100755 ECommerce.Domain/ECommerce.Domain.csproj create mode 100644 ECommerce.Domain/Entities/Category.cs create mode 100644 ECommerce.Domain/Entities/Product.cs create mode 100644 ECommerce.Domain/Migrations/20260105142147_InitialCreate.Designer.cs create mode 100644 ECommerce.Domain/Migrations/20260105142147_InitialCreate.cs create mode 100644 ECommerce.Domain/Migrations/20260110093153_ModelUpdate.Designer.cs create mode 100644 ECommerce.Domain/Migrations/20260110093153_ModelUpdate.cs create mode 100644 ECommerce.Domain/Migrations/20260119105451_MultiLanguageSupport.Designer.cs create mode 100644 ECommerce.Domain/Migrations/20260119105451_MultiLanguageSupport.cs create mode 100644 ECommerce.Domain/Migrations/20260119121446_TechnicalDetailsForCategories.Designer.cs create mode 100644 ECommerce.Domain/Migrations/20260119121446_TechnicalDetailsForCategories.cs create mode 100644 ECommerce.Domain/Migrations/20260119180718_MakeTechnicalDetailsNullable.Designer.cs create mode 100644 ECommerce.Domain/Migrations/20260119180718_MakeTechnicalDetailsNullable.cs create mode 100644 ECommerce.Domain/Migrations/20260120122747_NestedTranslations.Designer.cs create mode 100644 ECommerce.Domain/Migrations/20260120122747_NestedTranslations.cs create mode 100644 ECommerce.Domain/Migrations/20260120132648_QueryableTranslations.Designer.cs create mode 100644 ECommerce.Domain/Migrations/20260120132648_QueryableTranslations.cs create mode 100644 ECommerce.Domain/Migrations/ContextModelSnapshot.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9681aa7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Build results +bin/ +obj/ +[BBb]uild/ +[Oo]bj/ +[Dd]ebug/ +[Rr]elease/ + +# NuGet +*.nupkg +*.snupkg +packages/ + +# IDE +.vs/ +.vscode/ +*.user +*.suo + +# Test results +TestResults/ +*.trx +coverage*/ + +# OS +.DS_Store +Thumbs.db + +# Uploads +wwwroot/Uploads/ diff --git a/ECommerce.API/Config/Authentication.cs b/ECommerce.API/Config/Authentication.cs new file mode 100755 index 0000000..46230d9 --- /dev/null +++ b/ECommerce.API/Config/Authentication.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +namespace Commerce.Config; + +public static class Authentication +{ + public static IServiceCollection Authenticate(this IServiceCollection services, ConfigurationManager configuration) + { + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(o => + { + + var jwtKey = configuration["JWT:Key"] ?? throw new InvalidOperationException("JWT:Key is missing in configuration."); + var Key = Encoding.UTF8.GetBytes(jwtKey); + o.SaveToken = true; + o.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, // on production make it true + ValidateAudience = false, // on production make it true + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = configuration["JWT:Issuer"], + ValidAudience = configuration["JWT:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Key), + ClockSkew = TimeSpan.Zero + }; + o.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) + { + context.Response.Headers.Append("IS-TOKEN-EXPIRED", "true"); + } + return Task.CompletedTask; + } + }; + }); + + return services; + } + +} diff --git a/ECommerce.API/Config/Authorization.cs b/ECommerce.API/Config/Authorization.cs new file mode 100755 index 0000000..5bd1fae --- /dev/null +++ b/ECommerce.API/Config/Authorization.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + + +namespace Commerce.Config; + public static class Authorization + { + public static IServiceCollection Authorize(this IServiceCollection services) + { + services.AddAuthorization(options => + { + options.AddPolicy("Admin", policy => policy.RequireRole("ADMIN")); + options.AddPolicy("Moderator", policy => policy.RequireRole("Moderator")); + options.AddPolicy("Seller", policy => policy.RequireRole("Seller")); + }); + + return services; + + } + } diff --git a/ECommerce.API/Config/AuthorizeHandlers/OrderOwnerOrAdminHandler.cs b/ECommerce.API/Config/AuthorizeHandlers/OrderOwnerOrAdminHandler.cs new file mode 100644 index 0000000..ce811a7 --- /dev/null +++ b/ECommerce.API/Config/AuthorizeHandlers/OrderOwnerOrAdminHandler.cs @@ -0,0 +1,55 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading.Tasks; +// using Microsoft.AspNetCore.Authorization; +// using Core.Services; +// using System.Security.Claims; +// using Microsoft.AspNetCore.Identity; +// using System.Threading.Tasks; + +// namespace Commerce.Config.AuthorizeHandlers; +// public class OrderOwnerOrAdminHandler : AuthorizationHandler +// { +// // private readonly IServiceProvider _serviceProvider; + +// // public OrderOwnerOrAdminHandler(IServiceProvider serviceProvider) +// // { +// // _serviceProvider = serviceProvider; +// // } +// private readonly IServiceProvider _serviceProvider; + +// public OrderOwnerOrAdminHandler(IServiceProvider serviceProvider) +// { +// _serviceProvider = serviceProvider; +// } + +// protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, +// OrderOwnerOrAdminRequirement requirement, Guid orderId) +// { + +// using var scope = _serviceProvider.CreateScope(); +// var orderService = scope.ServiceProvider.GetRequiredService(); + +// var user = context.User; +// var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; +// var isAdmin = user.IsInRole("Admin"); + +// if (isAdmin) +// { +// context.Succeed(requirement); +// return; +// } + +// // // var order = await orderService.GetOrderById(orderId.ToString()); +// // if (order != null && order.CustomerId == userId) +// // { +// // context.Succeed(requirement); +// // } +// } +// } + +// public class OrderOwnerOrAdminRequirement : IAuthorizationRequirement { } + + + diff --git a/ECommerce.API/Config/Cors.cs b/ECommerce.API/Config/Cors.cs new file mode 100755 index 0000000..560d73c --- /dev/null +++ b/ECommerce.API/Config/Cors.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Commerce.Config; + +public static class Cors +{ + public static IServiceCollection AddCustomCors(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddDefaultPolicy(builder => + { + builder + .AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod() + .WithExposedHeaders("X-Pagination"); // This line! + + // .AllowCredentials() + // .SetIsOriginAllowedToAllowWildcardSubdomains() // For SameSite=None + // .WithExposedHeaders("*"); + }); + + options.AddPolicy( + "default", + builder => + { + builder + .AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod() + .WithExposedHeaders("X-Pagination"); // This line! + + // .AllowCredentials() + // .SetIsOriginAllowedToAllowWildcardSubdomains() // For SameSite=None + // .WithExposedHeaders("*"); + } + ); + }); + + return services; + } +} + diff --git a/ECommerce.API/Config/FluentEmail.cs b/ECommerce.API/Config/FluentEmail.cs new file mode 100755 index 0000000..1397fe4 --- /dev/null +++ b/ECommerce.API/Config/FluentEmail.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Commerce.Config; + public static class FluentEmailExtensions + { + public static void RegFluentEmail( + this IServiceCollection services, + ConfigurationManager configuration + ) + { + var emailSettings = configuration.GetSection("MailSettings"); + var defaultFromEmail = emailSettings["Mail"]; + var host = emailSettings["Host"]; + var port = emailSettings.GetValue("Port"); + var userName = emailSettings["UserName"]; + var password = emailSettings["Password"]; + + services + .AddFluentEmail(defaultFromEmail) + .AddSmtpSender(host, port, userName, password) + .AddRazorRenderer(); + } + } + diff --git a/ECommerce.API/Config/RegisteringServices.cs b/ECommerce.API/Config/RegisteringServices.cs new file mode 100755 index 0000000..ff1707b --- /dev/null +++ b/ECommerce.API/Config/RegisteringServices.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Commerce.Core.Services; +using Microsoft.AspNetCore.Authorization; + +namespace Commerce.Config; + +public static class RegisteringServices +{ + public static IServiceCollection RegisterServices(this IServiceCollection services) + { + //test + + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/ECommerce.API/Config/Swagger.cs b/ECommerce.API/Config/Swagger.cs new file mode 100755 index 0000000..e331923 --- /dev/null +++ b/ECommerce.API/Config/Swagger.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Scalar.AspNetCore; + +namespace Commerce.Config; + +public static class Swagger +{ + public static IServiceCollection AddSwag(this IServiceCollection services) + { + services.AddOpenApi(options => + { + options.AddDocumentTransformer((document, context, cancellationToken) => + { + document.Info.Title = "Commerce API"; + document.Info.Version = "v1"; + return Task.CompletedTask; + }); + }); + return services; + } + + public static IEndpointRouteBuilder UseSwag(this IEndpointRouteBuilder app) + { + app.MapScalarApiReference(); + return app; + } +} + diff --git a/ECommerce.API/Dockerfile b/ECommerce.API/Dockerfile new file mode 100755 index 0000000..0061d05 --- /dev/null +++ b/ECommerce.API/Dockerfile @@ -0,0 +1,57 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS base + +# WORKDIR /app +# ENV DOTNET_URLS=http://*:2000 +#ENV ConnectionStrings__PostgreDB="Host=db;Database=ECommerce;Username=said;Password=aaa111!!!AAA;Port=5435;" + +ENV ASPNETCORE_ENVIRONMENT=Production + +FROM base AS build +WORKDIR /src + + +RUN dotnet tool install --global dotnet-ef --version 9.0.* +ENV PATH="$PATH:/root/.dotnet/tools" + + + +COPY ./Core/ ./Core +COPY ./Contracts/ ./Contracts +COPY ./Domain/ ./Domain +COPY ./ECommerce_API/ ./ECommerce_API + +RUN dotnet ef migrations script --startup-project ECommerce_API --project Domain -o sqlscript.sql + + + + +WORKDIR /src/ECommerce_API +RUN dotnet restore +RUN dotnet build -c Release -o /build + +WORKDIR /src + + +FROM build AS publish + +WORKDIR /src/ECommerce_API + + + +RUN dotnet publish -c Release -o /app/publish + + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21-amd64 AS runtime +WORKDIR /app + +COPY --from=publish /app/publish . +COPY --from=build /src/sqlscript.sql . +# ENV PATH="$PATH:/root/.dotnet/tools" +ENV ASPNETCORE_URLS=http://*:80 + + + + +# COPY --from=publish /src/Core.Contracts/EmailTemplates/. ./EmailTemplates +ENTRYPOINT ["dotnet", "ECommerce_API.dll"] + diff --git a/ECommerce.API/ECommerce.API.csproj b/ECommerce.API/ECommerce.API.csproj new file mode 100644 index 0000000..6e707c5 --- /dev/null +++ b/ECommerce.API/ECommerce.API.csproj @@ -0,0 +1,35 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ECommerce.API/Endpoints/RegisterEndpoints.cs b/ECommerce.API/Endpoints/RegisterEndpoints.cs new file mode 100644 index 0000000..4eb5876 --- /dev/null +++ b/ECommerce.API/Endpoints/RegisterEndpoints.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Commerce.Endpoints.v1; + +namespace Commerce.Endpoints; + +public static class RegisterEndpoints +{ + public static void MapAPIv1(this IEndpointRouteBuilder app) + { + //hello + app.MapGroup("api/v1/").ProductEndpoints().WithTags("Product").RequireCors("default"); + app.MapGroup("api/v1/").CategoryEndpoints().WithTags("Category").RequireCors("default"); + } + public static void MapAPIv2(this IEndpointRouteBuilder app) + { + //hello + app.MapGroup("api/v1/").ProductEndpoints().WithTags("Product").RequireCors("default"); + app.MapGroup("api/v1/").CategoryEndpoints().WithTags("Category").RequireCors("default"); + } +} diff --git a/ECommerce.API/Endpoints/v1/CategoryMap.cs b/ECommerce.API/Endpoints/v1/CategoryMap.cs new file mode 100644 index 0000000..0317e53 --- /dev/null +++ b/ECommerce.API/Endpoints/v1/CategoryMap.cs @@ -0,0 +1,90 @@ + +using Commerce.Contracts.DTOs; +using Commerce.Core.Services; +using Commerce.Domain.Entities; +using Generic.Contracts.Generics; +using Microsoft.AspNetCore.Mvc; + +namespace Commerce.Endpoints.v1; + +public static class CategoryMap +{ + public static RouteGroupBuilder CategoryEndpoints(this RouteGroupBuilder group) + { + + + // No Fetch + group.MapGet( + "categories", + async ( + [AsParameters] CategoryFilter categoryFilter, + [AsParameters] Pagination pagination, + CategoryService categoryService, + HttpContext httpContext + ) => + { + var result = await categoryService.FetchCategory(categoryFilter, pagination); + httpContext.Response.Headers["X-Pagination"] = result.Item2.ToString(); + + return result.Item1; + } + ); + + group.MapGet( + "categories/tree", + async (string? name, CategoryService categoryService) => + { + var result = await categoryService.GetAllCategories_Tree(name ?? ""); + return result.Select(x => Category.ToVM(x)).ToList(); + } + ); + + + // No Get + group.MapGet( + "category/{id}", + async (Guid id, CategoryService categoryService) => + { + return await categoryService.GetCategory(id); + } + ); + + + + // No Delete + group + .MapDelete( + "category/{id}", + async (Guid id, HttpContext httpContext, CategoryService categoryService) => + { + var userId = httpContext.User.Claims.First(x => x.Type == "uid").Value; + await categoryService.RemoveCategory(id); + } + ) + .RequireAuthorization(); + + // No Put + group.MapPut( + "category", + async ([FromBody] CategoryVM category, CategoryService categoryService) => + { + await categoryService.UpdateCategory(category); + } + ); + + + + // No Post + group.MapPost( + "category", + async ([FromBody] CategoryVM category, CategoryService categoryService) => + { + await categoryService.CreateCategory(category); + } + ); + + + + return group; + } +} diff --git a/ECommerce.API/Endpoints/v1/ProductMap.cs b/ECommerce.API/Endpoints/v1/ProductMap.cs new file mode 100644 index 0000000..e792b7f --- /dev/null +++ b/ECommerce.API/Endpoints/v1/ProductMap.cs @@ -0,0 +1,77 @@ + +using Commerce.Contracts.DTOs; +using Commerce.Core.Services; +using Generic.Contracts.Generics; +using Microsoft.AspNetCore.Mvc; + +namespace Commerce.Endpoints.v1; + +public static class ProductMap +{ + public static RouteGroupBuilder ProductEndpoints(this RouteGroupBuilder group) + { + + + // No Fetch + group.MapGet( + "products", + async ( + [AsParameters] ProductFilter productFilter, + [AsParameters] Pagination pagination, + HttpContext httpContext, + ProductService productService + ) => + { + var result = await productService.FetchProduct(productFilter, pagination); + httpContext.Response.Headers["X-Pagination"] = result.Item2.ToString(); + + return result.Item1; + } + ); + + + // No Get + group.MapGet( + "product/{id}", + async (Guid id, ProductService productService) => + { + return await productService.GetProduct(id); + } + ); + + + + // No Delete + group.MapDelete( + "product/{id}", + async (Guid id, HttpContext httpContext, ProductService productService) => + { + var userId = httpContext.User.Claims.First(x => x.Type == "uid").Value; + await productService.RemoveProduct(id); + } + ) + .RequireAuthorization(); + + // No Put + group.MapPut( + "product", + async ([FromBody] ProductVM product, ProductService productService) => + { + await productService.UpdateProduct(product); + } + ); + + + + // No Post + group.MapPost( + "product", + async ([FromBody] ProductVM product, ProductService productService) => + { + await productService.CreateProduct(product); + } + ); + + return group; + } +} diff --git a/ECommerce.API/Program.cs b/ECommerce.API/Program.cs new file mode 100644 index 0000000..d27d7a2 --- /dev/null +++ b/ECommerce.API/Program.cs @@ -0,0 +1,103 @@ +using Commerce.Config; +using Commerce.Endpoints; +using Commerce.Contracts.Options; +using Commerce.Domain; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +#pragma warning disable CS0618 +Npgsql.NpgsqlConnection.GlobalTypeMapper.EnableDynamicJson(); +#pragma warning restore CS0618 + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +// builder.Services.AddSwaggerGen(); + +builder.Services.Configure(builder.Configuration.GetSection("JWT")); + +// builder.Services.AddDbContext( +// options => options.(builder.Configuration["ConnectionStrings:maria"], ServerVersion.AutoDetect(builder.Configuration["ConnectionStrings:maria"]))); + +var connectionString = builder.Configuration.GetConnectionString("PostgreDB"); +var dataSourceBuilder = new Npgsql.NpgsqlDataSourceBuilder(connectionString); +dataSourceBuilder.EnableDynamicJson(); +var dataSource = dataSourceBuilder.Build(); + +builder.Services.AddDbContext(options => + options.UseNpgsql(dataSource) +); + + +// builder.Services.AddCustomCors(); + +builder.Services.RegFluentEmail(builder.Configuration); +builder.Services.AddCustomCors(); +builder.Services.AddSwag(); +builder.Services.Authenticate(builder.Configuration); + +builder.Services.Authorize(); +builder.Services.RegisterServices(); + +// builder.Services.AddAntiforgery(); +builder.WebHost.ConfigureKestrel(options => +{ + options.Limits.MaxRequestBodySize = 512 * 1024 * 1024; // 512 MB +}); + +// Configure form options for multipart body length +builder.Services.Configure(options => +{ + options.MultipartBodyLengthLimit = 512 * 1024 * 1024; // 512 MB +}); + +// builder.WebHost.UseUrls("http://*:80"); // Explicit HTTP binding + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); // Applies pending migrations +} + +// if (args.Contains("--migrate")) +// { +// using var scope = app.Services.CreateScope(); +// var db = scope.ServiceProvider.GetRequiredService(); +// db.Database.Migrate(); +// } + +app.UseRouting(); + +app.UseStaticFiles(); + +app.UseCors("default"); +app.UseAuthentication(); +app.UseAuthorization(); + +// Configure the HTTP request pipeline. +// if (app.Environment.IsDevelopment()) +// { +// app.UseSwagger(); +// app.UseSwaggerUI(); +// } + +// app.UseAntiforgery(); + +// app.UseHttpsRedirection(); +app.MapOpenApi(); +if (app.Environment.IsDevelopment()) +{ + app.UseSwag(); +} +app.MapAPIv1(); + +app.Run(); diff --git a/ECommerce.API/Properties/launchSettings.json b/ECommerce.API/Properties/launchSettings.json new file mode 100644 index 0000000..b35445b --- /dev/null +++ b/ECommerce.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:20864", + "sslPort": 44342 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:2001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7153;http://localhost:5141", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/ECommerce.API/api_output.log b/ECommerce.API/api_output.log new file mode 100644 index 0000000..00122df --- /dev/null +++ b/ECommerce.API/api_output.log @@ -0,0 +1,56 @@ +Using launch settings from /mnt/Dataa/Work/Programming/MainProgram/Backend/APIs/Commerce/Commerce.API/Properties/launchSettings.json... +Building... +info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (22ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] + SELECT "MigrationId", "ProductVersion" + FROM "__EFMigrationsHistory" + ORDER BY "MigrationId"; +info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (11ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] + CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") + ); +info: Microsoft.EntityFrameworkCore.Migrations[20411] + Acquiring an exclusive lock for migration application. See https://aka.ms/efcore-docs-migrations-lock for more information if this takes too long. +info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] + LOCK TABLE "__EFMigrationsHistory" IN ACCESS EXCLUSIVE MODE +info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] + SELECT "MigrationId", "ProductVersion" + FROM "__EFMigrationsHistory" + ORDER BY "MigrationId"; +info: Microsoft.EntityFrameworkCore.Migrations[20405] + No migrations were applied. The database is already up to date. +info: Microsoft.Hosting.Lifetime[14] + Now listening on: http://localhost:2001 +info: Microsoft.Hosting.Lifetime[0] + Application started. Press Ctrl+C to shut down. +info: Microsoft.Hosting.Lifetime[0] + Hosting environment: Development +info: Microsoft.Hosting.Lifetime[0] + Content root path: /mnt/Dataa/Work/Programming/MainProgram/Backend/APIs/Commerce/Commerce.API +info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] + SELECT count(*)::int + FROM "Products" AS p +info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (13ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] + SELECT p."Id", p."CategoryId", p."CategoryName", p."CodeName", p."Created", p."Description", p."Discount", p."IsBundle", p."Name", p."Photos", p."Price", p."TechnicalDetails" + FROM "Products" AS p + ORDER BY gen_random_uuid() +info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] + SELECT count(*)::int + FROM "Products" AS p +warn: Microsoft.EntityFrameworkCore.Query[10102] + The query uses a row limiting operator ('Skip'/'Take') without an 'OrderBy' operator. This may lead to unpredictable results. If the 'Distinct' operator is used after 'OrderBy', then make sure to use the 'OrderBy' operator after 'Distinct' as the ordering would otherwise get erased. +warn: Microsoft.EntityFrameworkCore.Query[10102] + The query uses a row limiting operator ('Skip'/'Take') without an 'OrderBy' operator. This may lead to unpredictable results. If the 'Distinct' operator is used after 'OrderBy', then make sure to use the 'OrderBy' operator after 'Distinct' as the ordering would otherwise get erased. +info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (14ms) [Parameters=[@__p_1='?' (DbType = Int32), @__p_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30'] + SELECT p."Id", p."CategoryId", p."CategoryName", p."CodeName", p."Created", p."Description", p."Discount", p."IsBundle", p."Name", p."Photos", p."Price", p."TechnicalDetails" + FROM "Products" AS p + LIMIT @__p_1 OFFSET @__p_0 diff --git a/ECommerce.API/appsettings.Development.json b/ECommerce.API/appsettings.Development.json new file mode 100644 index 0000000..a5f6872 --- /dev/null +++ b/ECommerce.API/appsettings.Development.json @@ -0,0 +1,32 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "maria": "UserID=root;Password=ama111AMA!!!;Host=localhost;Port=3306;Database=ECommerce;Protocol=TCP;", + "PostgreDB": "Host=localhost;Database=ECommerce;Username=test;Password=test;Port=5432;IncludeErrorDetail=true;" + }, + "JWT": { + "Key": "thisisasecretKeyIneedtoprovideenoughletterstomakeitwork", + "Issuer": "https://hello.com", + "Audience": "hello.com", + "DurationInMinutes": 10 + }, + "EmailTemplate": { + "EmailConfirmation": "placeHolder", + "ResetPassword": "placeHolder" + }, + "hostname": "placeHolder", + "MailSettings": { + "Mail": "merge1942@gmail.com", + "DisplayName": "Merge", + "UserName": "merge1942@gmail.com", + "Password": "ftyqwyqpqtixalxp", + "Host": "smtp.gmail.com", + "Port": 587 + } +} \ No newline at end of file diff --git a/ECommerce.API/appsettings.json b/ECommerce.API/appsettings.json new file mode 100644 index 0000000..64cfae7 --- /dev/null +++ b/ECommerce.API/appsettings.json @@ -0,0 +1,33 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "maria": "UserID=root;Password=ama111AMA!!!;Host=localhost;Port=3306;Database=ECommerce;Protocol=TCP;", + "PostgreDB": "Host=localhost;Database=ECommerce;Username=test;Password=test;Port=5432;" + }, + "Domain": "https://api.mass-tech.net", + "JWT": { + "Key": "thisisasecretKeyIneedtoprovideenoughletterstomakeitwork", + "Issuer": "https://hello.com", + "Audience": "hello.com", + "DurationInMinutes": 10 + }, + "EmailTemplate": { + "EmailConfirmation": "placeHolder", + "ResetPassword": "placeHolder" + }, + "hostname": "placeHolder", + "MailSettings": { + "Mail": "merge1942@gmail.com", + "DisplayName": "Merge", + "UserName": "merge1942@gmail.com", + "Password": "ftyqwyqpqtixalxp", + "Host": "smtp.gmail.com", + "Port": 587 + } +} \ No newline at end of file diff --git a/ECommerce.API/watch_output.log b/ECommerce.API/watch_output.log new file mode 100644 index 0000000..e69de29 diff --git a/ECommerce.Contracts/Constants/AppCon.cs b/ECommerce.Contracts/Constants/AppCon.cs new file mode 100755 index 0000000..11e49b6 --- /dev/null +++ b/ECommerce.Contracts/Constants/AppCon.cs @@ -0,0 +1,11 @@ +namespace Commerce.Contracts.Constants; + public static class AppCon + { + public static class ConnectionStrings + { + public const string SQLServer = "server=(localdb)\\MSSQLLocalDB;database=Ecommerce;Integrated Security=true"; + public const string MariaDB = "UserID=said;Password=aaa111!!!AAA;server=db;Port=3310;Database=ecommercedb;Protocol=TCP"; + public const string Postgresql = "Host=localhost;Database=ecommerce;Username=said1996;Password=a44781680;"; + } + } + diff --git a/ECommerce.Contracts/Constants/AppEnum.cs b/ECommerce.Contracts/Constants/AppEnum.cs new file mode 100755 index 0000000..27b2f2a --- /dev/null +++ b/ECommerce.Contracts/Constants/AppEnum.cs @@ -0,0 +1,63 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Commerce.Contracts.Constants; + public static class AppEnum + { + public enum OrderStatus + { + FillingCart, + // Paid, + InQueue, + + DeliveryInProgress, + Delivered, + + UserNotFound, + + Delayed, + // Reserved + } + + public enum QueryOrder + { + [Display(Name = "Popular")] + Name, + + Recent, + + Oldest, + + [Display(Name = "Lowest Price")] + Lowest, + + [Display(Name = "Highest Price")] + Highest + } + + public enum Roles + { + SuperAdmin, + Admin, + Moderator, + Standard + } + + public enum ShippingMethod + { + [Display(Name = "Standard shipping - $10.00")] NormalShipping, + [Display(Name = "Fast shipping - $50.00")] FastShipping, + + } + + public enum SalesOrPurchases + { + None, + Purchase, + Sales, + + } + } + + + diff --git a/ECommerce.Contracts/DTOs/CategoryFilter.cs b/ECommerce.Contracts/DTOs/CategoryFilter.cs new file mode 100644 index 0000000..d5ba0c8 --- /dev/null +++ b/ECommerce.Contracts/DTOs/CategoryFilter.cs @@ -0,0 +1,10 @@ +namespace Commerce.Contracts.DTOs; + +public class CategoryFilter +{ + public string? Name { get; set; } + public bool? IsBundle { get; set; } + public string? Language { get; set; } + + public bool? Random { get; set; } = false; +} diff --git a/ECommerce.Contracts/DTOs/CategoryVM.cs b/ECommerce.Contracts/DTOs/CategoryVM.cs new file mode 100644 index 0000000..8462af1 --- /dev/null +++ b/ECommerce.Contracts/DTOs/CategoryVM.cs @@ -0,0 +1,25 @@ +namespace Commerce.Contracts.DTOs; + +public class CategoryVM +{ + public Guid? Id { get; set; } + public string[]? Photos { get; set; } + public Guid? ParentCategoryId { get; set; } + public ICollection? ChildCategories { get; set; } + public bool IsBundle { get; set; } + + public List Translations { get; set; } = new(); +} + +public class CategoryTranslationVM +{ + public string Language { get; set; } = string.Empty; + public CategoryInfoVM Info { get; set; } = new(); +} + +public class CategoryInfoVM +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List TechnicalDetails { get; set; } = new(); +} diff --git a/ECommerce.Contracts/DTOs/ProductFilter.cs b/ECommerce.Contracts/DTOs/ProductFilter.cs new file mode 100644 index 0000000..e3db47c --- /dev/null +++ b/ECommerce.Contracts/DTOs/ProductFilter.cs @@ -0,0 +1,14 @@ +namespace Commerce.Contracts.DTOs; + +public class ProductFilter +{ + public string? Name { get; set; } + public double? Price { get; set; } + public double? Discount { get; set; } + public bool? IsBundle { get; set; } + public Guid? CategoryId { get; set; } + public string? Language { get; set; } + + public bool? Random { get; set; } = false; + +} diff --git a/ECommerce.Contracts/DTOs/ProductVM.cs b/ECommerce.Contracts/DTOs/ProductVM.cs new file mode 100644 index 0000000..3dc9173 --- /dev/null +++ b/ECommerce.Contracts/DTOs/ProductVM.cs @@ -0,0 +1,38 @@ +namespace Commerce.Contracts.DTOs; + +public class ProductVM +{ + public Guid? Id { get; set; } + public string[]? Photos { get; set; } + public double? Price { get; set; } + public double? Discount { get; set; } + public DateTime? Created { get; set; } + public string? CodeName { get; set; } + public string? CategoryName { get; set; } + public bool IsBundle { get; set; } + public ICollection? ChildProducts { get; set; } + public Guid? CategoryId { get; set; } + public Guid? ParentCategoryId { get; set; } + public ICollection? ChildCategories { get; set; } + + public List? Translations { get; set; } = new(); +} + +public class ProductTranslationVM +{ + public string Language { get; set; } = string.Empty; + public ProductInfoVM Info { get; set; } = new(); +} + +public class ProductInfoVM +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List TechnicalDetails { get; set; } = new(); +} + +public class TechnicalDetailVM +{ + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; +} diff --git a/ECommerce.Contracts/DTOs/TechnicalDetail.cs b/ECommerce.Contracts/DTOs/TechnicalDetail.cs new file mode 100644 index 0000000..c4a4537 --- /dev/null +++ b/ECommerce.Contracts/DTOs/TechnicalDetail.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Commerce.Contracts.DTOs; + +public class TechnicalDetail +{ + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; +} diff --git a/ECommerce.Contracts/ECommerce.Contracts.csproj b/ECommerce.Contracts/ECommerce.Contracts.csproj new file mode 100755 index 0000000..7e5b61a --- /dev/null +++ b/ECommerce.Contracts/ECommerce.Contracts.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + diff --git a/ECommerce.Contracts/Exceptions/NotFoundException.cs b/ECommerce.Contracts/Exceptions/NotFoundException.cs new file mode 100755 index 0000000..ebc5f04 --- /dev/null +++ b/ECommerce.Contracts/Exceptions/NotFoundException.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Commerce.Contracts.Exceptions; + public class NotFoundException : Exception + { + public NotFoundException(string message, Exception innerException) : base(message, innerException) + { + + } + } diff --git a/ECommerce.Contracts/Options/JWT.cs b/ECommerce.Contracts/Options/JWT.cs new file mode 100755 index 0000000..e856e07 --- /dev/null +++ b/ECommerce.Contracts/Options/JWT.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Commerce.Contracts.Options; + public class JWT + { + public string Key { get; set; } + public string Issuer { get; set; } + public string Audience { get; set; } + public double DurationInMinutes { get; set; } + } diff --git a/ECommerce.Contracts/Validators/CategoryVMValidator.cs b/ECommerce.Contracts/Validators/CategoryVMValidator.cs new file mode 100644 index 0000000..f9e9b30 --- /dev/null +++ b/ECommerce.Contracts/Validators/CategoryVMValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Commerce.Contracts.DTOs; + +namespace Commerce.Contracts.Validators; + +public class CategoryVMValidator : AbstractValidator +{ + public CategoryVMValidator() + { + RuleFor(x => x.Translations).NotEmpty().WithMessage("At least one translation is required"); + } +} diff --git a/ECommerce.Contracts/Validators/ProductVMValidator.cs b/ECommerce.Contracts/Validators/ProductVMValidator.cs new file mode 100644 index 0000000..2297062 --- /dev/null +++ b/ECommerce.Contracts/Validators/ProductVMValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Commerce.Contracts.DTOs; + +namespace Commerce.Contracts.Validators; + +public class ProductVMValidator : AbstractValidator +{ + public ProductVMValidator() + { + RuleFor(x => x.Translations).NotEmpty().WithMessage("At least one translation is required"); + RuleFor(x => x.Price).GreaterThan(0).When(x => x.Price.HasValue).WithMessage("Price must be greater than 0"); + RuleFor(x => x.CategoryId).NotEmpty().When(x => !x.IsBundle).WithMessage("Category is required for products"); + } +} diff --git a/ECommerce.Core.Tests/CategoryServiceTests.cs b/ECommerce.Core.Tests/CategoryServiceTests.cs new file mode 100644 index 0000000..40932ef --- /dev/null +++ b/ECommerce.Core.Tests/CategoryServiceTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Commerce.Contracts.DTOs; +using Commerce.Core.Services; +using Commerce.Domain; +using Commerce.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Commerce.Core.Tests; + +public class CategoryServiceTests +{ + private Context GetContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + return new Context(options); + } + + [Fact] + public async Task GetAllCategories_ShouldFilterByLocalizedName() + { + // Arrange + using var context = GetContext(); + await context.Database.EnsureCreatedAsync(); + var id1 = Guid.NewGuid(); + context.Categories.AddRange(new List + { + new Category { + Id = id1, + Translations = new List + { + new CategoryTranslation { Language = "en", Info = new CategoryInfo { Name = "Electronics", Description = "Desc" } }, + new CategoryTranslation { Language = "ar", Info = new CategoryInfo { Name = "إلكترونيات", Description = "Desc" } } + }, + Photos = Array.Empty() + }, + new Category { + Id = Guid.NewGuid(), + Translations = new List + { + new CategoryTranslation { Language = "en", Info = new CategoryInfo { Name = "Fashion", Description = "Desc" } } + }, + Photos = Array.Empty() + } + }); + await context.SaveChangesAsync(); + + var service = new CategoryService(context); + + // Act - Search for Arabic name + var resultsAr = await service.GetAllCategories("إلكترونيات"); + + // Act - Search for English name + var resultsEn = await service.GetAllCategories("Elec"); + + // Assert + Assert.Single(resultsAr); + Assert.Equal(id1, resultsAr.First().Id); + + Assert.Single(resultsEn); + Assert.Equal(id1, resultsEn.First().Id); + } +} diff --git a/ECommerce.Core.Tests/ECommerce.Core.Tests.csproj b/ECommerce.Core.Tests/ECommerce.Core.Tests.csproj new file mode 100644 index 0000000..9499a0b --- /dev/null +++ b/ECommerce.Core.Tests/ECommerce.Core.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ECommerce.Core.Tests/LocalizationMappingTests.cs b/ECommerce.Core.Tests/LocalizationMappingTests.cs new file mode 100644 index 0000000..dccb58d --- /dev/null +++ b/ECommerce.Core.Tests/LocalizationMappingTests.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Commerce.Contracts.DTOs; +using Commerce.Domain.Entities; +using Xunit; +using TechnicalDetail = Commerce.Domain.Entities.TechnicalDetail; + +namespace Commerce.Core.Tests; + +public class LocalizationMappingTests +{ + [Fact] + public void Product_ToVM_ShouldMapIndependentTechnicalDetails() + { + // Arrange + var product = new Product + { + Id = Guid.NewGuid(), + Translations = new List + { + new ProductTranslation + { + Language = "en", + Info = new ProductInfo + { + Name = "Product 1", + TechnicalDetails = new List { new TechnicalDetail { Key = "Color", Value = "Red" } } + } + }, + new ProductTranslation + { + Language = "ar", + Info = new ProductInfo + { + Name = "منتج 1", + TechnicalDetails = new List { new TechnicalDetail { Key = "اللون", Value = "أحمر" } } + } + } + } + }; + + // Act + var vm = Product.ToVM(product); + + // Assert + Assert.NotNull(vm.Translations); + Assert.Equal(2, vm.Translations.Count); + + var enTrans = vm.Translations.First(t => t.Language == "en"); + Assert.Single(enTrans.Info.TechnicalDetails); + Assert.Equal("Color", enTrans.Info.TechnicalDetails[0].Key); + Assert.Equal("Red", enTrans.Info.TechnicalDetails[0].Value); + + var arTrans = vm.Translations.First(t => t.Language == "ar"); + Assert.Single(arTrans.Info.TechnicalDetails); + Assert.Equal("اللون", arTrans.Info.TechnicalDetails[0].Key); + Assert.Equal("أحمر", arTrans.Info.TechnicalDetails[0].Value); + } + + [Fact] + public void Product_ToEntity_ShouldMapIndependentTechnicalDetails() + { + // Arrange + var vm = new ProductVM + { + Id = Guid.NewGuid(), + Translations = new List + { + new ProductTranslationVM + { + Language = "en", + Info = new ProductInfoVM + { + Name = "Product 1", + TechnicalDetails = new List { new TechnicalDetailVM { Key = "Weight", Value = "1kg" } } + } + } + } + }; + + // Act + var entity = Product.ToEntity(vm); + + // Assert + Assert.NotNull(entity.Translations); + Assert.Single(entity.Translations.First(t => t.Language == "en").Info.TechnicalDetails); + Assert.Equal("Weight", entity.Translations.First(t => t.Language == "en").Info.TechnicalDetails[0].Key); + Assert.Equal("1kg", entity.Translations.First(t => t.Language == "en").Info.TechnicalDetails[0].Value); + } + + [Fact] + public void Category_ToVM_ShouldMapIndependentTechnicalDetails() + { + // Arrange + var category = new Category + { + Id = Guid.NewGuid(), + Translations = new List + { + new CategoryTranslation + { + Language = "en", + Info = new CategoryInfo + { + Name = "Category 1", + TechnicalDetails = new List { new TechnicalDetail { Key = "Material", Value = "Leather" } } + } + } + } + }; + + // Act + var vm = Category.ToVM(category); + + // Assert + Assert.NotNull(vm.Translations); + Assert.Single(vm.Translations.First(t => t.Language == "en").Info.TechnicalDetails); + Assert.Equal("Material", vm.Translations.First(t => t.Language == "en").Info.TechnicalDetails[0].Key); + Assert.Equal("Leather", vm.Translations.First(t => t.Language == "en").Info.TechnicalDetails[0].Value); + } + + [Fact] + public void Category_ToEntity_ShouldMapIndependentTechnicalDetails() + { + // Arrange + var vm = new CategoryVM + { + Id = Guid.NewGuid(), + Translations = new List + { + new CategoryTranslationVM + { + Language = "en", + Info = new CategoryInfoVM + { + Name = "Category 1", + TechnicalDetails = new List { new TechnicalDetailVM { Key = "Power", Value = "100W" } } + } + } + } + }; + + // Act + var entity = Category.ToEntity(vm); + + // Assert + Assert.NotNull(entity.Translations); + Assert.Single(entity.Translations.First(t => t.Language == "en").Info.TechnicalDetails); + Assert.Equal("Power", entity.Translations.First(t => t.Language == "en").Info.TechnicalDetails[0].Key); + Assert.Equal("100W", entity.Translations.First(t => t.Language == "en").Info.TechnicalDetails[0].Value); + } +} diff --git a/ECommerce.Core.Tests/UnitTest1.cs b/ECommerce.Core.Tests/UnitTest1.cs new file mode 100644 index 0000000..3073aa3 --- /dev/null +++ b/ECommerce.Core.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Commerce.Core.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/ECommerce.Core/ECommerce.Core.csproj b/ECommerce.Core/ECommerce.Core.csproj new file mode 100755 index 0000000..d54334e --- /dev/null +++ b/ECommerce.Core/ECommerce.Core.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ECommerce.Core/Services/CategoryService.cs b/ECommerce.Core/Services/CategoryService.cs new file mode 100644 index 0000000..f602fb4 --- /dev/null +++ b/ECommerce.Core/Services/CategoryService.cs @@ -0,0 +1,236 @@ +using System; +using Commerce.Contracts.DTOs; +using Commerce.Domain; +using Commerce.Domain.Entities; +using Generic.Contracts.Generics; +using Generic.Services; +using Microsoft.EntityFrameworkCore; + +namespace Commerce.Core.Services; + +public class CategoryService +{ + + private readonly Context context; + + public CategoryService(Context context) + { + this.context = context; + } + + + + public async Task CreateCategory(CategoryVM categoryVM) + { + var category = Category.ToEntity(categoryVM); + await context.Categories.AddAsync(category); + await context.SaveChangesAsync(); + } + + + public async Task UpdateCategory(CategoryVM categoryVM) + { + if (categoryVM.Id == null) throw new ArgumentException("Category ID is required for update."); + + var existingCategory = await context.Categories + .FirstOrDefaultAsync(x => x.Id == categoryVM.Id); + + if (existingCategory == null) throw new KeyNotFoundException("Category not found."); + + // Update properties + existingCategory.Translations = categoryVM.Translations?.Select(t => new CategoryTranslation + { + Language = t.Language, + Info = new CategoryInfo + { + Name = t.Info.Name, + Description = t.Info.Description, + TechnicalDetails = t.Info.TechnicalDetails?.Select(td => new Commerce.Domain.Entities.TechnicalDetail { Key = td.Key, Value = td.Value }).ToList() ?? new() + } + }).ToList() ?? new(); + + existingCategory.Photos = categoryVM.Photos ?? Array.Empty(); + existingCategory.ParentCategoryId = categoryVM.ParentCategoryId; + existingCategory.IsBundle = categoryVM.IsBundle; + + await context.SaveChangesAsync(); + } + + + public async Task RemoveCategory(Guid id) + { + var category = await context.Categories.FindAsync(id); + if (category != null) context.Categories.Remove(category); + await context.SaveChangesAsync(); + } + + + + public async Task GetCategory(Guid id) + { + var category = await context.Categories.FindAsync(id); + return category != null ? Category.ToVM(category) : new CategoryVM(); + } + + + + public async Task<(List, int)> FetchCategory( + CategoryFilter categoryFilter, + Pagination pagination + ) + { + var query = context.Categories.AsQueryable(); + query = query.ApplyAdvancedFilter(categoryFilter); + // query = query.ApplySorting(sortBy); + if (categoryFilter.Random ?? false) + { + query = query.OrderBy(x => Guid.NewGuid()); + } + else + { + // query = query.ApplySorting(sortBy); + } + var count = query.Count(); + query = query.Paginate(pagination); + + var lang = categoryFilter.Language; + return (await query.Select(x => Category.ToVM(x, lang)).ToListAsync(), count); + } + + + + public async Task> GetAllCategories(string name = "") + { + + var categories = await context.Categories.ToListAsync(); + if (!string.IsNullOrEmpty(name)) + categories = categories.Where(x => x.Translations.Any(t => t.Info.Name.Contains(name, StringComparison.OrdinalIgnoreCase))).ToList(); + return categories; + + } + + + public async Task> GetAllCategories_Tree(string name = "") + { + + var cate = await context.Categories + .ToListAsync(); + + + if (!string.IsNullOrEmpty(name)) + { + cate = cate.Where(x => x.Translations.Any(t => t.Info.Name.Contains(name, StringComparison.OrdinalIgnoreCase))).ToList(); + } + + var lista = cate.Where(x => x.ParentCategoryId == null).ToList(); + Console.WriteLine($"{lista.Count}"); + + + foreach (var cat in lista) + { + cat.ChildCategories = NewMethod(cate, cat.Id); + } + + Console.WriteLine($"{lista.Count}"); + + return lista; + + + + // if (!string.IsNullOrEmpty(name)) + // categories = categories.Where(x => x.Name.Contains(name)); + + // var lista = await categories.ToListAsync(); + // return lista; + + } + + private static List NewMethod(List categories, Guid parent) + { + var toreturn = new List(); + + var children = categories.Where(x => x.ParentCategoryId == parent).ToList(); + + foreach (var item in children) + { + item.ChildCategories = NewMethod(categories, item.Id); + } + + return children; + } + + public async Task> GetChildCategories(Guid categoryId) + { + var categories = new List(); + + categories = await context.Categories.Where(x => x.ParentCategoryId == categoryId).ToListAsync(); + + if (categories.Count != 0) + { + foreach (var category in categories) + { + categories.AddRange(await GetChildCategories(categoryId)); + } + } + + return categories; + } + + public async Task> GetChildrenList(Guid categoryId) + { + return await context.Categories.Where(x => x.Id == categoryId) + .Include(x => x.ChildCategories) + .SelectMany(x => x.ChildCategories) + .ToListAsync(); + + + } + + + // public async Task> GetRandomCategories(int categoriesNeeded, int productsNeeded) + // { + // var count = await context.Categories.CountAsync(); + // var categories = new List(); + // var random = new Random(); + // var randoms = new List(); + // for (int i = 0; i < categoriesNeeded; i++) + // { + // var newRandom = random.Next(count); + // if (!randoms.Contains(newRandom)) + // { + // randoms.Add(newRandom); + // categories.Add(await context.Categories + // .Skip(random.Next(count)) + // .Take(1) + // .Include(x => x.Products) + // .FirstOrDefaultAsync() + // ); + // } + // } + + // return categories; + + // } + + // public async Task GetCategoryAsync(int id) + // { + // return await context.Categories.FirstOrDefaultAsync(c => c.Id == id); + // } + + // public async Task> GetCategoriesPaginated(Pagination pagination) + // { + // var query = context.Categories.AsQueryable(); + // query = query.Skip((pagination.CurrentPage - 1) * pagination.PageSize) + // .Take(pagination.PageSize); + // return await query.ToListAsync(); + // } + + // public async Task> GetRandomCategories(int count = 4) + // { + // return await context + // .Categorys.OrderBy(x => Guid.NewGuid()) + // .Take(count) + // .Select(x => Category.ToVM(x)) + // .ToListAsync(); + // } +} diff --git a/ECommerce.Core/Services/ProductService.cs b/ECommerce.Core/Services/ProductService.cs new file mode 100644 index 0000000..c98f56a --- /dev/null +++ b/ECommerce.Core/Services/ProductService.cs @@ -0,0 +1,138 @@ +using System; +using Commerce.Contracts.DTOs; +using Commerce.Domain; +using Commerce.Domain.Entities; +using Generic.Contracts.Generics; +using Generic.Services; +using Microsoft.EntityFrameworkCore; + +namespace Commerce.Core.Services; + +public class ProductService +{ + private readonly Context context; + + public ProductService(Context context) + { + this.context = context; + } + + public async Task CreateProduct(ProductVM productVM) + { + var product = Product.ToEntity(productVM); + + if (productVM.ChildProducts?.Any() == true) + { + var childIds = productVM.ChildProducts.Select(x => x.Id).ToList(); + var existingChildren = await context.Products.Where(x => childIds.Contains(x.Id)).ToListAsync(); + product.ChildProducts = existingChildren; + } + + await context.Products.AddAsync(product); + await context.SaveChangesAsync(); + } + + public async Task UpdateProduct(ProductVM productVM) + { + if (productVM.Id == null) throw new ArgumentException("Product ID is required for update."); + + var existingProduct = await context.Products + .Include(x => x.ChildProducts) + .FirstOrDefaultAsync(x => x.Id == productVM.Id); + + if (existingProduct == null) throw new KeyNotFoundException("Product not found."); + + // Update properties + existingProduct.Translations = productVM.Translations?.Select(t => new ProductTranslation + { + Language = t.Language, + Info = new ProductInfo + { + Name = t.Info.Name, + Description = t.Info.Description, + TechnicalDetails = t.Info.TechnicalDetails?.Select(td => new Commerce.Domain.Entities.TechnicalDetail { Key = td.Key, Value = td.Value }).ToList() ?? new() + } + }).ToList() ?? new(); + + existingProduct.Photos = productVM.Photos ?? Array.Empty(); + existingProduct.Price = productVM.Price ?? 0; + existingProduct.Discount = productVM.Discount ?? 0; + existingProduct.CodeName = productVM.CodeName ?? string.Empty; + existingProduct.CategoryId = productVM.CategoryId ?? Guid.Empty; + existingProduct.IsBundle = productVM.IsBundle; + + // Update ChildProducts collection + if (productVM.IsBundle) + { + var targetChildIds = productVM.ChildProducts?.Select(x => x.Id ?? Guid.Empty).ToList() ?? new List(); + + // Remove items no longer in the list + var itemsToRemove = existingProduct.ChildProducts.Where(x => !targetChildIds.Contains(x.Id)).ToList(); + foreach (var item in itemsToRemove) existingProduct.ChildProducts.Remove(item); + + // Add new items + var currentChildIds = existingProduct.ChildProducts.Select(x => x.Id).ToList(); + var idsToAdd = targetChildIds.Where(id => !currentChildIds.Contains(id)).ToList(); + + if (idsToAdd.Any()) + { + var newChildren = await context.Products.Where(x => idsToAdd.Contains(x.Id)).ToListAsync(); + foreach (var child in newChildren) existingProduct.ChildProducts.Add(child); + } + } + else + { + existingProduct.ChildProducts.Clear(); + } + + await context.SaveChangesAsync(); + } + + public async Task RemoveProduct(Guid id) + { + var product = await context.Products.FindAsync(id); + if (product != null) context.Products.Remove(product); + await context.SaveChangesAsync(); + } + + public async Task GetProduct(Guid id) + { + var product = await context.Products + .Include(x => x.ChildProducts) + .FirstOrDefaultAsync(x => x.Id == id); + + return product != null ? Product.ToVM(product) : new ProductVM(); + } + + public async Task<(List, int)> FetchProduct( + ProductFilter productFilter, + Pagination pagination) + { + var query = context.Products + .Include(x => x.ChildProducts) + .AsQueryable(); + query = query.ApplyAdvancedFilter(productFilter); + // query = query.ApplySorting(sortBy); + if (productFilter.Random ?? false) + { + query = query.OrderBy(x => Guid.NewGuid()); + } + else + { + // query = query.ApplySorting(sortBy); + } + var count = query.Count(); + query = query.Paginate(pagination); + var lang = productFilter.Language; + return (await query.Select(x => Product.ToVM(x, lang)).ToListAsync(), count); + } + + // public async Task> GetRandomProducts(int count = 6) + // { + // return await context + // .Products.OrderBy(x => Guid.NewGuid()) + // .Take(count) + // .Select(x => Product.ToVM(x)) + // .ToListAsync(); + // } +} diff --git a/ECommerce.Core/Utility/FilterExtension.cs b/ECommerce.Core/Utility/FilterExtension.cs new file mode 100644 index 0000000..9b0f959 --- /dev/null +++ b/ECommerce.Core/Utility/FilterExtension.cs @@ -0,0 +1,126 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Linq.Expressions; +// using System.Reflection; +// using System.Threading.Tasks; + +// namespace Commerce.Core.Utility; + +// public static class FilterExtension +// { +// public static IQueryable ApplyFilter(this IQueryable query, TFilter filter) +// where TFilter : class +// { +// if (filter == null) +// return query; + +// var parameter = Expression.Parameter(typeof(T), "x"); +// Expression finalExpression = null; + +// var filterProperties = typeof(TFilter) +// .GetProperties() +// .Where(p => p.GetValue(filter) != null); + +// foreach (var filterProperty in filterProperties) +// { +// var filterValue = filterProperty.GetValue(filter); +// if (filterValue == null) +// continue; + +// var propertyName = filterProperty.Name; +// PropertyInfo entityProperty; + +// Expression propertyExpression = null; +// Expression comparison = null; + +// // Handle special suffix cases +// if (propertyName.EndsWith("_Start") || propertyName.EndsWith("_Min")) +// { +// entityProperty = typeof(T).GetProperty( +// propertyName.Substring(0, propertyName.LastIndexOf('_')) +// ); +// if (entityProperty != null) +// { +// propertyExpression = Expression.Property(parameter, entityProperty); +// comparison = Expression.GreaterThanOrEqual( +// propertyExpression, +// Expression.Constant(filterValue, entityProperty.PropertyType) +// ); +// } +// } +// else if (propertyName.EndsWith("_End") || propertyName.EndsWith("_Max")) +// { +// entityProperty = typeof(T).GetProperty( +// propertyName.Substring(0, propertyName.LastIndexOf('_')) +// ); +// if (entityProperty != null) +// { +// propertyExpression = Expression.Property(parameter, entityProperty); +// comparison = Expression.LessThanOrEqual( +// propertyExpression, +// Expression.Constant(filterValue, entityProperty.PropertyType) +// ); +// } +// } +// else if (propertyName.EndsWith("Name", StringComparison.OrdinalIgnoreCase)) +// { +// entityProperty = typeof(T).GetProperty(propertyName); +// if (entityProperty != null && entityProperty.PropertyType == typeof(string)) +// { +// propertyExpression = Expression.Property(parameter, entityProperty); + +// // For string Contains operation +// var containsMethod = typeof(string).GetMethod( +// "Contains", +// new[] { typeof(string) } +// ); +// var filterValueString = filterValue.ToString(); + +// comparison = Expression.Call( +// propertyExpression, +// containsMethod, +// Expression.Constant(filterValueString, typeof(string)) +// ); +// } +// else if (entityProperty != null) +// { +// // Fall back to equality comparison if it's not a string +// propertyExpression = Expression.Property(parameter, entityProperty); +// comparison = Expression.Equal( +// propertyExpression, +// Expression.Constant(filterValue, entityProperty.PropertyType) +// ); +// } +// } +// else +// { +// entityProperty = typeof(T).GetProperty(propertyName); +// if (entityProperty != null) +// { +// propertyExpression = Expression.Property(parameter, entityProperty); +// comparison = Expression.Equal( +// propertyExpression, +// Expression.Constant(filterValue, entityProperty.PropertyType) +// ); +// } +// } + +// if (comparison != null) +// { +// finalExpression = +// finalExpression == null +// ? comparison +// : Expression.AndAlso(finalExpression, comparison); +// } +// } + +// if (finalExpression != null) +// { +// var lambda = Expression.Lambda>(finalExpression, parameter); +// query = query.Where(lambda); +// } + +// return query; +// } +// } diff --git a/ECommerce.Core/Utility/IEnumerableExtensions.cs b/ECommerce.Core/Utility/IEnumerableExtensions.cs new file mode 100644 index 0000000..59b093e --- /dev/null +++ b/ECommerce.Core/Utility/IEnumerableExtensions.cs @@ -0,0 +1,27 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading.Tasks; + +// namespace Commerce.Core.Utility; +// public static class IEnumerableExtensions +// { +// public static IEnumerable> Batch(this IEnumerable source, int batchSize) +// { +// var batch = new List(batchSize); +// foreach (var item in source) +// { +// batch.Add(item); +// if (batch.Count == batchSize) +// { +// yield return batch; +// batch = new List(batchSize); +// } +// } +// if (batch.Count > 0) +// { +// yield return batch; +// } +// } +// } + diff --git a/ECommerce.Core/Utility/Pagination.cs b/ECommerce.Core/Utility/Pagination.cs new file mode 100755 index 0000000..71b31ee --- /dev/null +++ b/ECommerce.Core/Utility/Pagination.cs @@ -0,0 +1,19 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading.Tasks; +// using Contracts.DTOs; +// using Contracts.DTOs.Generic; + +// namespace Commerce.Core.Utility; +// public static class Pagination +// where T : class +// { +// public static IQueryable Paginate(IQueryable query, Pagination pagination) +// { +// return query +// .Skip((pagination.CurrentPage.Value - 1) * pagination.PageSize.Value) +// .Take(pagination.PageSize.Value); +// } +// } + diff --git a/ECommerce.Core/Utility/PaginationExtension.cs b/ECommerce.Core/Utility/PaginationExtension.cs new file mode 100644 index 0000000..5240b09 --- /dev/null +++ b/ECommerce.Core/Utility/PaginationExtension.cs @@ -0,0 +1,21 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading.Tasks; +// using Contracts.DTOs; +// using Contracts.DTOs.Generic; + +// namespace Commerce.Core.Utility; +// public static class PaginationExtension +// { +// public static IQueryable Paginate(this IQueryable query, Pagination pagination) +// { +// if (pagination.CurrentPage.HasValue && pagination.PageSize.HasValue) +// return query +// .Skip((pagination.CurrentPage.Value - 1) * pagination.PageSize.Value) +// .Take(pagination.PageSize.Value); +// else +// return query; +// } +// } + diff --git a/ECommerce.Core/Utility/QueryableExtensions.cs b/ECommerce.Core/Utility/QueryableExtensions.cs new file mode 100644 index 0000000..0aff868 --- /dev/null +++ b/ECommerce.Core/Utility/QueryableExtensions.cs @@ -0,0 +1,31 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Linq.Expressions; +// using System.Threading.Tasks; +// using Contracts.DTOs.Generic; + +// namespace Commerce.Core.Utility; + +// public static class QueryableExtensions +// { +// public static IQueryable ApplySorting(this IQueryable source, SortModel sortBy) +// { +// // Parameter for lambda expression, e.g., "p" in "p => p.Property" +// var parameter = Expression.Parameter(typeof(T), "p"); + +// // Access the property on the parameter, e.g., "p.Property" +// var property = Expression.Property(parameter, sortBy.SortBy); + +// // Cast property access to an object (boxing value types) +// var converted = Expression.Convert(property, typeof(object)); + +// // Create the lambda expression, e.g., "p => (object)p.Property" +// var keySelector = Expression.Lambda>(converted, parameter); + +// // Apply OrderBy or OrderByDescending based on the ascending flag +// return sortBy.SortDirection == SortDir.Ascending +// ? source.OrderBy(keySelector) +// : source.OrderByDescending(keySelector); +// } +// } diff --git a/ECommerce.Core/Utility/SortExt.cs b/ECommerce.Core/Utility/SortExt.cs new file mode 100644 index 0000000..fd38221 --- /dev/null +++ b/ECommerce.Core/Utility/SortExt.cs @@ -0,0 +1,45 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Linq.Expressions; +// using System.Reflection; +// using System.Threading.Tasks; +// using Contracts.DTOs.Generic; +// using Microsoft.EntityFrameworkCore; + +// namespace Commerce.Core.Utility; + +// public static class SortExt +// { +// public static IQueryable ApplySort(this IQueryable query, SortModel sortModel) +// { +// if (string.IsNullOrWhiteSpace(sortModel?.SortBy)) +// return query; + +// // Get the property info +// var propertyInfo = typeof(T).GetProperty( +// sortModel.SortBy, +// BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance +// ); + +// if (propertyInfo == null) +// throw new ArgumentException( +// $"No property '{sortModel.SortBy}' on type '{typeof(T).Name}'" +// ); + +// // Create x => x.PropertyName +// var parameter = Expression.Parameter(typeof(T), "x"); +// var property = Expression.Property(parameter, propertyInfo); +// var lambda = Expression.Lambda(property, parameter); + +// // Call OrderBy or OrderByDescending dynamically +// string methodName = sortModel.SortBy?.ToLower() == "desc" ? "OrderByDescending" : "OrderBy"; +// var result = typeof(Queryable) +// .GetMethods() +// .First(m => m.Name == methodName && m.GetParameters().Length == 2) +// .MakeGenericMethod(typeof(T), propertyInfo.PropertyType) +// .Invoke(null, new object[] { query, lambda }); + +// return (IQueryable)result; +// } +// } diff --git a/ECommerce.Domain/Context.cs b/ECommerce.Domain/Context.cs new file mode 100755 index 0000000..456b80a --- /dev/null +++ b/ECommerce.Domain/Context.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using Bogus; +using Commerce.Contracts.DTOs; +using Commerce.Domain.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Commerce.Domain; + +public class Context : DbContext +{ + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + + public Context(DbContextOptions options) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + if (Database.IsNpgsql()) + { + modelBuilder.Entity(entity => + { + entity.OwnsMany(e => e.Translations, b => + { + b.ToJson(); + b.OwnsOne(t => t.Info, i => + { + i.OwnsMany(ti => ti.TechnicalDetails); + }); + }); + }); + + modelBuilder.Entity(entity => + { + entity.OwnsMany(e => e.Translations, b => + { + b.ToJson(); + b.OwnsOne(t => t.Info, i => + { + i.OwnsMany(ti => ti.TechnicalDetails); + }); + }); + }); + } + else + { + var productTranslationsConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter, string>( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => JsonSerializer.Deserialize>(v, (JsonSerializerOptions?)null) ?? new List()); + + var categoryTranslationsConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter, string>( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => JsonSerializer.Deserialize>(v, (JsonSerializerOptions?)null) ?? new List()); + + modelBuilder.Entity(entity => + { + entity.Property(e => e.Translations).HasConversion(productTranslationsConverter); + }); + + modelBuilder.Entity(entity => + { + entity.Property(e => e.Translations).HasConversion(categoryTranslationsConverter); + }); + } + } +} diff --git a/ECommerce.Domain/ECommerce.Domain.csproj b/ECommerce.Domain/ECommerce.Domain.csproj new file mode 100755 index 0000000..7c4d02a --- /dev/null +++ b/ECommerce.Domain/ECommerce.Domain.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + \ No newline at end of file diff --git a/ECommerce.Domain/Entities/Category.cs b/ECommerce.Domain/Entities/Category.cs new file mode 100644 index 0000000..0cbe77b --- /dev/null +++ b/ECommerce.Domain/Entities/Category.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Commerce.Contracts.DTOs; + +namespace Commerce.Domain.Entities; + +public class Category +{ + public Guid Id { get; set; } + public string[] Photos { get; set; } = Array.Empty(); + public Guid? ParentCategoryId { get; set; } + public Category? ParentCategory { get; set; } + public ICollection ChildCategories { get; set; } = new List(); + public bool IsBundle { get; set; } + + // New nested structure for JSONB + public List Translations { get; set; } = new(); + + public static CategoryVM ToVM(Category category, string? language = null) + { + var translations = category.Translations?.ToList() ?? new List(); + List selectedTranslations; + + if (string.IsNullOrEmpty(language)) + { + selectedTranslations = translations + .Select(t => new CategoryTranslationVM + { + Language = t.Language, + Info = new CategoryInfoVM + { + Name = t.Info.Name, + Description = t.Info.Description, + TechnicalDetails = t.Info.TechnicalDetails?.Select(td => new TechnicalDetailVM { Key = td.Key, Value = td.Value }).ToList() ?? new() + } + }).ToList(); + } + else + { + var matchedTranslation = translations + .FirstOrDefault(t => t.Language == language) + ?? translations.FirstOrDefault(t => t.Language == "en"); + + selectedTranslations = matchedTranslation != null + ? new List + { + new CategoryTranslationVM + { + Language = matchedTranslation.Language, + Info = new CategoryInfoVM + { + Name = matchedTranslation.Info.Name, + Description = matchedTranslation.Info.Description, + TechnicalDetails = matchedTranslation.Info.TechnicalDetails?.Select(td => new TechnicalDetailVM { Key = td.Key, Value = td.Value }).ToList() ?? new() + } + } + } + : new List(); + } + + return new CategoryVM + { + Id = category.Id, + Photos = category.Photos, + ParentCategoryId = category.ParentCategoryId, + IsBundle = category.IsBundle, + Translations = selectedTranslations, + ChildCategories = category.ChildCategories?.Select(x => Category.ToVM(x, language)).ToList() ?? new List(), + }; + } + + public static Category ToEntity(CategoryVM categoryVM) + { + return new Category + { + Id = categoryVM.Id ?? Guid.NewGuid(), + Photos = categoryVM.Photos ?? Array.Empty(), + ParentCategoryId = categoryVM.ParentCategoryId, + IsBundle = categoryVM.IsBundle, + Translations = categoryVM.Translations?.Select(t => new CategoryTranslation + { + Language = t.Language, + Info = new CategoryInfo + { + Name = t.Info.Name, + Description = t.Info.Description, + TechnicalDetails = t.Info.TechnicalDetails?.Select(td => new TechnicalDetail { Key = td.Key, Value = td.Value }).ToList() ?? new() + } + }).ToList() ?? new(), + ChildCategories = categoryVM.ChildCategories?.Select(x => Category.ToEntity(x)).ToList() ?? new List(), + }; + } +} + +public class CategoryTranslation +{ + public string Language { get; set; } = string.Empty; + public CategoryInfo Info { get; set; } = new(); +} + +public class CategoryInfo +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List TechnicalDetails { get; set; } = new(); +} diff --git a/ECommerce.Domain/Entities/Product.cs b/ECommerce.Domain/Entities/Product.cs new file mode 100644 index 0000000..37c0b71 --- /dev/null +++ b/ECommerce.Domain/Entities/Product.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations.Schema; +using Commerce.Contracts.DTOs; + +namespace Commerce.Domain.Entities; + +public class Product +{ + public Guid Id { get; set; } + public double Price { get; set; } + public double Discount { get; set; } + public DateTime Created { get; set; } + public string CodeName { get; set; } = string.Empty; + public bool IsBundle { get; set; } + public string? CategoryName { get; set; } + public Guid? CategoryId { get; set; } + + public string[] Photos { get; set; } = Array.Empty(); + + [ForeignKey("CategoryId")] + public virtual Category Category { get; set; } = null!; + + public virtual List ChildProducts { get; set; } = new(); + public virtual List? ParentBundles { get; set; } + + // New nested structure for JSONB + public List Translations { get; set; } = new(); + + public static ProductVM ToVM(Product product, string? language = null) + { + var translations = product.Translations?.ToList() ?? new List(); + List selectedTranslations; + + if (string.IsNullOrEmpty(language)) + { + selectedTranslations = translations + .Select(t => new ProductTranslationVM + { + Language = t.Language, + Info = new ProductInfoVM + { + Name = t.Info.Name, + Description = t.Info.Description, + TechnicalDetails = t.Info.TechnicalDetails?.Select(td => new TechnicalDetailVM { Key = td.Key, Value = td.Value }).ToList() ?? new() + } + }).ToList(); + } + else + { + var matchedTranslation = translations + .FirstOrDefault(t => t.Language == language) + ?? translations.FirstOrDefault(t => t.Language == "en"); + + selectedTranslations = matchedTranslation != null + ? new List + { + new ProductTranslationVM + { + Language = matchedTranslation.Language, + Info = new ProductInfoVM + { + Name = matchedTranslation.Info.Name, + Description = matchedTranslation.Info.Description, + TechnicalDetails = matchedTranslation.Info.TechnicalDetails?.Select(td => new TechnicalDetailVM { Key = td.Key, Value = td.Value }).ToList() ?? new() + } + } + } + : new List(); + } + + return new ProductVM + { + Id = product.Id, + Price = product.Price, + Discount = product.Discount, + Created = product.Created, + CodeName = product.CodeName, + CategoryName = product.CategoryName, + IsBundle = product.IsBundle, + CategoryId = product.CategoryId, + Photos = product.Photos, + Translations = selectedTranslations, + ChildProducts = product.ChildProducts?.Select(x => Product.ToVM(x, language)).ToList() ?? new List(), + }; + } + + public static Product ToEntity(ProductVM productVM) + { + return new Product + { + Id = productVM.Id ?? Guid.NewGuid(), + Price = productVM.Price ?? 0, + Discount = productVM.Discount ?? 0, + Created = productVM.Created ?? DateTime.UtcNow, + CodeName = productVM.CodeName ?? string.Empty, + CategoryName = productVM.CategoryName, + IsBundle = productVM.IsBundle, + CategoryId = productVM.CategoryId ?? Guid.Empty, + Photos = productVM.Photos ?? Array.Empty(), + Translations = productVM.Translations?.Select(t => new ProductTranslation + { + Language = t.Language, + Info = new ProductInfo + { + Name = t.Info.Name, + Description = t.Info.Description, + TechnicalDetails = t.Info.TechnicalDetails?.Select(td => new TechnicalDetail { Key = td.Key, Value = td.Value }).ToList() ?? new() + } + }).ToList() ?? new(), + ChildProducts = new List(), + }; + } +} + +public class ProductTranslation +{ + public string Language { get; set; } = string.Empty; + public ProductInfo Info { get; set; } = new(); +} + +public class ProductInfo +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List TechnicalDetails { get; set; } = new(); +} + +public class TechnicalDetail +{ + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; +} diff --git a/ECommerce.Domain/Migrations/20260105142147_InitialCreate.Designer.cs b/ECommerce.Domain/Migrations/20260105142147_InitialCreate.Designer.cs new file mode 100644 index 0000000..4650979 --- /dev/null +++ b/ECommerce.Domain/Migrations/20260105142147_InitialCreate.Designer.cs @@ -0,0 +1,153 @@ +// +using System; +using System.Collections.Generic; +using Commerce.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20260105142147_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("Categorys"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CategoryName") + .HasColumnType("text"); + + b.Property("CodeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Discount") + .HasColumnType("double precision"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Price") + .HasColumnType("double precision"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property>("TechnicalDetails") + .IsRequired() + .HasColumnType("hstore"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany("ChildProducts") + .HasForeignKey("ProductId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Navigation("ChildCategories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Navigation("ChildProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.Domain/Migrations/20260105142147_InitialCreate.cs b/ECommerce.Domain/Migrations/20260105142147_InitialCreate.cs new file mode 100644 index 0000000..1e3352b --- /dev/null +++ b/ECommerce.Domain/Migrations/20260105142147_InitialCreate.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:hstore", ",,"); + + migrationBuilder.CreateTable( + name: "Categorys", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + Photos = table.Column(type: "text[]", nullable: false), + ParentCategoryId = table.Column(type: "uuid", nullable: true), + IsBundle = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categorys", x => x.Id); + table.ForeignKey( + name: "FK_Categorys_Categorys_ParentCategoryId", + column: x => x.ParentCategoryId, + principalTable: "Categorys", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + Photos = table.Column(type: "text[]", nullable: false), + TechnicalDetails = table.Column>(type: "hstore", nullable: false), + Price = table.Column(type: "double precision", nullable: false), + Discount = table.Column(type: "double precision", nullable: false), + Created = table.Column(type: "timestamp with time zone", nullable: false), + CodeName = table.Column(type: "text", nullable: false), + IsBundle = table.Column(type: "boolean", nullable: false), + CategoryName = table.Column(type: "text", nullable: true), + CategoryId = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + table.ForeignKey( + name: "FK_Products_Categorys_CategoryId", + column: x => x.CategoryId, + principalTable: "Categorys", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Products_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Categorys_ParentCategoryId", + table: "Categorys", + column: "ParentCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Products_CategoryId", + table: "Products", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Products_ProductId", + table: "Products", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Categorys"); + } + } +} diff --git a/ECommerce.Domain/Migrations/20260110093153_ModelUpdate.Designer.cs b/ECommerce.Domain/Migrations/20260110093153_ModelUpdate.Designer.cs new file mode 100644 index 0000000..529d03b --- /dev/null +++ b/ECommerce.Domain/Migrations/20260110093153_ModelUpdate.Designer.cs @@ -0,0 +1,153 @@ +// +using System; +using System.Collections.Generic; +using Commerce.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20260110093153_ModelUpdate")] + partial class ModelUpdate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CategoryName") + .HasColumnType("text"); + + b.Property("CodeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Discount") + .HasColumnType("double precision"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Price") + .HasColumnType("double precision"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property>("TechnicalDetails") + .IsRequired() + .HasColumnType("hstore"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany("ChildProducts") + .HasForeignKey("ProductId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Navigation("ChildCategories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Navigation("ChildProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.Domain/Migrations/20260110093153_ModelUpdate.cs b/ECommerce.Domain/Migrations/20260110093153_ModelUpdate.cs new file mode 100644 index 0000000..324d3f9 --- /dev/null +++ b/ECommerce.Domain/Migrations/20260110093153_ModelUpdate.cs @@ -0,0 +1,100 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + /// + public partial class ModelUpdate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Categorys_Categorys_ParentCategoryId", + table: "Categorys"); + + migrationBuilder.DropForeignKey( + name: "FK_Products_Categorys_CategoryId", + table: "Products"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Categorys", + table: "Categorys"); + + migrationBuilder.RenameTable( + name: "Categorys", + newName: "Categories"); + + migrationBuilder.RenameIndex( + name: "IX_Categorys_ParentCategoryId", + table: "Categories", + newName: "IX_Categories_ParentCategoryId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Categories", + table: "Categories", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Categories_Categories_ParentCategoryId", + table: "Categories", + column: "ParentCategoryId", + principalTable: "Categories", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Products_Categories_CategoryId", + table: "Products", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Categories_Categories_ParentCategoryId", + table: "Categories"); + + migrationBuilder.DropForeignKey( + name: "FK_Products_Categories_CategoryId", + table: "Products"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Categories", + table: "Categories"); + + migrationBuilder.RenameTable( + name: "Categories", + newName: "Categorys"); + + migrationBuilder.RenameIndex( + name: "IX_Categories_ParentCategoryId", + table: "Categorys", + newName: "IX_Categorys_ParentCategoryId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Categorys", + table: "Categorys", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Categorys_Categorys_ParentCategoryId", + table: "Categorys", + column: "ParentCategoryId", + principalTable: "Categorys", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Products_Categorys_CategoryId", + table: "Products", + column: "CategoryId", + principalTable: "Categorys", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/ECommerce.Domain/Migrations/20260119105451_MultiLanguageSupport.Designer.cs b/ECommerce.Domain/Migrations/20260119105451_MultiLanguageSupport.Designer.cs new file mode 100644 index 0000000..e2ef543 --- /dev/null +++ b/ECommerce.Domain/Migrations/20260119105451_MultiLanguageSupport.Designer.cs @@ -0,0 +1,152 @@ +// +using System; +using System.Collections.Generic; +using Commerce.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20260119105451_MultiLanguageSupport")] + partial class MultiLanguageSupport + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property>("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property>("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CategoryName") + .HasColumnType("text"); + + b.Property("CodeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property>("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Discount") + .HasColumnType("double precision"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property>("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Price") + .HasColumnType("double precision"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property>("TechnicalDetails") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany("ChildProducts") + .HasForeignKey("ProductId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Navigation("ChildCategories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Navigation("ChildProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.Domain/Migrations/20260119105451_MultiLanguageSupport.cs b/ECommerce.Domain/Migrations/20260119105451_MultiLanguageSupport.cs new file mode 100644 index 0000000..b463c28 --- /dev/null +++ b/ECommerce.Domain/Migrations/20260119105451_MultiLanguageSupport.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + /// + public partial class MultiLanguageSupport : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .OldAnnotation("Npgsql:PostgresExtension:hstore", ",,"); + + // Use Sql for all column type changes to include the USING clause + migrationBuilder.Sql("ALTER TABLE \"Products\" ALTER COLUMN \"TechnicalDetails\" TYPE jsonb USING \"TechnicalDetails\"::jsonb;"); + migrationBuilder.Sql("ALTER TABLE \"Products\" ALTER COLUMN \"Name\" TYPE jsonb USING jsonb_build_object('en', \"Name\");"); + migrationBuilder.Sql("ALTER TABLE \"Products\" ALTER COLUMN \"Description\" TYPE jsonb USING jsonb_build_object('en', \"Description\");"); + + migrationBuilder.Sql("ALTER TABLE \"Categories\" ALTER COLUMN \"Name\" TYPE jsonb USING jsonb_build_object('en', \"Name\");"); + migrationBuilder.Sql("ALTER TABLE \"Categories\" ALTER COLUMN \"Description\" TYPE jsonb USING jsonb_build_object('en', \"Description\");"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:hstore", ",,"); + + migrationBuilder.AlterColumn>( + name: "TechnicalDetails", + table: "Products", + type: "hstore", + nullable: false, + oldClrType: typeof(Dictionary), + oldType: "jsonb"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Products", + type: "text", + nullable: false, + oldClrType: typeof(Dictionary), + oldType: "jsonb"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "Products", + type: "text", + nullable: false, + oldClrType: typeof(Dictionary), + oldType: "jsonb"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Categories", + type: "text", + nullable: false, + oldClrType: typeof(Dictionary), + oldType: "jsonb"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "Categories", + type: "text", + nullable: false, + oldClrType: typeof(Dictionary), + oldType: "jsonb"); + } + } +} diff --git a/ECommerce.Domain/Migrations/20260119121446_TechnicalDetailsForCategories.Designer.cs b/ECommerce.Domain/Migrations/20260119121446_TechnicalDetailsForCategories.Designer.cs new file mode 100644 index 0000000..46d5f5e --- /dev/null +++ b/ECommerce.Domain/Migrations/20260119121446_TechnicalDetailsForCategories.Designer.cs @@ -0,0 +1,171 @@ +// +using System; +using System.Collections.Generic; +using Commerce.Contracts.DTOs; +using Commerce.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20260119121446_TechnicalDetailsForCategories")] + partial class TechnicalDetailsForCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property>("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property>("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property>>("TechnicalDetails") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CategoryName") + .HasColumnType("text"); + + b.Property("CodeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property>("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Discount") + .HasColumnType("double precision"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property>("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Price") + .HasColumnType("double precision"); + + b.Property>>("TechnicalDetails") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.Property("ChildProductsId") + .HasColumnType("uuid"); + + b.Property("ParentBundlesId") + .HasColumnType("uuid"); + + b.HasKey("ChildProductsId", "ParentBundlesId"); + + b.HasIndex("ParentBundlesId"); + + b.ToTable("ProductProduct"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ChildProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ParentBundlesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Navigation("ChildCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.Domain/Migrations/20260119121446_TechnicalDetailsForCategories.cs b/ECommerce.Domain/Migrations/20260119121446_TechnicalDetailsForCategories.cs new file mode 100644 index 0000000..8b3763a --- /dev/null +++ b/ECommerce.Domain/Migrations/20260119121446_TechnicalDetailsForCategories.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using Commerce.Contracts.DTOs; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + /// + public partial class TechnicalDetailsForCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Products_Categories_CategoryId", + table: "Products"); + + migrationBuilder.DropForeignKey( + name: "FK_Products_Products_ProductId", + table: "Products"); + + migrationBuilder.DropIndex( + name: "IX_Products_ProductId", + table: "Products"); + + migrationBuilder.DropColumn( + name: "ProductId", + table: "Products"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Products", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn>>( + name: "TechnicalDetails", + table: "Categories", + type: "jsonb", + nullable: true); + + migrationBuilder.CreateTable( + name: "ProductProduct", + columns: table => new + { + ChildProductsId = table.Column(type: "uuid", nullable: false), + ParentBundlesId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductProduct", x => new { x.ChildProductsId, x.ParentBundlesId }); + table.ForeignKey( + name: "FK_ProductProduct_Products_ChildProductsId", + column: x => x.ChildProductsId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProductProduct_Products_ParentBundlesId", + column: x => x.ParentBundlesId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProductProduct_ParentBundlesId", + table: "ProductProduct", + column: "ParentBundlesId"); + + migrationBuilder.AddForeignKey( + name: "FK_Products_Categories_CategoryId", + table: "Products", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Products_Categories_CategoryId", + table: "Products"); + + migrationBuilder.DropTable( + name: "ProductProduct"); + + migrationBuilder.DropColumn( + name: "TechnicalDetails", + table: "Categories"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Products", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "ProductId", + table: "Products", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Products_ProductId", + table: "Products", + column: "ProductId"); + + migrationBuilder.AddForeignKey( + name: "FK_Products_Categories_CategoryId", + table: "Products", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Products_Products_ProductId", + table: "Products", + column: "ProductId", + principalTable: "Products", + principalColumn: "Id"); + } + } +} diff --git a/ECommerce.Domain/Migrations/20260119180718_MakeTechnicalDetailsNullable.Designer.cs b/ECommerce.Domain/Migrations/20260119180718_MakeTechnicalDetailsNullable.Designer.cs new file mode 100644 index 0000000..a370880 --- /dev/null +++ b/ECommerce.Domain/Migrations/20260119180718_MakeTechnicalDetailsNullable.Designer.cs @@ -0,0 +1,169 @@ +// +using System; +using System.Collections.Generic; +using Commerce.Contracts.DTOs; +using Commerce.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20260119180718_MakeTechnicalDetailsNullable")] + partial class MakeTechnicalDetailsNullable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property>("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property>("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property>>("TechnicalDetails") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CategoryName") + .HasColumnType("text"); + + b.Property("CodeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property>("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Discount") + .HasColumnType("double precision"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property>("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Price") + .HasColumnType("double precision"); + + b.Property>>("TechnicalDetails") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.Property("ChildProductsId") + .HasColumnType("uuid"); + + b.Property("ParentBundlesId") + .HasColumnType("uuid"); + + b.HasKey("ChildProductsId", "ParentBundlesId"); + + b.HasIndex("ParentBundlesId"); + + b.ToTable("ProductProduct"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ChildProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ParentBundlesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Navigation("ChildCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.Domain/Migrations/20260119180718_MakeTechnicalDetailsNullable.cs b/ECommerce.Domain/Migrations/20260119180718_MakeTechnicalDetailsNullable.cs new file mode 100644 index 0000000..8f6aaca --- /dev/null +++ b/ECommerce.Domain/Migrations/20260119180718_MakeTechnicalDetailsNullable.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Commerce.Contracts.DTOs; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + /// + public partial class MakeTechnicalDetailsNullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn>>( + name: "TechnicalDetails", + table: "Products", + type: "jsonb", + nullable: true, + oldClrType: typeof(Dictionary>), + oldType: "jsonb"); + + migrationBuilder.AlterColumn>>( + name: "TechnicalDetails", + table: "Categories", + type: "jsonb", + nullable: true, + oldClrType: typeof(Dictionary>), + oldType: "jsonb"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn>>( + name: "TechnicalDetails", + table: "Products", + type: "jsonb", + nullable: false, + oldClrType: typeof(Dictionary>), + oldType: "jsonb", + oldNullable: true); + + migrationBuilder.AlterColumn>>( + name: "TechnicalDetails", + table: "Categories", + type: "jsonb", + nullable: false, + oldClrType: typeof(Dictionary>), + oldType: "jsonb", + oldNullable: true); + } + } +} diff --git a/ECommerce.Domain/Migrations/20260120122747_NestedTranslations.Designer.cs b/ECommerce.Domain/Migrations/20260120122747_NestedTranslations.Designer.cs new file mode 100644 index 0000000..531d579 --- /dev/null +++ b/ECommerce.Domain/Migrations/20260120122747_NestedTranslations.Designer.cs @@ -0,0 +1,155 @@ +// +using System; +using System.Collections.Generic; +using Commerce.Domain; +using Commerce.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20260120122747_NestedTranslations")] + partial class NestedTranslations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property>("Translations") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CategoryName") + .HasColumnType("text"); + + b.Property("CodeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasColumnType("double precision"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Price") + .HasColumnType("double precision"); + + b.Property>("Translations") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.Property("ChildProductsId") + .HasColumnType("uuid"); + + b.Property("ParentBundlesId") + .HasColumnType("uuid"); + + b.HasKey("ChildProductsId", "ParentBundlesId"); + + b.HasIndex("ParentBundlesId"); + + b.ToTable("ProductProduct"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ChildProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ParentBundlesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Navigation("ChildCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.Domain/Migrations/20260120122747_NestedTranslations.cs b/ECommerce.Domain/Migrations/20260120122747_NestedTranslations.cs new file mode 100644 index 0000000..9a22e7e --- /dev/null +++ b/ECommerce.Domain/Migrations/20260120122747_NestedTranslations.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using Commerce.Contracts.DTOs; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + /// + public partial class NestedTranslations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Name", + table: "Products", + newName: "Translations"); + + migrationBuilder.RenameColumn( + name: "Name", + table: "Categories", + newName: "Translations"); + + // Data Transformation for Products + migrationBuilder.Sql(@" + UPDATE ""Products"" + SET ""Translations"" = ( + SELECT jsonb_agg( + jsonb_build_object( + 'Language', key, + 'Info', jsonb_build_object( + 'Name', value, + 'Description', COALESCE(""Description""->>key, ''), + 'TechnicalDetails', COALESCE(""TechnicalDetails""->key, '[]'::jsonb) + ) + ) + ) + FROM jsonb_each_text(""Translations"") + ) + WHERE jsonb_typeof(""Translations"") = 'object'; + "); + + // Data Transformation for Categories + migrationBuilder.Sql(@" + UPDATE ""Categories"" + SET ""Translations"" = ( + SELECT jsonb_agg( + jsonb_build_object( + 'Language', key, + 'Info', jsonb_build_object( + 'Name', value, + 'Description', COALESCE(""Description""->>key, ''), + 'TechnicalDetails', COALESCE(""TechnicalDetails""->key, '[]'::jsonb) + ) + ) + ) + FROM jsonb_each_text(""Translations"") + ) + WHERE jsonb_typeof(""Translations"") = 'object'; + "); + + migrationBuilder.DropColumn( + name: "Description", + table: "Products"); + + migrationBuilder.DropColumn( + name: "TechnicalDetails", + table: "Products"); + + migrationBuilder.DropColumn( + name: "Description", + table: "Categories"); + + migrationBuilder.DropColumn( + name: "TechnicalDetails", + table: "Categories"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Translations", + table: "Products", + newName: "Name"); + + migrationBuilder.RenameColumn( + name: "Translations", + table: "Categories", + newName: "Name"); + + migrationBuilder.AddColumn>( + name: "Description", + table: "Products", + type: "jsonb", + nullable: false); + + migrationBuilder.AddColumn>>( + name: "TechnicalDetails", + table: "Products", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn>( + name: "Description", + table: "Categories", + type: "jsonb", + nullable: false); + + migrationBuilder.AddColumn>>( + name: "TechnicalDetails", + table: "Categories", + type: "jsonb", + nullable: true); + } + } +} diff --git a/ECommerce.Domain/Migrations/20260120132648_QueryableTranslations.Designer.cs b/ECommerce.Domain/Migrations/20260120132648_QueryableTranslations.Designer.cs new file mode 100644 index 0000000..ecbebab --- /dev/null +++ b/ECommerce.Domain/Migrations/20260120132648_QueryableTranslations.Designer.cs @@ -0,0 +1,309 @@ +// +using System; +using Commerce.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20260120132648_QueryableTranslations")] + partial class QueryableTranslations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CategoryName") + .HasColumnType("text"); + + b.Property("CodeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasColumnType("double precision"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Price") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.Property("ChildProductsId") + .HasColumnType("uuid"); + + b.Property("ParentBundlesId") + .HasColumnType("uuid"); + + b.HasKey("ChildProductsId", "ParentBundlesId"); + + b.HasIndex("ParentBundlesId"); + + b.ToTable("ProductProduct"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId"); + + b.OwnsMany("Commerce.Domain.Entities.CategoryTranslation", "Translations", b1 => + { + b1.Property("CategoryId") + .HasColumnType("uuid"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("CategoryId", "__synthesizedOrdinal"); + + b1.ToTable("Categories"); + + b1.ToJson("Translations"); + + b1.WithOwner() + .HasForeignKey("CategoryId"); + + b1.OwnsOne("Commerce.Domain.Entities.CategoryInfo", "Info", b2 => + { + b2.Property("CategoryTranslationCategoryId") + .HasColumnType("uuid"); + + b2.Property("CategoryTranslation__synthesizedOrdinal") + .HasColumnType("integer"); + + b2.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("CategoryTranslationCategoryId", "CategoryTranslation__synthesizedOrdinal"); + + b2.ToTable("Categories"); + + b2.WithOwner() + .HasForeignKey("CategoryTranslationCategoryId", "CategoryTranslation__synthesizedOrdinal"); + + b2.OwnsMany("Commerce.Domain.Entities.TechnicalDetail", "TechnicalDetails", b3 => + { + b3.Property("CategoryInfoCategoryTranslationCategoryId") + .HasColumnType("uuid"); + + b3.Property("CategoryInfoCategoryTranslation__synthesizedOrdinal") + .HasColumnType("integer"); + + b3.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b3.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b3.HasKey("CategoryInfoCategoryTranslationCategoryId", "CategoryInfoCategoryTranslation__synthesizedOrdinal", "__synthesizedOrdinal"); + + b3.ToTable("Categories"); + + b3.WithOwner() + .HasForeignKey("CategoryInfoCategoryTranslationCategoryId", "CategoryInfoCategoryTranslation__synthesizedOrdinal"); + }); + + b2.Navigation("TechnicalDetails"); + }); + + b1.Navigation("Info") + .IsRequired(); + }); + + b.Navigation("ParentCategory"); + + b.Navigation("Translations"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.OwnsMany("Commerce.Domain.Entities.ProductTranslation", "Translations", b1 => + { + b1.Property("ProductId") + .HasColumnType("uuid"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("ProductId", "__synthesizedOrdinal"); + + b1.ToTable("Products"); + + b1.ToJson("Translations"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + + b1.OwnsOne("Commerce.Domain.Entities.ProductInfo", "Info", b2 => + { + b2.Property("ProductTranslationProductId") + .HasColumnType("uuid"); + + b2.Property("ProductTranslation__synthesizedOrdinal") + .HasColumnType("integer"); + + b2.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("ProductTranslationProductId", "ProductTranslation__synthesizedOrdinal"); + + b2.ToTable("Products"); + + b2.WithOwner() + .HasForeignKey("ProductTranslationProductId", "ProductTranslation__synthesizedOrdinal"); + + b2.OwnsMany("Commerce.Domain.Entities.TechnicalDetail", "TechnicalDetails", b3 => + { + b3.Property("ProductInfoProductTranslationProductId") + .HasColumnType("uuid"); + + b3.Property("ProductInfoProductTranslation__synthesizedOrdinal") + .HasColumnType("integer"); + + b3.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b3.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b3.HasKey("ProductInfoProductTranslationProductId", "ProductInfoProductTranslation__synthesizedOrdinal", "__synthesizedOrdinal"); + + b3.ToTable("Products"); + + b3.WithOwner() + .HasForeignKey("ProductInfoProductTranslationProductId", "ProductInfoProductTranslation__synthesizedOrdinal"); + }); + + b2.Navigation("TechnicalDetails"); + }); + + b1.Navigation("Info") + .IsRequired(); + }); + + b.Navigation("Category"); + + b.Navigation("Translations"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ChildProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ParentBundlesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Navigation("ChildCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.Domain/Migrations/20260120132648_QueryableTranslations.cs b/ECommerce.Domain/Migrations/20260120132648_QueryableTranslations.cs new file mode 100644 index 0000000..1bd9878 --- /dev/null +++ b/ECommerce.Domain/Migrations/20260120132648_QueryableTranslations.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Commerce.Domain.Entities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + /// + public partial class QueryableTranslations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Translations", + table: "Products", + type: "jsonb", + nullable: true, + oldClrType: typeof(List), + oldType: "jsonb"); + + migrationBuilder.AlterColumn( + name: "Translations", + table: "Categories", + type: "jsonb", + nullable: true, + oldClrType: typeof(List), + oldType: "jsonb"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn>( + name: "Translations", + table: "Products", + type: "jsonb", + nullable: false, + oldClrType: typeof(string), + oldType: "jsonb", + oldNullable: true); + + migrationBuilder.AlterColumn>( + name: "Translations", + table: "Categories", + type: "jsonb", + nullable: false, + oldClrType: typeof(string), + oldType: "jsonb", + oldNullable: true); + } + } +} diff --git a/ECommerce.Domain/Migrations/ContextModelSnapshot.cs b/ECommerce.Domain/Migrations/ContextModelSnapshot.cs new file mode 100644 index 0000000..b8f917b --- /dev/null +++ b/ECommerce.Domain/Migrations/ContextModelSnapshot.cs @@ -0,0 +1,306 @@ +// +using System; +using Commerce.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Commerce.Domain.Migrations +{ + [DbContext(typeof(Context))] + partial class ContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CategoryName") + .HasColumnType("text"); + + b.Property("CodeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasColumnType("double precision"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.PrimitiveCollection("Photos") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Price") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.Property("ChildProductsId") + .HasColumnType("uuid"); + + b.Property("ParentBundlesId") + .HasColumnType("uuid"); + + b.HasKey("ChildProductsId", "ParentBundlesId"); + + b.HasIndex("ParentBundlesId"); + + b.ToTable("ProductProduct"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "ParentCategory") + .WithMany("ChildCategories") + .HasForeignKey("ParentCategoryId"); + + b.OwnsMany("Commerce.Domain.Entities.CategoryTranslation", "Translations", b1 => + { + b1.Property("CategoryId") + .HasColumnType("uuid"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("CategoryId", "__synthesizedOrdinal"); + + b1.ToTable("Categories"); + + b1.ToJson("Translations"); + + b1.WithOwner() + .HasForeignKey("CategoryId"); + + b1.OwnsOne("Commerce.Domain.Entities.CategoryInfo", "Info", b2 => + { + b2.Property("CategoryTranslationCategoryId") + .HasColumnType("uuid"); + + b2.Property("CategoryTranslation__synthesizedOrdinal") + .HasColumnType("integer"); + + b2.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("CategoryTranslationCategoryId", "CategoryTranslation__synthesizedOrdinal"); + + b2.ToTable("Categories"); + + b2.WithOwner() + .HasForeignKey("CategoryTranslationCategoryId", "CategoryTranslation__synthesizedOrdinal"); + + b2.OwnsMany("Commerce.Domain.Entities.TechnicalDetail", "TechnicalDetails", b3 => + { + b3.Property("CategoryInfoCategoryTranslationCategoryId") + .HasColumnType("uuid"); + + b3.Property("CategoryInfoCategoryTranslation__synthesizedOrdinal") + .HasColumnType("integer"); + + b3.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b3.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b3.HasKey("CategoryInfoCategoryTranslationCategoryId", "CategoryInfoCategoryTranslation__synthesizedOrdinal", "__synthesizedOrdinal"); + + b3.ToTable("Categories"); + + b3.WithOwner() + .HasForeignKey("CategoryInfoCategoryTranslationCategoryId", "CategoryInfoCategoryTranslation__synthesizedOrdinal"); + }); + + b2.Navigation("TechnicalDetails"); + }); + + b1.Navigation("Info") + .IsRequired(); + }); + + b.Navigation("ParentCategory"); + + b.Navigation("Translations"); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Product", b => + { + b.HasOne("Commerce.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.OwnsMany("Commerce.Domain.Entities.ProductTranslation", "Translations", b1 => + { + b1.Property("ProductId") + .HasColumnType("uuid"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("ProductId", "__synthesizedOrdinal"); + + b1.ToTable("Products"); + + b1.ToJson("Translations"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + + b1.OwnsOne("Commerce.Domain.Entities.ProductInfo", "Info", b2 => + { + b2.Property("ProductTranslationProductId") + .HasColumnType("uuid"); + + b2.Property("ProductTranslation__synthesizedOrdinal") + .HasColumnType("integer"); + + b2.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("ProductTranslationProductId", "ProductTranslation__synthesizedOrdinal"); + + b2.ToTable("Products"); + + b2.WithOwner() + .HasForeignKey("ProductTranslationProductId", "ProductTranslation__synthesizedOrdinal"); + + b2.OwnsMany("Commerce.Domain.Entities.TechnicalDetail", "TechnicalDetails", b3 => + { + b3.Property("ProductInfoProductTranslationProductId") + .HasColumnType("uuid"); + + b3.Property("ProductInfoProductTranslation__synthesizedOrdinal") + .HasColumnType("integer"); + + b3.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b3.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b3.HasKey("ProductInfoProductTranslationProductId", "ProductInfoProductTranslation__synthesizedOrdinal", "__synthesizedOrdinal"); + + b3.ToTable("Products"); + + b3.WithOwner() + .HasForeignKey("ProductInfoProductTranslationProductId", "ProductInfoProductTranslation__synthesizedOrdinal"); + }); + + b2.Navigation("TechnicalDetails"); + }); + + b1.Navigation("Info") + .IsRequired(); + }); + + b.Navigation("Category"); + + b.Navigation("Translations"); + }); + + modelBuilder.Entity("ProductProduct", b => + { + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ChildProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Commerce.Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ParentBundlesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Commerce.Domain.Entities.Category", b => + { + b.Navigation("ChildCategories"); + }); +#pragma warning restore 612, 618 + } + } +}