diff --git a/apps/merchant-api/ApiGateway/RoutingConfiguration/ocelot.review.json b/apps/merchant-api/ApiGateway/RoutingConfiguration/ocelot.review.json new file mode 100644 index 00000000..89a89b2b --- /dev/null +++ b/apps/merchant-api/ApiGateway/RoutingConfiguration/ocelot.review.json @@ -0,0 +1,26 @@ +[ + { + "UpstreamPathTemplate": "/api/review/{everything}", + "UpstreamHttpMethod": ["GET"], + "DownstreamPathTemplate": "/api/review/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "review_service", + "Port": 8080 + } + ] + }, + { + "UpstreamPathTemplate": "/api/review/{everything}", + "UpstreamHttpMethod": ["POST", "PUT", "DELETE"], + "DownstreamPathTemplate": "/api/review/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "review_service", + "Port": 8080 + } + ] + } +] diff --git a/apps/merchant-api/ApiGateway/RoutingConfiguration/ocelot.swagger.json b/apps/merchant-api/ApiGateway/RoutingConfiguration/ocelot.swagger.json index 2e43942a..209345ec 100644 --- a/apps/merchant-api/ApiGateway/RoutingConfiguration/ocelot.swagger.json +++ b/apps/merchant-api/ApiGateway/RoutingConfiguration/ocelot.swagger.json @@ -46,5 +46,17 @@ ], "UpstreamPathTemplate": "/swagger/notification", "UpstreamHttpMethod": ["GET"] + }, + { + "DownstreamPathTemplate": "/swagger/v1/swagger.json", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "review_service", + "Port": 8080 + } + ], + "UpstreamPathTemplate": "/swagger/review", + "UpstreamHttpMethod": ["GET"] } ] diff --git a/apps/merchant-api/ApiGateway/ServerConfigurations/SwaggerConfiguration.cs b/apps/merchant-api/ApiGateway/ServerConfigurations/SwaggerConfiguration.cs index 09e9cca6..bad8a5e6 100644 --- a/apps/merchant-api/ApiGateway/ServerConfigurations/SwaggerConfiguration.cs +++ b/apps/merchant-api/ApiGateway/ServerConfigurations/SwaggerConfiguration.cs @@ -12,6 +12,8 @@ public static void ConfigureSwagger(this IServiceCollection services) c.SwaggerDoc("v1", new OpenApiInfo { Title = "Inventory Service", Version = "v1" }); c.SwaggerDoc("v2", new OpenApiInfo { Title = "User Service", Version = "v1" }); c.SwaggerDoc("v3", new OpenApiInfo { Title = "Payment Service", Version = "v1" }); + c.SwaggerDoc("v4", new OpenApiInfo { Title = "Notification Service", Version = "v1" }); + c.SwaggerDoc("v5", new OpenApiInfo { Title = "Review Service", Version = "v1" }); }); } @@ -24,6 +26,7 @@ public static void UseSwaggerConfig(this WebApplication app) c.SwaggerEndpoint("http://localhost:5001/swagger/user", "User Service V1"); c.SwaggerEndpoint("http://localhost:5001/swagger/payment", "Payment Service V1"); c.SwaggerEndpoint("http://localhost:5001/swagger/notification", "Notification Service V1"); + c.SwaggerEndpoint("http://localhost:5001/swagger/review", "Review Service V1"); c.RoutePrefix = string.Empty; }); } diff --git a/apps/merchant-api/InventoryService/src/InventoryService.Api/Program.cs b/apps/merchant-api/InventoryService/src/InventoryService.Api/Program.cs index 030c03ad..8815a05e 100644 --- a/apps/merchant-api/InventoryService/src/InventoryService.Api/Program.cs +++ b/apps/merchant-api/InventoryService/src/InventoryService.Api/Program.cs @@ -1,14 +1,16 @@ using InventoryService.Api; using DotNetEnv; +using InventoryService.Api.Controllers; using InventoryService.Intraestructure.Data; using InventoryService.Application; using InventoryService.Application.Profiles; using InventoryService.Application.ValidatorSettings; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; +using RabbitMQMessaging.Extensions; -var builder = WebApplication.CreateBuilder(args); Env.Load("../../../.env"); +var builder = WebApplication.CreateBuilder(args); var apiGatewayUrl = builder.Configuration["ApiGatewayUrl"] ?? "http://localhost:5001"; builder.Services.AddCors(options => @@ -31,6 +33,7 @@ builder.Services.AddMediatR(cfg=>cfg.RegisterServicesFromAssemblies(typeof(ProductProfile).Assembly)); builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); +builder.Services.AddMassTransitWithRabbitMq("inventory", typeof(CategoryController)); var connectionString = builder.Configuration["POSTGRES_SQL_CONNECTION"] ?? throw new ArgumentNullException("POSTGRES_SQL_CONNECTION environment variable is not set."); diff --git a/apps/merchant-api/InventoryService/src/InventoryService.Api/QueueHandlers/RatingUpdateHandler.cs b/apps/merchant-api/InventoryService/src/InventoryService.Api/QueueHandlers/RatingUpdateHandler.cs new file mode 100644 index 00000000..e07c0855 --- /dev/null +++ b/apps/merchant-api/InventoryService/src/InventoryService.Api/QueueHandlers/RatingUpdateHandler.cs @@ -0,0 +1,14 @@ +using Commons.Messages; +using InventoryService.Intraestructure.Repositories.Interfaces; +using MassTransit; + +namespace InventoryService.Api.QueueHandlers; + +public class RatingUpdateHandler(IProductRepository repository) : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + var ratingUpdated = context.Message; + await repository.UpdateRating(ratingUpdated.ProductId, ratingUpdated.Rating); + } +} \ No newline at end of file diff --git a/apps/merchant-api/InventoryService/src/InventoryService.Application/Dtos/Products/ProductDto.cs b/apps/merchant-api/InventoryService/src/InventoryService.Application/Dtos/Products/ProductDto.cs index 1787934a..6ed83489 100644 --- a/apps/merchant-api/InventoryService/src/InventoryService.Application/Dtos/Products/ProductDto.cs +++ b/apps/merchant-api/InventoryService/src/InventoryService.Application/Dtos/Products/ProductDto.cs @@ -12,6 +12,7 @@ public class ProductDto public bool IsLiked { get; set; } public string Brand { get; set; } = string.Empty; public int? LowStockThreshold { get; set; } + public float Rating { get; set; } public List Categories { get; set; } = []; public List Images { get; set; } = []; public List ProductVariants { get; set; } = []; diff --git a/apps/merchant-api/InventoryService/src/InventoryService.Application/Services/ProductService.cs b/apps/merchant-api/InventoryService/src/InventoryService.Application/Services/ProductService.cs index c44bb515..275d79e8 100644 --- a/apps/merchant-api/InventoryService/src/InventoryService.Application/Services/ProductService.cs +++ b/apps/merchant-api/InventoryService/src/InventoryService.Application/Services/ProductService.cs @@ -22,6 +22,7 @@ public ProductDto GetProductDtoByProduct(Product existingProduct, List pro IsLiked = isLiked, Price = existingProduct.BasePrice, Brand = existingProduct.Brand, + Rating = existingProduct.Rating, Categories = categoriesDto, Images = existingProduct.Images.Where(i => i.IsActive).Select(i => new ProductVariantImageDto { diff --git a/apps/merchant-api/InventoryService/src/InventoryService.Domain/Concretes/Product.cs b/apps/merchant-api/InventoryService/src/InventoryService.Domain/Concretes/Product.cs index 6e75630e..89e45de0 100644 --- a/apps/merchant-api/InventoryService/src/InventoryService.Domain/Concretes/Product.cs +++ b/apps/merchant-api/InventoryService/src/InventoryService.Domain/Concretes/Product.cs @@ -10,6 +10,7 @@ public class Product : BaseEntity public double BasePrice { get; set; } public string Brand { get; set; } = string.Empty; public int? LowStockThreshold { get; set; } + public float Rating { get; set; } = 0; public ICollection Images { get; set; } = new List(); public ICollection ProductVariants { get; set; } = new List(); public ICollection Categories { get; set; } = new List(); diff --git a/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Concretes/ProductRepository.cs b/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Concretes/ProductRepository.cs index ba612a31..0bcddc60 100644 --- a/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Concretes/ProductRepository.cs +++ b/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Concretes/ProductRepository.cs @@ -111,4 +111,14 @@ public async Task GetCountProductsByStoreId(Guid id, ProductFilteringQueryP } return await query.CountAsync(); } + + public async Task UpdateRating(Guid id, int rating) + { + var product = await GetByIdAsync(id); + if (product == null) return false; + + product.Rating = rating; + var response = await UpdateAsync(product); + return true; + } } \ No newline at end of file diff --git a/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Interfaces/IProductRepository.cs b/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Interfaces/IProductRepository.cs index 0191f67d..d90a27dc 100644 --- a/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Interfaces/IProductRepository.cs +++ b/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Interfaces/IProductRepository.cs @@ -6,4 +6,5 @@ public interface IProductRepository : IRepository { Task> GetProductsByStoreId(Guid id, int pageNumber, int pageSize, ProductFilteringQueryParams queryParams); Task GetCountProductsByStoreId(Guid id, ProductFilteringQueryParams queryParams); + Task UpdateRating(Guid id, float rating); } \ No newline at end of file diff --git a/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Utils/ProductFiltersManager.cs b/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Utils/ProductFiltersManager.cs index 54a07371..6a1c78eb 100644 --- a/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Utils/ProductFiltersManager.cs +++ b/apps/merchant-api/InventoryService/src/InventoryService.Intraestructure/Repositories/Utils/ProductFiltersManager.cs @@ -53,12 +53,12 @@ private async Task>>> _GetFilters(ProductFil if (queryParams.MinRating.HasValue) { - // TODO: Implement filter support. + predicates.Add(p => p.Rating >= queryParams.MinRating.Value); } if (queryParams.MaxRating.HasValue) { - // TODO: Implement filter support. + predicates.Add(p => p.Rating <= queryParams.MaxRating.Value); } return predicates; @@ -91,7 +91,14 @@ public IQueryable _ApplySort(IQueryable query, ProductFilterin if (queryParams.RatingAsc.HasValue) { - // TODO: Add support for rating sort. + query = (bool)queryParams.RatingAsc + ? someSortApplied + ? ((IOrderedQueryable)query).ThenBy(p => p.Rating) + : query.OrderBy(p => p.Rating) + : someSortApplied + ? ((IOrderedQueryable)query).ThenByDescending(p => p.Rating) + : query.OrderByDescending(p => p.Rating); + someSortApplied = true; } if (!someSortApplied) diff --git a/apps/merchant-api/MerchantCommon/Commons/Dto/PaginatedResponseDto.cs b/apps/merchant-api/MerchantCommon/Commons/Dto/PaginatedResponseDto.cs new file mode 100644 index 00000000..1f34a7f6 --- /dev/null +++ b/apps/merchant-api/MerchantCommon/Commons/Dto/PaginatedResponseDto.cs @@ -0,0 +1,11 @@ +namespace Commons.Dto; + +public class PaginatedResponseDto(List items, int totalItems, int page, int pageSize) +{ + public List? Items { get; set; } = items; + public int TotalCount { get; set; } = items.Count; + public int Page { get; set; } = page; + public int PageSize { get; set; } = pageSize; + public int TotalItems { get; set; } = totalItems; + public int TotalPages { get; set; } = (int)Math.Ceiling(totalItems / (double)pageSize); +} \ No newline at end of file diff --git a/apps/merchant-api/MerchantCommon/Commons/Messages/RatingMessage.cs b/apps/merchant-api/MerchantCommon/Commons/Messages/RatingMessage.cs new file mode 100644 index 00000000..4e1c35a9 --- /dev/null +++ b/apps/merchant-api/MerchantCommon/Commons/Messages/RatingMessage.cs @@ -0,0 +1,7 @@ +namespace Commons.Messages; + +public class RatingMessage +{ + public required Guid ProductId { get; set; } + public required float Rating { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/MerchantCommon/RabbitMqMessaging/Extensions/AddRabbitMqConfiguration.cs b/apps/merchant-api/MerchantCommon/RabbitMqMessaging/Extensions/AddRabbitMqConfiguration.cs index 612267e0..26985318 100644 --- a/apps/merchant-api/MerchantCommon/RabbitMqMessaging/Extensions/AddRabbitMqConfiguration.cs +++ b/apps/merchant-api/MerchantCommon/RabbitMqMessaging/Extensions/AddRabbitMqConfiguration.cs @@ -9,7 +9,6 @@ public static class Extensions { public static IServiceCollection AddMassTransitWithRabbitMq(this IServiceCollection services, string serviceName, Type? type=null) { - var rabbitMqConnectionString = Environment.GetEnvironmentVariable("RABBITMQ_CONNECTION_STRING"); var rabbitMqUser = Environment.GetEnvironmentVariable("RABBITMQ_USER") ?? throw new("RABBITMQ_USER"); var rabbitMqPass = Environment.GetEnvironmentVariable("RABBITMQ_PASS") ?? throw new("RABBITMQ_PASS"); diff --git a/apps/merchant-api/ReviewService/Dockerfile b/apps/merchant-api/ReviewService/Dockerfile new file mode 100644 index 00000000..eb61ee80 --- /dev/null +++ b/apps/merchant-api/ReviewService/Dockerfile @@ -0,0 +1,29 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY ReviewService/src/ReviewService.Api/ReviewService.Api.csproj ReviewService/src/ReviewService.Api/ +COPY ReviewService/src/ReviewService.Application/ReviewService.Application.csproj ReviewService/src/ReviewService.Application/ +COPY ReviewService/src/ReviewService.Domain/ReviewService.Domain.csproj ReviewService/src/ReviewService.Domain/ +COPY ReviewService/src/ReviewService.Infrastructure/ReviewService.Infrastructure.csproj ReviewService/src/ReviewService.Infrastructure/ +COPY MerchantCommon/RabbitMqMessaging/RabbitMqMessaging.csproj MerchantCommon/RabbitMqMessaging/ +COPY NotificationService/src/NotificationService.Domain/NotificationService.Domain.csproj NotificationService/src/NotificationService.Domain/ +COPY MerchantCommon/Commons/Commons.csproj MerchantCommon/Commons/ + +WORKDIR /app/ReviewService/src/ReviewService.Api +RUN dotnet restore + +COPY ReviewService/src/ReviewService.Api/. . +COPY ReviewService/src/ReviewService.Application/. ../ReviewService.Application/ +COPY ReviewService/src/ReviewService.Domain/. ../ReviewService.Domain/ +COPY ReviewService/src/ReviewService.Infrastructure/. ../ReviewService.Infrastructure/ +COPY NotificationService/src/NotificationService.Domain/. /app/NotificationService/src/NotificationService.Domain/ +COPY MerchantCommon/RabbitMqMessaging/. /app/MerchantCommon/RabbitMqMessaging/ +COPY MerchantCommon/Commons/. /app/MerchantCommon/Commons/ + +RUN dotnet publish -c Release -o /app/out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app +COPY --from=build /app/out . +EXPOSE 8080 +ENTRYPOINT ["dotnet", "ReviewService.Api.dll"] \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/Configuration/ConfigDb.cs b/apps/merchant-api/ReviewService/src/ReviewService.Api/Configuration/ConfigDb.cs new file mode 100644 index 00000000..52686ec5 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/Configuration/ConfigDb.cs @@ -0,0 +1,25 @@ +using MongoDB.Driver; +using ReviewService.Concretes; +using ReviewService.Infrastructure.Context.Concretes; +using ReviewService.Infrastructure.Context.Interfaces; + +namespace ReviewService.Api.Configuration; + +public static class ConfigDb +{ + public static IServiceCollection ConfigDbSet(this IServiceCollection services, string connectionString) + { + var client = new MongoClient(connectionString); + var db = client.GetDatabase("reviews_db"); + + services.AddScoped(_ => db); + services.ConfigContext(); + return services; + } + + public static IServiceCollection ConfigContext(this IServiceCollection services) + { + services.AddScoped, ProductReviewContext>(); + return services; + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/Configuration/ConfigRepositories.cs b/apps/merchant-api/ReviewService/src/ReviewService.Api/Configuration/ConfigRepositories.cs new file mode 100644 index 00000000..4f07f368 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/Configuration/ConfigRepositories.cs @@ -0,0 +1,13 @@ +using ReviewService.Concretes; +using ReviewService.Infrastructure.Repositories.Concretes; +using ReviewService.Infrastructure.Repositories.Interfaces; + +namespace ReviewService.Api.Configuration; + +public static class ConfigRepositories +{ + public static IServiceCollection AddRepositories(this IServiceCollection services) + { + return services.AddScoped, ProductReviewRepository>(); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/Controllers/ProductReviewController.cs b/apps/merchant-api/ReviewService/src/ReviewService.Api/Controllers/ProductReviewController.cs new file mode 100644 index 00000000..07f4683a --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/Controllers/ProductReviewController.cs @@ -0,0 +1,69 @@ +using Commons.ResponseHandler.Responses.Concretes; + +using MediatR; +using Microsoft.AspNetCore.Mvc; +using ReviewService.Application.CommandsQueries.Commons; +using ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; +using ReviewService.Application.CommandsQueries.ProductReview.Queries.Requests; +using ReviewService.Application.Dtos.ProductReview; +using ReviewService.Application.Dtos.Review; +using ReviewService.Concretes; + +namespace ReviewService.Api.Controllers; + +[ApiController] +[Route("api/review/[controller]")] +public class ProductReviewController(IMediator mediator) : ControllerBase +{ + [HttpPost] + public async Task Create(CreateProductReviewDto productReview) + { + var result = await mediator.Send(new CreateProductReview(productReview)); + if (result is ErrorResponse errorResponse) + return StatusCode(errorResponse.StatusCode, errorResponse); + var successResponse = (SuccessResponse)result; + return StatusCode(successResponse.StatusCode, successResponse); + } + + [HttpPost("{productId}")] + public async Task AddReview(Guid productId, CreateReviewDto review) + { + var result = await mediator.Send(new AddReview(productId, review)); + if (result is ErrorResponse errorResponse) + return StatusCode(errorResponse.StatusCode, errorResponse); + + var successResponse = (SuccessResponse)result; + return StatusCode(successResponse.StatusCode, successResponse); + } + + [HttpGet("{id}")] + public async Task GetProductReview(Guid id, Guid? clientId, int page, int pageSize) + { + var result = await mediator + .Send(new GetProductReviewById(id, clientId, new PaginationRequest(page, pageSize))); + if (result is ErrorResponse errorResponse) + return StatusCode(errorResponse.StatusCode, errorResponse); + var successResponse = (SuccessResponse)result; + return StatusCode(successResponse.StatusCode, successResponse); + } + + [HttpPatch("{productId}")] + public async Task UpdateReview(Guid productId, Guid clientId, UpdateReviewDto updateReviewDto) + { + var result = await mediator.Send(new UpdateReview(productId, clientId, updateReviewDto)); + if (result is ErrorResponse errorResponse) + return StatusCode(errorResponse.StatusCode, errorResponse); + var successResponse = (SuccessResponse)result; + return StatusCode(successResponse.StatusCode, successResponse); + } + + [HttpDelete("{productId}")] + public async Task DeleteReview(Guid productId, Guid clientId) + { + var result = await mediator.Send(new DeleteReview(productId, clientId)); + if (result is ErrorResponse errorResponse) + return StatusCode(errorResponse.StatusCode, errorResponse); + var successResponse = (SuccessResponse)result; + return StatusCode(successResponse.StatusCode, successResponse); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/Program.cs b/apps/merchant-api/ReviewService/src/ReviewService.Api/Program.cs new file mode 100644 index 00000000..671bdbce --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/Program.cs @@ -0,0 +1,61 @@ +using Commons.ResponseHandler.Handler.Concretes; +using Commons.ResponseHandler.Handler.Interfaces; +using DotNetEnv; +using FluentValidation; +using MassTransit.Internals; +using RabbitMQMessaging.Extensions; +using ReviewService.Api; +using ReviewService.Api.Configuration; +using ReviewService.Application.Profiles; +using ReviewService.Application.ValidationSettings; + +var builder = WebApplication.CreateBuilder(args); +Env.Load("../../../.env"); + +builder.Configuration.AddEnvironmentVariables(); +builder.Configuration.AddJsonFile("validationSettings.json", optional: false, reloadOnChange: true); +builder.Services.Configure(builder.Configuration); + +var apiGateway = builder.Configuration["ApiGatewayUrl"] ?? "http://localhost:5001"; +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowedLocalhost", + policyBuilder => policyBuilder + .WithOrigins(apiGateway) + .AllowAnyHeader() + .AllowAnyMethod()); +}); + +var connectionString = builder.Configuration["MONGODB_URI"]; +if (connectionString == null) +{ + Console.WriteLine("You must set your 'MONGODB_URI' environment variable. To learn how to set it, see https://www.mongodb.com/docs/drivers/csharp/current/quick-start/#set-your-connection-string"); + Environment.Exit(0); +} + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.ConfigureSwagger(); + +builder.Services.AddRepositories(); +builder.Services.ConfigDbSet(connectionString); +builder.Services.AddValidatorsFromAssembly(typeof(ReviewServiceProfile).Assembly); +builder.Services.AddAutoMapper(typeof(ReviewServiceProfile)); +builder.Services.AddMediatR(cfg => + cfg.RegisterServicesFromAssemblies(typeof(ReviewServiceProfile).Assembly)); +builder.Services.AddScoped(); +builder.Services.AddMassTransitWithRabbitMq("review"); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapControllers(); +app.UseCors("AllowApiGateway"); +app.UseAuthentication(); +app.UseAuthorization(); +app.Run(); \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/Properties/launchSettings.json b/apps/merchant-api/ReviewService/src/ReviewService.Api/Properties/launchSettings.json new file mode 100644 index 00000000..7d779e24 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:12039", + "sslPort": 44349 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5500", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7269;http://localhost:5500", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/ReviewService.Api.csproj b/apps/merchant-api/ReviewService/src/ReviewService.Api/ReviewService.Api.csproj new file mode 100644 index 00000000..ddfec229 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/ReviewService.Api.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + ReviewService.Api + + + + + + + + + + + + + + + + + + + + diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/SwaggerConf.cs b/apps/merchant-api/ReviewService/src/ReviewService.Api/SwaggerConf.cs new file mode 100644 index 00000000..6df1dfeb --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/SwaggerConf.cs @@ -0,0 +1,49 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace ReviewService.Api; + +public static class SwaggerConf +{ + public static void ConfigureSwagger(this IServiceCollection services) + { + services.AddSwaggerGen(o => + { + ConfigureApiKeyBearer(o); + AddApiKeyRequirement(o); + }); + } + + private static void ConfigureApiKeyBearer(SwaggerGenOptions o) + { + o.AddSecurityDefinition("ApiKeyBearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter the JWT token with the 'Bearer ' prefix", + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + } + + private static void AddApiKeyRequirement(SwaggerGenOptions o) + { + o.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "ApiKeyBearer" + }, + Scheme = "Bearer", + Name = "Authorization", + In = ParameterLocation.Header, + }, + new List() + } + }); + } +} diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/appsettings.json b/apps/merchant-api/ReviewService/src/ReviewService.Api/appsettings.json new file mode 100644 index 00000000..e0a4eaaa --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ApiGatewayUrl": "http://localhost:5001", + "IdentityUrlVerification": "" +} diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Api/validationSettings.json b/apps/merchant-api/ReviewService/src/ReviewService.Api/validationSettings.json new file mode 100644 index 00000000..760386c9 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Api/validationSettings.json @@ -0,0 +1,9 @@ +{ + "Review": { + "MinLengthName": 3, + "MaxLengthName": 50, + "MaxLengthComment": 500, + "MinRatingRange": 0, + "MaxRatingRange": 5 + } +} diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/Commons/IPaginationRequest.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/Commons/IPaginationRequest.cs new file mode 100644 index 00000000..2df09518 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/Commons/IPaginationRequest.cs @@ -0,0 +1,7 @@ +namespace ReviewService.Application.CommandsQueries.Commons; + +public class PaginationRequest(int page, int pageSize) +{ + public int Page { get; set; } = page; + public int PageSize { get; set; } = pageSize; +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/AddReviewHandler.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/AddReviewHandler.cs new file mode 100644 index 00000000..91739f57 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/AddReviewHandler.cs @@ -0,0 +1,67 @@ +using AutoMapper; +using Commons.Messages; +using Commons.ResponseHandler.Handler.Interfaces; +using Commons.ResponseHandler.Responses.Bases; +using FluentValidation; +using MassTransit; +using MediatR; +using MongoDB.Driver; +using ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; +using ReviewService.Application.Dtos.Review; +using ReviewService.Concretes; +using ReviewService.Infrastructure.Repositories.Interfaces; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Commands.Handlers; + +public class AddReviewHandler( + IRepository repository, + IValidator validator, + IResponseHandlingHelper responseHandlingHelper, + IMapper mapper, + IBus producer) + : IRequestHandler +{ + public async Task Handle(AddReview request, CancellationToken cancellationToken) + { + var result = await validator.ValidateAsync(request.Review, cancellationToken); + if (!result.IsValid) + return responseHandlingHelper.BadRequest( + "Validations failed", + result.Errors.Select(e => e.ErrorMessage).ToList()); + var productReview = await repository.GetByIdAsync(request.ProductId); + + if (productReview == null) + { + await repository.AddAsync(new Concretes.ProductReview + { + ProductId = request.ProductId, + Reviews = [mapper.Map(request.Review)] + }); + return responseHandlingHelper.Ok("Review added successfully", mapper.Map(request.Review)); + } + + var clientReview = productReview.Reviews.Find(r => r.ClientId == request.Review.ClientId); + if (clientReview is { IsActive: true }) + return responseHandlingHelper.BadRequest("Client already make review for this product"); + + var reviewsUpdated = productReview.Reviews; + reviewsUpdated.Add(mapper.Map(request.Review)); + var updateBuilder = new UpdateDefinitionBuilder() + .Set>(e => e.Reviews, reviewsUpdated); + + var response = await repository.UpdateAsync(productReview.Id, productReview, updateBuilder); + + float ratingSummation = reviewsUpdated.Aggregate(0, (i, review) => i + review.Rating); + var averageRating = ratingSummation / reviewsUpdated.Count; + + await producer.Publish(new RatingMessage + { + ProductId = request.ProductId, + Rating = averageRating + }); + + if (response < 0) + return responseHandlingHelper.InternalServerError("Some error happens while try to add review"); + return responseHandlingHelper.Ok("Review added successfully", mapper.Map(request.Review)); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/CreateProductReviewHandler.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/CreateProductReviewHandler.cs new file mode 100644 index 00000000..0fc37dbe --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/CreateProductReviewHandler.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using Commons.ResponseHandler.Handler.Concretes; +using Commons.ResponseHandler.Handler.Interfaces; +using Commons.ResponseHandler.Responses.Bases; +using FluentValidation; +using MediatR; +using ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; +using ReviewService.Application.Dtos.ProductReview; +using ReviewService.Infrastructure.Repositories.Interfaces; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Commands.Handlers; + +public class CreateProductReviewHandler( + IRepository repository, + IValidator validator, + IResponseHandlingHelper responseHandlingHelper, + IMapper mapper) + : IRequestHandler +{ + public async Task Handle(CreateProductReview request, CancellationToken cancellationToken) + { + var result = await validator.ValidateAsync(request.ProductReview, cancellationToken); + if (!result.IsValid) return responseHandlingHelper + .BadRequest( + "Validations failed", + result.Errors.Select(e => e.ErrorMessage).ToList()); + + var productReview = mapper.Map(request.ProductReview); + await repository.AddAsync(productReview); + return responseHandlingHelper.Created("Product review successfully added", productReview); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/DeleteReviewHandler.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/DeleteReviewHandler.cs new file mode 100644 index 00000000..b0d9cf7d --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/DeleteReviewHandler.cs @@ -0,0 +1,38 @@ +using Commons.ResponseHandler.Handler.Interfaces; +using Commons.ResponseHandler.Responses.Bases; +using MediatR; +using MongoDB.Driver; +using ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; +using ReviewService.Concretes; +using ReviewService.Infrastructure.Repositories.Interfaces; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Commands.Handlers; + +public class DeleteReviewHandler( + IRepository repository, + IResponseHandlingHelper responseHandlingHelper) + : IRequestHandler +{ + public async Task Handle(DeleteReview request, CancellationToken cancellationToken) + { + var productReview = await repository.GetByIdAsync(request.ProductId); + if (productReview == null) + return responseHandlingHelper.NotFound("Product review not found"); + var review = productReview.Reviews.Find(r => r.ClientId == request.ClientId); + if (review == null) + return responseHandlingHelper.NotFound("Client's review not found"); + + var reviewsUpdated = productReview.Reviews.Where(r => r.ClientId != request.ClientId).ToList(); + review.IsActive = false; + review.DeletedAt = DateTime.UtcNow; + reviewsUpdated.Add(review); + + var updateBuilder = new UpdateDefinitionBuilder() + .Set>(e => e.Reviews, reviewsUpdated); + var response = await repository.UpdateAsync(request.ProductId, productReview, updateBuilder); + + return response > 0 + ? responseHandlingHelper.Ok("Review deleted successfully", review) + : responseHandlingHelper.InternalServerError("Some error happens while try to delete review"); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/UpdateReviewHandler.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/UpdateReviewHandler.cs new file mode 100644 index 00000000..0874d519 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Handlers/UpdateReviewHandler.cs @@ -0,0 +1,38 @@ +using Commons.ResponseHandler.Handler.Interfaces; +using Commons.ResponseHandler.Responses.Bases; +using MediatR; +using MongoDB.Driver; +using ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; +using ReviewService.Concretes; +using ReviewService.Infrastructure.Repositories.Interfaces; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Commands.Handlers; + +public class UpdateReviewHandler( + IRepository repository, + IResponseHandlingHelper responseHandlingHelper) + : IRequestHandler +{ + public async Task Handle(UpdateReview request, CancellationToken cancellationToken) + { + var productReview = await repository.GetByIdAsync(request.ProductId); + if (productReview == null) + return responseHandlingHelper.NotFound("Product review not found"); + var review = productReview.Reviews.Find(r => r.ClientId == request.ClientId && r.IsActive); + if (review == null) return responseHandlingHelper.NotFound("Client's review not found"); + + var reviewsExcluded = productReview.Reviews.Where(r => r.Id != review.Id); + review.Comment = request.UpdateReviewDto.Comment ?? review.Comment; + review.Rating = request.UpdateReviewDto.Rating ?? review.Rating; + review.UpdatedAt = DateTime.UtcNow; + var reviewsUpdated = reviewsExcluded.Append(review); + + var updateBuilder = new UpdateDefinitionBuilder() + .Set>(e => e.Reviews, reviewsUpdated.ToList()); + var response = await repository.UpdateAsync(productReview.Id, productReview, updateBuilder); + + return response > 0 + ? responseHandlingHelper.Ok("Review updated successfully", review) + : responseHandlingHelper.InternalServerError("Some error happens while review updating"); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/AddReview.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/AddReview.cs new file mode 100644 index 00000000..6b3b7b9e --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/AddReview.cs @@ -0,0 +1,11 @@ +using Commons.ResponseHandler.Responses.Bases; +using MediatR; +using ReviewService.Application.Dtos.Review; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; + +public class AddReview(Guid productId, CreateReviewDto review) : IRequest +{ + public Guid ProductId { get; set; } = productId; + public CreateReviewDto Review { get; set; } = review; +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/CreateProductReview.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/CreateProductReview.cs new file mode 100644 index 00000000..e106425b --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/CreateProductReview.cs @@ -0,0 +1,10 @@ +using Commons.ResponseHandler.Responses.Bases; +using MediatR; +using ReviewService.Application.Dtos.ProductReview; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; + +public class CreateProductReview(CreateProductReviewDto productReview) : IRequest +{ + public CreateProductReviewDto ProductReview { get; set; } = productReview; +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/DeleteReview.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/DeleteReview.cs new file mode 100644 index 00000000..f36b6d69 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/DeleteReview.cs @@ -0,0 +1,10 @@ +using Commons.ResponseHandler.Responses.Bases; +using MediatR; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; + +public class DeleteReview(Guid productId, Guid clientId) : IRequest +{ + public Guid ProductId { get; set; } = productId; + public Guid ClientId { get; set; } = clientId; +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/UpdateReview.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/UpdateReview.cs new file mode 100644 index 00000000..245fa184 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Commands/Requests/UpdateReview.cs @@ -0,0 +1,12 @@ +using Commons.ResponseHandler.Responses.Bases; +using MediatR; +using ReviewService.Application.Dtos.Review; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; + +public class UpdateReview(Guid productId, Guid clientId, UpdateReviewDto updateReviewDto) : IRequest +{ + public Guid ProductId { get; set; } = productId; + public Guid ClientId { get; set; } = clientId; + public UpdateReviewDto UpdateReviewDto { get; set; } = updateReviewDto; +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Queries/Handlers/GetProductReviewByIdHandler.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Queries/Handlers/GetProductReviewByIdHandler.cs new file mode 100644 index 00000000..1fde7589 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Queries/Handlers/GetProductReviewByIdHandler.cs @@ -0,0 +1,54 @@ +using Commons.Dto; +using Commons.ResponseHandler.Handler.Interfaces; +using Commons.ResponseHandler.Responses.Bases; +using FluentValidation; +using MediatR; +using ReviewService.Application.CommandsQueries.Commons; +using ReviewService.Application.CommandsQueries.ProductReview.Queries.Requests; +using ReviewService.Application.Dtos.ProductReview; +using ReviewService.Concretes; +using ReviewService.Infrastructure.Repositories.Interfaces; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Queries.Handlers; + +public class GetProductReviewByIdHandler( + IRepository repository, + IValidator validator, + IResponseHandlingHelper responseHandlingHelper) + : IRequestHandler +{ + public async Task Handle(GetProductReviewById request, CancellationToken cancellationToken) + { + var result = await validator.ValidateAsync(request.PaginationRequest, cancellationToken); + if (!result.IsValid) + return responseHandlingHelper.BadRequest("Bad pagination request"); + + var response = await repository.GetByIdAsync(request.ProductId); + if (response == null) + return responseHandlingHelper.NotFound("Product review not found"); + double summationRating = 0; + response.Reviews.ForEach(r => summationRating += r.IsActive ? r.Rating : 0); + + var paginatedReviews = response.Reviews + .Where(r => r.IsActive) + .OrderByDescending(r => r.Rating) + .ThenBy(r => r.CreatedAt) + .Skip((request.PaginationRequest.Page - 1) * request.PaginationRequest.PageSize) + .Take(request.PaginationRequest.PageSize).ToList(); + var paginatedResponse = new PaginatedResponseDto( + paginatedReviews, + response.Reviews.Count(r => r.IsActive), + request.PaginationRequest.Page, + request.PaginationRequest.PageSize); + + var clients = response.Reviews.Where(r => r.IsActive).Select(r => r.ClientId); + var ableToAdd = request.ClientId != null && !clients.Contains((Guid)request.ClientId); + + return responseHandlingHelper.Ok("Product review found", new ProductReviewDto + { + AverageRating = Math.Round((summationRating / response.Reviews.Count(r => r.IsActive)), 1), + Reviews = paginatedResponse, + ClientAbleToAdd = ableToAdd + }); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Queries/Requests/GetProductReviewById.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Queries/Requests/GetProductReviewById.cs new file mode 100644 index 00000000..4c5414b7 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/CommandsQueries/ProductReview/Queries/Requests/GetProductReviewById.cs @@ -0,0 +1,12 @@ +using Commons.ResponseHandler.Responses.Bases; +using MediatR; +using ReviewService.Application.CommandsQueries.Commons; + +namespace ReviewService.Application.CommandsQueries.ProductReview.Queries.Requests; + +public class GetProductReviewById(Guid productId, Guid? clientId, PaginationRequest paginationRequest) : IRequest +{ + public Guid ProductId { get; set; } = productId; + public Guid? ClientId { get; set; } = clientId; + public PaginationRequest PaginationRequest { get; set; } = paginationRequest; +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/ProductReview/CreateProductReviewDto.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/ProductReview/CreateProductReviewDto.cs new file mode 100644 index 00000000..482e1652 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/ProductReview/CreateProductReviewDto.cs @@ -0,0 +1,9 @@ +using ReviewService.Application.Dtos.Review; + +namespace ReviewService.Application.Dtos.ProductReview; + +public class CreateProductReviewDto +{ + public required Guid ProductId { get; set; } + public required List Reviews { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/ProductReview/ProductReviewDto.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/ProductReview/ProductReviewDto.cs new file mode 100644 index 00000000..08e1c970 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/ProductReview/ProductReviewDto.cs @@ -0,0 +1,12 @@ +using Commons.Dto; +using ReviewService.Application.CommandsQueries.Commons; +using ReviewService.Concretes; + +namespace ReviewService.Application.Dtos.ProductReview; + +public class ProductReviewDto +{ + public required double AverageRating { get; set; } + public required bool ClientAbleToAdd { get; set; } = true; + public required PaginatedResponseDto Reviews { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/Review/CreateReviewDto.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/Review/CreateReviewDto.cs new file mode 100644 index 00000000..b4e908d6 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/Review/CreateReviewDto.cs @@ -0,0 +1,9 @@ +namespace ReviewService.Application.Dtos.Review; + +public class CreateReviewDto +{ + public required Guid ClientId { get; set; } + public required string ClientName { get; set; } + public required uint Rating { get; set; } + public string? Comment { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/Review/UpdateReviewDto.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/Review/UpdateReviewDto.cs new file mode 100644 index 00000000..921f2b37 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Dtos/Review/UpdateReviewDto.cs @@ -0,0 +1,7 @@ +namespace ReviewService.Application.Dtos.Review; + +public class UpdateReviewDto +{ + public int? Rating { get; set; } + public string? Comment { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Profiles/ReviewServiceProfile.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Profiles/ReviewServiceProfile.cs new file mode 100644 index 00000000..f83ef978 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Profiles/ReviewServiceProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using ReviewService.Application.CommandsQueries.ProductReview.Commands.Requests; +using ReviewService.Application.Dtos.ProductReview; +using ReviewService.Application.Dtos.Review; +using ReviewService.Concretes; + +namespace ReviewService.Application.Profiles; + +public class ReviewServiceProfile : Profile +{ + public ReviewServiceProfile() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Program.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Program.cs new file mode 100644 index 00000000..e5dff12b --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/ReviewService.Application.csproj b/apps/merchant-api/ReviewService/src/ReviewService.Application/ReviewService.Application.csproj new file mode 100644 index 00000000..59570056 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/ReviewService.Application.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ProductReviewSettings.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ProductReviewSettings.cs new file mode 100644 index 00000000..816fa938 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ProductReviewSettings.cs @@ -0,0 +1,6 @@ +namespace ReviewService.Application.ValidationSettings; + +public class ProductReviewSettings +{ + +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ReviewSettings.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ReviewSettings.cs new file mode 100644 index 00000000..802548b4 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ReviewSettings.cs @@ -0,0 +1,11 @@ +namespace ReviewService.Application.ValidationSettings; + +public class ReviewSettings +{ + public int MinLengthName { get; set; } + public int MaxLengthName { get; set; } + + public int MaxLengthComment { get; set; } + public int MinRatingRange { get; set; } + public int MaxRatingRange { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ValidationSettings.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ValidationSettings.cs new file mode 100644 index 00000000..570302be --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/ValidationSettings/ValidationSettings.cs @@ -0,0 +1,6 @@ +namespace ReviewService.Application.ValidationSettings; + +public class ValidationSettings +{ + public ReviewSettings Review { get; set; } = new(); +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Commons/PaginationRequestValidator.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Commons/PaginationRequestValidator.cs new file mode 100644 index 00000000..599a36cd --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Commons/PaginationRequestValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using ReviewService.Application.CommandsQueries.Commons; + +namespace ReviewService.Application.Validators.Commons; + +public class PaginationRequestValidator : AbstractValidator +{ + public PaginationRequestValidator() + { + RuleFor(e => e.Page) + .NotNull() + .Must(p => p >= 0).WithMessage("Page should be greater than zero"); + RuleFor(e => e.PageSize) + .NotNull() + .Must(p => p >= 0).WithMessage("Page should be greater than zero"); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/ProductReview/CreateProductReviewDtoValidator.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/ProductReview/CreateProductReviewDtoValidator.cs new file mode 100644 index 00000000..d2dd6579 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/ProductReview/CreateProductReviewDtoValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Microsoft.Extensions.Options; +using ReviewService.Application.Dtos.ProductReview; +using ReviewService.Application.Validators.Review; + +namespace ReviewService.Application.Validators.ProductReview; + +public class CreateProductReviewDtoValidator : AbstractValidator +{ + public CreateProductReviewDtoValidator(IOptions validationSettings) + { + RuleFor(e => e.ProductId).NotNull(); + RuleForEach(e => e.Reviews) + .SetValidator(new CreateReviewDtoValidator(validationSettings)); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Review/CreateReviewDtoValidator.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Review/CreateReviewDtoValidator.cs new file mode 100644 index 00000000..12360fbb --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Review/CreateReviewDtoValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using Microsoft.Extensions.Options; +using ReviewService.Application.Dtos.Review; + +namespace ReviewService.Application.Validators.Review; + +public class CreateReviewDtoValidator : AbstractValidator +{ + public CreateReviewDtoValidator(IOptions validationSettings) + { + var reviewSettings = validationSettings.Value.Review; + RuleFor(r => r.ClientId).NotNull(); + RuleFor(r => r.ClientName) + .NotNull() + .MinimumLength(reviewSettings.MinLengthName) + .MaximumLength(reviewSettings.MaxLengthName); + RuleFor(r => r.Comment) + .MaximumLength(reviewSettings.MaxLengthComment); + RuleFor(r => r.Rating) + .NotNull() + .Must(r => r <= reviewSettings.MaxRatingRange).WithMessage($"The rating should be smaller than {reviewSettings.MaxRatingRange}") + .Must(r => r >= reviewSettings.MinRatingRange).WithMessage($"The rating should be bigger than {reviewSettings.MinRatingRange}"); + } +} diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Review/ReviewValidator.cs b/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Review/ReviewValidator.cs new file mode 100644 index 00000000..59ae0c2a --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Application/Validators/Review/ReviewValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using Microsoft.Extensions.Options; + +namespace ReviewService.Application.Validators.Review; + +public class ReviewValidator : AbstractValidator +{ + public ReviewValidator(IOptions validationSettings) + { + var reviewSettings = validationSettings.Value.Review; + RuleFor(r => r.ClientId).NotNull(); + RuleFor(r => r.ClientName) + .NotNull() + .MinimumLength(reviewSettings.MinLengthName) + .MaximumLength(reviewSettings.MaxLengthName); + RuleFor(r => r.Comment) + .NotEmpty() + .MaximumLength(reviewSettings.MaxLengthComment); + RuleFor(r => r.Rating) + .NotNull() + .Must(r => r <= reviewSettings.MaxRatingRange).WithMessage($"The rating should be smaller than {reviewSettings.MaxRatingRange}") + .Must(r => r >= reviewSettings.MinRatingRange).WithMessage($"The rating should be bigger than {reviewSettings.MinRatingRange}"); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Domain/Bases/BaseEntity.cs b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Bases/BaseEntity.cs new file mode 100644 index 00000000..2e1ccc4e --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Bases/BaseEntity.cs @@ -0,0 +1,15 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using ReviewService.Interfaces; + +namespace ReviewService.Bases; + +public class BaseEntity : BaseRegister, IEntity +{ + [BsonId] + [BsonGuidRepresentation(GuidRepresentation.Standard)] + public Guid Id { get; set; } = Guid.NewGuid(); + + [BsonElement("is_active")] + public bool IsActive { get; set; } = true; +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Domain/Bases/BaseRegister.cs b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Bases/BaseRegister.cs new file mode 100644 index 00000000..b671ba3d --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Bases/BaseRegister.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson.Serialization.Attributes; +using ReviewService.Interfaces; + +namespace ReviewService.Bases; + +public class BaseRegister : IRegister +{ + [BsonElement("created_at")] + public DateTime CreatedAt { get; } = DateTime.Now; + + [BsonElement("updated_at")] + public DateTime? UpdatedAt { get; set; } + + [BsonElement("deleted_at")] + public DateTime? DeletedAt { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Domain/Class1.cs b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Class1.cs new file mode 100644 index 00000000..e3238f8d --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Class1.cs @@ -0,0 +1,5 @@ +namespace ReviewService; + +public class Class1 +{ +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Domain/Concretes/ProductReview.cs b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Concretes/ProductReview.cs new file mode 100644 index 00000000..189b05fe --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Concretes/ProductReview.cs @@ -0,0 +1,25 @@ +using System.Text; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using ReviewService.Bases; + +namespace ReviewService.Concretes; + +public class ProductReview : BaseEntity +{ + [BsonElement("product_id")] + [BsonGuidRepresentation(GuidRepresentation.Standard)] + public required Guid ProductId { get; set; } + + [BsonElement("reviews")] + public required List Reviews { get; set; } = new(); + + public override string ToString() + { + StringBuilder sb = new StringBuilder("ProductReview: {"); + sb.Append($"ID: {Id}, "); + sb.Append($"ProductId: {ProductId}, "); + sb.Append($"Reviews: {Reviews.ToString()}"); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Domain/Concretes/Review.cs b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Concretes/Review.cs new file mode 100644 index 00000000..99d06a8a --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Concretes/Review.cs @@ -0,0 +1,35 @@ +using System.Text; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using ReviewService.Bases; + +namespace ReviewService.Concretes; + +public class Review : BaseEntity +{ + + [BsonElement("client_id")] + [BsonGuidRepresentation(GuidRepresentation.Standard)] + public required Guid ClientId { get; set; } + + [BsonElement("client_name")] + public required string ClientName { get; set; } + + [BsonElement("rating")] + public required int Rating { get; set; } + + [BsonElement("comment")] + public string? Comment { get; set; } + + public override string ToString() + { + StringBuilder sb = new StringBuilder("{Review: {"); + sb.Append($"ClientID: {ClientName}, "); + sb.Append($"ClientName: {ClientName}, "); + sb.Append($"Rating: {Rating}, "); + sb.Append($"Comment: {Comment}"); + sb.Append("}}"); + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Domain/Interfaces/IEntity.cs b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Interfaces/IEntity.cs new file mode 100644 index 00000000..e8028fb3 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Interfaces/IEntity.cs @@ -0,0 +1,7 @@ +namespace ReviewService.Interfaces; + +public interface IEntity : IRegister +{ + Guid Id { get; } + bool IsActive { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Domain/Interfaces/IRegister.cs b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Interfaces/IRegister.cs new file mode 100644 index 00000000..ddbe4ee7 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Domain/Interfaces/IRegister.cs @@ -0,0 +1,8 @@ +namespace ReviewService.Interfaces; + +public interface IRegister +{ + DateTime CreatedAt { get; } + DateTime? UpdatedAt { get; set; } + DateTime? DeletedAt { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Domain/ReviewService.Domain.csproj b/apps/merchant-api/ReviewService/src/ReviewService.Domain/ReviewService.Domain.csproj new file mode 100644 index 00000000..94749b2a --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Domain/ReviewService.Domain.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + ReviewService + + + + + + + diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Context/Concretes/ProductReviewContext.cs b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Context/Concretes/ProductReviewContext.cs new file mode 100644 index 00000000..f9444e26 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Context/Concretes/ProductReviewContext.cs @@ -0,0 +1,11 @@ +using MongoDB.Driver; +using ReviewService.Concretes; +using ReviewService.Infrastructure.Context.Interfaces; + +namespace ReviewService.Infrastructure.Context.Concretes; + +public class ProductReviewContext(IMongoDatabase database) : IContext +{ + public IMongoCollection Collection { get; set; } + = database.GetCollection("product_review"); +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Context/Interfaces/IContext.cs b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Context/Interfaces/IContext.cs new file mode 100644 index 00000000..84482335 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Context/Interfaces/IContext.cs @@ -0,0 +1,9 @@ +using MongoDB.Driver; +using ReviewService.Interfaces; + +namespace ReviewService.Infrastructure.Context.Interfaces; + +public interface IContext where T : IEntity +{ + IMongoCollection Collection { get; set; } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Program.cs b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Program.cs new file mode 100644 index 00000000..e5dff12b --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Bases/BaseRepository.cs b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Bases/BaseRepository.cs new file mode 100644 index 00000000..4e7fbb75 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Bases/BaseRepository.cs @@ -0,0 +1,72 @@ +using MongoDB.Driver; +using ReviewService.Infrastructure.Context.Interfaces; +using ReviewService.Infrastructure.Repositories.Interfaces; +using ReviewService.Interfaces; + +namespace ReviewService.Infrastructure.Repositories.Bases; + +public class BaseRepository(IContext context) : IRepository where T : IEntity +{ + public virtual async Task GetByIdAsync(Guid id) + { + var filter = Builders.Filter + .Eq(entity => entity.Id, id); + var entity = await context.Collection.Find(filter).FirstOrDefaultAsync(); + return entity; + } + + public async Task> GetAllAsync(int pageNumber, int pageSize) + { + var filter = Builders.Filter + .Eq(entity => entity.IsActive, true); + var collection = await context.Collection.Find(filter) + .SortBy(e => e.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Limit(pageSize).ToListAsync(); + return collection; + } + + public async Task CountAsync() + { + var filter = Builders.Filter + .Eq(entity => entity.IsActive, true); + var quantity = await context.Collection.Find(filter).CountDocumentsAsync(); + return (uint)quantity; + } + + public async Task AddAsync(T entity) + { + try + { + await context.Collection.InsertOneAsync(entity); + return entity; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public async Task UpdateAsync(Guid id, T entity, UpdateDefinition update) + { + entity.UpdatedAt = DateTime.UtcNow; + + var filter = Builders.Filter + .Eq(e => e.Id, id); + + var updateDefinition = update.Set(e => e.UpdatedAt, DateTime.UtcNow); + + var response = await context.Collection.UpdateOneAsync(filter, updateDefinition); + return (int)response.ModifiedCount; + } + + public async Task DeleteAsync(Guid id) + { + var filter = Builders.Filter + .Eq(e => e.Id, id); + + var result = await context.Collection.DeleteOneAsync(filter); + return (int)result.DeletedCount; + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Concretes/ProductReviewRepository.cs b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Concretes/ProductReviewRepository.cs new file mode 100644 index 00000000..c09d3f39 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Concretes/ProductReviewRepository.cs @@ -0,0 +1,19 @@ +using MongoDB.Driver; +using ReviewService.Concretes; +using ReviewService.Infrastructure.Context.Interfaces; +using ReviewService.Infrastructure.Repositories.Bases; + +namespace ReviewService.Infrastructure.Repositories.Concretes; + +public class ProductReviewRepository(IContext context) : BaseRepository(context) +{ + private readonly IContext _context = context; + + public override async Task GetByIdAsync(Guid id) + { + var filter = Builders.Filter + .Eq(entity => entity.ProductId, id); + var entity = await _context.Collection.Find(filter).FirstOrDefaultAsync(); + return entity; + } +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Interfaces/IRepository.cs b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Interfaces/IRepository.cs new file mode 100644 index 00000000..d4e2658e --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/Repositories/Interfaces/IRepository.cs @@ -0,0 +1,14 @@ +using MongoDB.Driver; +using ReviewService.Interfaces; + +namespace ReviewService.Infrastructure.Repositories.Interfaces; + +public interface IRepository where T : IEntity +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(int pageNumber, int pageSize); + Task CountAsync(); + Task AddAsync(T entity); + Task UpdateAsync(Guid id, T entity, UpdateDefinition update); + Task DeleteAsync(Guid id); +} \ No newline at end of file diff --git a/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/ReviewService.Infrastructure.csproj b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/ReviewService.Infrastructure.csproj new file mode 100644 index 00000000..5e45a840 --- /dev/null +++ b/apps/merchant-api/ReviewService/src/ReviewService.Infrastructure/ReviewService.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/apps/merchant-api/ReviewService/test/TestProject1/TestProject1.csproj b/apps/merchant-api/ReviewService/test/TestProject1/TestProject1.csproj new file mode 100644 index 00000000..39c01e67 --- /dev/null +++ b/apps/merchant-api/ReviewService/test/TestProject1/TestProject1.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + diff --git a/apps/merchant-api/ReviewService/test/TestProject1/UnitTest1.cs b/apps/merchant-api/ReviewService/test/TestProject1/UnitTest1.cs new file mode 100644 index 00000000..77d2dc6f --- /dev/null +++ b/apps/merchant-api/ReviewService/test/TestProject1/UnitTest1.cs @@ -0,0 +1,15 @@ +namespace TestProject1; + +public class Tests +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Test1() + { + Assert.Pass(); + } +} \ No newline at end of file diff --git a/apps/merchant-api/docker-compose.yml b/apps/merchant-api/docker-compose.yml index f34f6c89..aabe9001 100644 --- a/apps/merchant-api/docker-compose.yml +++ b/apps/merchant-api/docker-compose.yml @@ -148,6 +148,43 @@ services: depends_on: rabbitmq: condition: service_healthy + + reviews_db: + image: mongo:7.0.16-rc1 + restart: always + ports: + - "${REVIEW_DB_PORT}:${REVIEW_DB_PORT}" + environment: + MONGO_INITDB_ROOT_USERNAME: ${REVIEW_DB_USER} + MONGO_INITDB_ROOT_PASSWORD: ${REVIEW_DB_PASS} + volumes: + - mongodb_data:/data/db + networks: + - merchant-network + + review_service: + build: + context: . + dockerfile: ReviewService/Dockerfile + ports: + - "5500:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - REVIEW_DB_PORT=${REVIEW_DB_PORT} + - REVIEW_DB_USER=${REVIEW_DB_USER} + - REVIEW_DB_PASS=${REVIEW_DB_PASS} + - MONGODB_URI=${MONGODB_URI} + - RABBITMQ_CONNECTION_STRING=${RABBITMQ_CONNECTION_STRING} + - RABBITMQ_USER=${RABBITMQ_USER} + - RABBITMQ_PASS=${RABBITMQ_PASS} + - RABBITMQ_HOST=${RABBITMQ_HOST} + - RABBITMQ_PORT=${RABBITMQ_PORT} + - RABBITMQ_VHOST=${RABBITMQ_VHOST} + networks: + - merchant-network + depends_on: + rabbitmq: + condition: service_healthy networks: merchant-network: @@ -156,3 +193,4 @@ networks: volumes: postgres_data: rabbitmq_data: + mongodb_data: \ No newline at end of file diff --git a/apps/merchant-api/merchant-api.sln b/apps/merchant-api/merchant-api.sln index 2d0708e0..defdce0a 100644 --- a/apps/merchant-api/merchant-api.sln +++ b/apps/merchant-api/merchant-api.sln @@ -3,77 +3,91 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.002.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "ApiGateway\ApiGateway.csproj", "{72FEA5ED-F129-4180-AEB1-B635528E5773}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "ApiGateway\ApiGateway.csproj", "{1071D398-B0D1-4C77-AC18-E8E2792A7B38}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MerchantCommon", "MerchantCommon", "{E65BEF82-5698-437E-847B-FFA6BD340DB2}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MerchantCommon", "MerchantCommon", "{8D623CEF-6CAA-4220-A85D-C1768B1602B4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMqMessaging", "MerchantCommon\RabbitMqMessaging\RabbitMqMessaging.csproj", "{C7D3696D-4B48-465E-AEE9-5124ACED816F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMqMessaging", "MerchantCommon\RabbitMqMessaging\RabbitMqMessaging.csproj", "{2E740634-8267-4EA5-9C18-9C89487E7A0F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Commons", "MerchantCommon\Commons\Commons.csproj", "{641D5831-29F7-4E30-ADA3-2B278479F4C3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Commons", "MerchantCommon\Commons\Commons.csproj", "{86D9DEF0-38DC-4559-8E1F-93687B4B9291}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PaymentService", "PaymentService", "{D4DE45EE-E027-43ED-BB50-BF5136664DD4}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PaymentService", "PaymentService", "{A91C63E4-FAE1-4D99-9BD0-880A5202BAD3}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EC25300E-BE2E-4987-B7A1-AC8AA9380F2D}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D8A3298A-2E52-4DE1-84B2-97D2ED1A3680}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Domain", "PaymentService\src\PaymentService.Domain\PaymentService.Domain.csproj", "{3696BCB5-5C0A-4060-B079-579A9D8D2A6E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Domain", "PaymentService\src\PaymentService.Domain\PaymentService.Domain.csproj", "{9A6CD5DF-72C7-4997-894A-EE2294EE1917}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Application", "PaymentService\src\PaymentService.Application\PaymentService.Application.csproj", "{AA82A51C-5725-44D4-9265-6A05F8AC4DD2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Application", "PaymentService\src\PaymentService.Application\PaymentService.Application.csproj", "{DF0F7F3F-08A8-4393-8846-900BC1D518B0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Infrastructure", "PaymentService\src\PaymentService.Infrastructure\PaymentService.Infrastructure.csproj", "{1DFAC371-9AF7-427B-9483-2A5140EBB620}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Infrastructure", "PaymentService\src\PaymentService.Infrastructure\PaymentService.Infrastructure.csproj", "{3F75D64C-9B03-4FC1-A46E-11F6A5881EE7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Commons", "PaymentService\src\PaymentService.Commons\PaymentService.Commons.csproj", "{B08D111D-B003-419C-AE8A-145FE1571105}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Api", "PaymentService\src\PaymentService.Api\PaymentService.Api.csproj", "{F8A61614-D1C9-4EB2-979B-A9AABDBBCD58}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService.Api", "PaymentService\src\PaymentService.Api\PaymentService.Api.csproj", "{46F79B66-C7ED-4F03-9169-BB7FC5F9F552}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "InventoryService", "InventoryService", "{50C4F989-1DC5-4085-B0DA-6ABE54D57CE3}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "InventoryService", "InventoryService", "{B72BE68D-0106-46A6-A281-D912E90703C3}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4B66FB5F-B6AC-4097-BFFA-C613967F34EC}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{57E1E4D9-98FE-424E-AC8D-47C50537CDC8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryServiceTests", "InventoryService\test\InventoryServiceTests\InventoryServiceTests.csproj", "{35374A82-8786-4817-A4F1-1CF317C16597}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryServiceTests", "InventoryService\test\InventoryServiceTests\InventoryServiceTests.csproj", "{2D0FCE69-11D5-410C-9879-B3969C4BCF9A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{613127B5-4792-4540-A58F-A415ED4A4BC9}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{00EE6191-1D03-460D-A6B8-41C5A13C8CBE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Intraestructure", "InventoryService\src\InventoryService.Intraestructure\InventoryService.Intraestructure.csproj", "{8B9A75A4-1A4D-43F4-9EBB-A8A5937452B4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Intraestructure", "InventoryService\src\InventoryService.Intraestructure\InventoryService.Intraestructure.csproj", "{D9BF7615-A829-4CE4-A790-DB79F6C0595A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Api", "InventoryService\src\InventoryService.Api\InventoryService.Api.csproj", "{E91C4C31-2C6C-4100-9388-B35857919DF6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Api", "InventoryService\src\InventoryService.Api\InventoryService.Api.csproj", "{9839B241-AB3D-4E9B-B41C-DE3B8DD93942}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Commons", "InventoryService\src\InventoryService.Commons\InventoryService.Commons.csproj", "{F40D7457-0273-4B86-AA99-C313BFB6F6E7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Commons", "InventoryService\src\InventoryService.Commons\InventoryService.Commons.csproj", "{D3CC172A-6019-4202-8AF3-7B9B979F2760}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Domain", "InventoryService\src\InventoryService.Domain\InventoryService.Domain.csproj", "{0A9442B1-53A1-4B26-B033-A893ADF6BCEE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Domain", "InventoryService\src\InventoryService.Domain\InventoryService.Domain.csproj", "{40BAFDCF-0608-4668-B73F-FC542F1CFD26}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Application", "InventoryService\src\InventoryService.Application\InventoryService.Application.csproj", "{8C094A65-8558-4A18-8BBC-DBD1DD35A686}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InventoryService.Application", "InventoryService\src\InventoryService.Application\InventoryService.Application.csproj", "{C0B6DF50-9333-49AF-9142-51974051630C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NotificationService", "NotificationService", "{A4C9FED3-73F2-4A05-91BC-142B785F3950}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NotificationService", "NotificationService", "{079C8990-4FA9-468B-8067-79E100741DE8}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3117A4F1-7B05-45FC-A0E1-11230D16C81E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D735030A-0EF5-4CB5-809B-FF434BCEDA4F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationServiceTests", "NotificationService\test\NotificationServiceTests\NotificationServiceTests.csproj", "{02157519-DF43-4CF7-BE6B-ED2B4FF80AE4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationServiceTests", "NotificationService\test\NotificationServiceTests\NotificationServiceTests.csproj", "{F2F21161-AC70-4D04-AE19-6A601745E59C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0400C5C6-540C-4605-B88F-31F33B7FFB86}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FD01570B-0EA5-4B29-8E23-FA1E55AE892B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationService.Domain", "NotificationService\src\NotificationService.Domain\NotificationService.Domain.csproj", "{B179A8B9-504C-477D-90FA-ACFF227324CE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationService.Domain", "NotificationService\src\NotificationService.Domain\NotificationService.Domain.csproj", "{8B2020A5-F742-4467-BAAC-B7C6257FE9F6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationService.Api", "NotificationService\src\NotificationService.Api\NotificationService.Api.csproj", "{4F58EDA0-3365-4FF6-9BE3-6C9F08557899}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationService.Api", "NotificationService\src\NotificationService.Api\NotificationService.Api.csproj", "{8F253C80-A291-4D9E-9B6D-AB6EB4BB7CFA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationService.Application", "NotificationService\src\NotificationService.Application\NotificationService.Application.csproj", "{4A754162-490A-4F15-8F0B-899825BF81E9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationService.Application", "NotificationService\src\NotificationService.Application\NotificationService.Application.csproj", "{2C5F0523-5D96-4CF2-9484-817FB00B86C6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationService.Infraestructure", "NotificationService\src\NotificationService.Infraestructure\NotificationService.Infraestructure.csproj", "{5C3CD3EA-1A14-4858-AC9D-A9FBF9AB37FA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationService.Infraestructure", "NotificationService\src\NotificationService.Infraestructure\NotificationService.Infraestructure.csproj", "{6F9FB955-89AE-484B-8720-334FF714A4A3}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UserService", "UserService", "{0203A6FF-E1E7-4E8A-BB56-6378848CFA4B}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UserService", "UserService", "{319BC3FC-1248-45FA-8534-0335BEA81214}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{33B15287-39C5-4A0B-A7C5-B27CD258DE2A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E188FFD0-FEA7-4313-8974-5F1363B207E6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserServiceTests", "UserService\test\UserServiceTests\UserServiceTests.csproj", "{EEF9B93B-ABB4-41EE-BF16-08F472813D92}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserServiceTests", "UserService\test\UserServiceTests\UserServiceTests.csproj", "{A7E1CE21-AA1C-48C6-BA21-81F61AD00F31}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{91E2E3B6-E17D-4F53-9424-0D7AD55C49D3}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D85044CE-2EFE-4A9D-9B6E-7616B852EFA6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserService.Infrastructure", "UserService\src\UserService.Infrastructure\UserService.Infrastructure.csproj", "{382E5761-DBC8-4729-ABC1-358FF4DE1731}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserService.Infrastructure", "UserService\src\UserService.Infrastructure\UserService.Infrastructure.csproj", "{FA6C583D-261E-4E7D-9D03-9C3AE0DB1134}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserService.Domain", "UserService\src\UserService.Domain\UserService.Domain.csproj", "{439B1504-61A7-497B-9145-BAE6FA303100}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserService.Domain", "UserService\src\UserService.Domain\UserService.Domain.csproj", "{A0E551F7-D4C3-4A0F-AD69-C1CA4AC3F88B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserService.Application", "UserService\src\UserService.Application\UserService.Application.csproj", "{001F148D-8AC4-4294-B45D-8480C111FA75}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserService.Application", "UserService\src\UserService.Application\UserService.Application.csproj", "{E1896DA1-3A31-44A1-8304-AD52A1632070}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserService.Api", "UserService\src\UserService.Api\UserService.Api.csproj", "{47E5DFF9-CC6D-41CC-9953-5EA6A03E7E2E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserService.Api", "UserService\src\UserService.Api\UserService.Api.csproj", "{E6447958-3E7D-4FB7-8185-20B5497F5FB5}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReviewService", "ReviewService", "{E289E7B1-492E-412E-A093-4577015078EF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4129CE38-CE3C-473E-9E66-3F5729896349}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReviewService.Api", "ReviewService\src\ReviewService.Api\ReviewService.Api.csproj", "{3E11FBD4-DA05-4418-9AAA-BAE7E0069A26}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReviewService.Application", "ReviewService\src\ReviewService.Application\ReviewService.Application.csproj", "{9B34679B-ED3F-45B5-A532-FDE38EFF3E62}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReviewService.Domain", "ReviewService\src\ReviewService.Domain\ReviewService.Domain.csproj", "{0A9EF588-5765-4097-A235-C3427BFF6A6D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReviewService.Infrastructure", "ReviewService\src\ReviewService.Infrastructure\ReviewService.Infrastructure.csproj", "{AB0DC86C-1020-4EF7-9286-2367CD0F5A9D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C1F05FCE-5D03-40D9-A714-2993DDBAE03F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "ReviewService\test\TestProject1\TestProject1.csproj", "{33986658-70F9-4A3F-9061-2B0E668CEEB6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -81,139 +95,161 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {72FEA5ED-F129-4180-AEB1-B635528E5773}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72FEA5ED-F129-4180-AEB1-B635528E5773}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72FEA5ED-F129-4180-AEB1-B635528E5773}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72FEA5ED-F129-4180-AEB1-B635528E5773}.Release|Any CPU.Build.0 = Release|Any CPU - {C7D3696D-4B48-465E-AEE9-5124ACED816F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7D3696D-4B48-465E-AEE9-5124ACED816F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C7D3696D-4B48-465E-AEE9-5124ACED816F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7D3696D-4B48-465E-AEE9-5124ACED816F}.Release|Any CPU.Build.0 = Release|Any CPU - {641D5831-29F7-4E30-ADA3-2B278479F4C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {641D5831-29F7-4E30-ADA3-2B278479F4C3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {641D5831-29F7-4E30-ADA3-2B278479F4C3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {641D5831-29F7-4E30-ADA3-2B278479F4C3}.Release|Any CPU.Build.0 = Release|Any CPU - {3696BCB5-5C0A-4060-B079-579A9D8D2A6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3696BCB5-5C0A-4060-B079-579A9D8D2A6E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3696BCB5-5C0A-4060-B079-579A9D8D2A6E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3696BCB5-5C0A-4060-B079-579A9D8D2A6E}.Release|Any CPU.Build.0 = Release|Any CPU - {AA82A51C-5725-44D4-9265-6A05F8AC4DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA82A51C-5725-44D4-9265-6A05F8AC4DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA82A51C-5725-44D4-9265-6A05F8AC4DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA82A51C-5725-44D4-9265-6A05F8AC4DD2}.Release|Any CPU.Build.0 = Release|Any CPU - {1DFAC371-9AF7-427B-9483-2A5140EBB620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1DFAC371-9AF7-427B-9483-2A5140EBB620}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1DFAC371-9AF7-427B-9483-2A5140EBB620}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1DFAC371-9AF7-427B-9483-2A5140EBB620}.Release|Any CPU.Build.0 = Release|Any CPU - {B08D111D-B003-419C-AE8A-145FE1571105}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B08D111D-B003-419C-AE8A-145FE1571105}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B08D111D-B003-419C-AE8A-145FE1571105}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B08D111D-B003-419C-AE8A-145FE1571105}.Release|Any CPU.Build.0 = Release|Any CPU - {46F79B66-C7ED-4F03-9169-BB7FC5F9F552}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {46F79B66-C7ED-4F03-9169-BB7FC5F9F552}.Debug|Any CPU.Build.0 = Debug|Any CPU - {46F79B66-C7ED-4F03-9169-BB7FC5F9F552}.Release|Any CPU.ActiveCfg = Release|Any CPU - {46F79B66-C7ED-4F03-9169-BB7FC5F9F552}.Release|Any CPU.Build.0 = Release|Any CPU - {2D0FCE69-11D5-410C-9879-B3969C4BCF9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D0FCE69-11D5-410C-9879-B3969C4BCF9A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D0FCE69-11D5-410C-9879-B3969C4BCF9A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D0FCE69-11D5-410C-9879-B3969C4BCF9A}.Release|Any CPU.Build.0 = Release|Any CPU - {D9BF7615-A829-4CE4-A790-DB79F6C0595A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D9BF7615-A829-4CE4-A790-DB79F6C0595A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D9BF7615-A829-4CE4-A790-DB79F6C0595A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D9BF7615-A829-4CE4-A790-DB79F6C0595A}.Release|Any CPU.Build.0 = Release|Any CPU - {9839B241-AB3D-4E9B-B41C-DE3B8DD93942}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9839B241-AB3D-4E9B-B41C-DE3B8DD93942}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9839B241-AB3D-4E9B-B41C-DE3B8DD93942}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9839B241-AB3D-4E9B-B41C-DE3B8DD93942}.Release|Any CPU.Build.0 = Release|Any CPU - {D3CC172A-6019-4202-8AF3-7B9B979F2760}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3CC172A-6019-4202-8AF3-7B9B979F2760}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3CC172A-6019-4202-8AF3-7B9B979F2760}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3CC172A-6019-4202-8AF3-7B9B979F2760}.Release|Any CPU.Build.0 = Release|Any CPU - {40BAFDCF-0608-4668-B73F-FC542F1CFD26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {40BAFDCF-0608-4668-B73F-FC542F1CFD26}.Debug|Any CPU.Build.0 = Debug|Any CPU - {40BAFDCF-0608-4668-B73F-FC542F1CFD26}.Release|Any CPU.ActiveCfg = Release|Any CPU - {40BAFDCF-0608-4668-B73F-FC542F1CFD26}.Release|Any CPU.Build.0 = Release|Any CPU - {C0B6DF50-9333-49AF-9142-51974051630C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C0B6DF50-9333-49AF-9142-51974051630C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C0B6DF50-9333-49AF-9142-51974051630C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C0B6DF50-9333-49AF-9142-51974051630C}.Release|Any CPU.Build.0 = Release|Any CPU - {F2F21161-AC70-4D04-AE19-6A601745E59C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F2F21161-AC70-4D04-AE19-6A601745E59C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F2F21161-AC70-4D04-AE19-6A601745E59C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F2F21161-AC70-4D04-AE19-6A601745E59C}.Release|Any CPU.Build.0 = Release|Any CPU - {8B2020A5-F742-4467-BAAC-B7C6257FE9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B2020A5-F742-4467-BAAC-B7C6257FE9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B2020A5-F742-4467-BAAC-B7C6257FE9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B2020A5-F742-4467-BAAC-B7C6257FE9F6}.Release|Any CPU.Build.0 = Release|Any CPU - {8F253C80-A291-4D9E-9B6D-AB6EB4BB7CFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F253C80-A291-4D9E-9B6D-AB6EB4BB7CFA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F253C80-A291-4D9E-9B6D-AB6EB4BB7CFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F253C80-A291-4D9E-9B6D-AB6EB4BB7CFA}.Release|Any CPU.Build.0 = Release|Any CPU - {2C5F0523-5D96-4CF2-9484-817FB00B86C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2C5F0523-5D96-4CF2-9484-817FB00B86C6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C5F0523-5D96-4CF2-9484-817FB00B86C6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2C5F0523-5D96-4CF2-9484-817FB00B86C6}.Release|Any CPU.Build.0 = Release|Any CPU - {6F9FB955-89AE-484B-8720-334FF714A4A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F9FB955-89AE-484B-8720-334FF714A4A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F9FB955-89AE-484B-8720-334FF714A4A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F9FB955-89AE-484B-8720-334FF714A4A3}.Release|Any CPU.Build.0 = Release|Any CPU - {A7E1CE21-AA1C-48C6-BA21-81F61AD00F31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A7E1CE21-AA1C-48C6-BA21-81F61AD00F31}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A7E1CE21-AA1C-48C6-BA21-81F61AD00F31}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A7E1CE21-AA1C-48C6-BA21-81F61AD00F31}.Release|Any CPU.Build.0 = Release|Any CPU - {FA6C583D-261E-4E7D-9D03-9C3AE0DB1134}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA6C583D-261E-4E7D-9D03-9C3AE0DB1134}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA6C583D-261E-4E7D-9D03-9C3AE0DB1134}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA6C583D-261E-4E7D-9D03-9C3AE0DB1134}.Release|Any CPU.Build.0 = Release|Any CPU - {A0E551F7-D4C3-4A0F-AD69-C1CA4AC3F88B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A0E551F7-D4C3-4A0F-AD69-C1CA4AC3F88B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A0E551F7-D4C3-4A0F-AD69-C1CA4AC3F88B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A0E551F7-D4C3-4A0F-AD69-C1CA4AC3F88B}.Release|Any CPU.Build.0 = Release|Any CPU - {E1896DA1-3A31-44A1-8304-AD52A1632070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E1896DA1-3A31-44A1-8304-AD52A1632070}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E1896DA1-3A31-44A1-8304-AD52A1632070}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E1896DA1-3A31-44A1-8304-AD52A1632070}.Release|Any CPU.Build.0 = Release|Any CPU - {E6447958-3E7D-4FB7-8185-20B5497F5FB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E6447958-3E7D-4FB7-8185-20B5497F5FB5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E6447958-3E7D-4FB7-8185-20B5497F5FB5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E6447958-3E7D-4FB7-8185-20B5497F5FB5}.Release|Any CPU.Build.0 = Release|Any CPU + {1071D398-B0D1-4C77-AC18-E8E2792A7B38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1071D398-B0D1-4C77-AC18-E8E2792A7B38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1071D398-B0D1-4C77-AC18-E8E2792A7B38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1071D398-B0D1-4C77-AC18-E8E2792A7B38}.Release|Any CPU.Build.0 = Release|Any CPU + {2E740634-8267-4EA5-9C18-9C89487E7A0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E740634-8267-4EA5-9C18-9C89487E7A0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E740634-8267-4EA5-9C18-9C89487E7A0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E740634-8267-4EA5-9C18-9C89487E7A0F}.Release|Any CPU.Build.0 = Release|Any CPU + {86D9DEF0-38DC-4559-8E1F-93687B4B9291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86D9DEF0-38DC-4559-8E1F-93687B4B9291}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86D9DEF0-38DC-4559-8E1F-93687B4B9291}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86D9DEF0-38DC-4559-8E1F-93687B4B9291}.Release|Any CPU.Build.0 = Release|Any CPU + {9A6CD5DF-72C7-4997-894A-EE2294EE1917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A6CD5DF-72C7-4997-894A-EE2294EE1917}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A6CD5DF-72C7-4997-894A-EE2294EE1917}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A6CD5DF-72C7-4997-894A-EE2294EE1917}.Release|Any CPU.Build.0 = Release|Any CPU + {DF0F7F3F-08A8-4393-8846-900BC1D518B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF0F7F3F-08A8-4393-8846-900BC1D518B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF0F7F3F-08A8-4393-8846-900BC1D518B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF0F7F3F-08A8-4393-8846-900BC1D518B0}.Release|Any CPU.Build.0 = Release|Any CPU + {3F75D64C-9B03-4FC1-A46E-11F6A5881EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F75D64C-9B03-4FC1-A46E-11F6A5881EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F75D64C-9B03-4FC1-A46E-11F6A5881EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F75D64C-9B03-4FC1-A46E-11F6A5881EE7}.Release|Any CPU.Build.0 = Release|Any CPU + {F8A61614-D1C9-4EB2-979B-A9AABDBBCD58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8A61614-D1C9-4EB2-979B-A9AABDBBCD58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8A61614-D1C9-4EB2-979B-A9AABDBBCD58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8A61614-D1C9-4EB2-979B-A9AABDBBCD58}.Release|Any CPU.Build.0 = Release|Any CPU + {35374A82-8786-4817-A4F1-1CF317C16597}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35374A82-8786-4817-A4F1-1CF317C16597}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35374A82-8786-4817-A4F1-1CF317C16597}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35374A82-8786-4817-A4F1-1CF317C16597}.Release|Any CPU.Build.0 = Release|Any CPU + {8B9A75A4-1A4D-43F4-9EBB-A8A5937452B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B9A75A4-1A4D-43F4-9EBB-A8A5937452B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B9A75A4-1A4D-43F4-9EBB-A8A5937452B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B9A75A4-1A4D-43F4-9EBB-A8A5937452B4}.Release|Any CPU.Build.0 = Release|Any CPU + {E91C4C31-2C6C-4100-9388-B35857919DF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E91C4C31-2C6C-4100-9388-B35857919DF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E91C4C31-2C6C-4100-9388-B35857919DF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E91C4C31-2C6C-4100-9388-B35857919DF6}.Release|Any CPU.Build.0 = Release|Any CPU + {F40D7457-0273-4B86-AA99-C313BFB6F6E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F40D7457-0273-4B86-AA99-C313BFB6F6E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F40D7457-0273-4B86-AA99-C313BFB6F6E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F40D7457-0273-4B86-AA99-C313BFB6F6E7}.Release|Any CPU.Build.0 = Release|Any CPU + {0A9442B1-53A1-4B26-B033-A893ADF6BCEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A9442B1-53A1-4B26-B033-A893ADF6BCEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A9442B1-53A1-4B26-B033-A893ADF6BCEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A9442B1-53A1-4B26-B033-A893ADF6BCEE}.Release|Any CPU.Build.0 = Release|Any CPU + {8C094A65-8558-4A18-8BBC-DBD1DD35A686}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C094A65-8558-4A18-8BBC-DBD1DD35A686}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C094A65-8558-4A18-8BBC-DBD1DD35A686}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C094A65-8558-4A18-8BBC-DBD1DD35A686}.Release|Any CPU.Build.0 = Release|Any CPU + {02157519-DF43-4CF7-BE6B-ED2B4FF80AE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02157519-DF43-4CF7-BE6B-ED2B4FF80AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02157519-DF43-4CF7-BE6B-ED2B4FF80AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02157519-DF43-4CF7-BE6B-ED2B4FF80AE4}.Release|Any CPU.Build.0 = Release|Any CPU + {B179A8B9-504C-477D-90FA-ACFF227324CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B179A8B9-504C-477D-90FA-ACFF227324CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B179A8B9-504C-477D-90FA-ACFF227324CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B179A8B9-504C-477D-90FA-ACFF227324CE}.Release|Any CPU.Build.0 = Release|Any CPU + {4F58EDA0-3365-4FF6-9BE3-6C9F08557899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F58EDA0-3365-4FF6-9BE3-6C9F08557899}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F58EDA0-3365-4FF6-9BE3-6C9F08557899}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F58EDA0-3365-4FF6-9BE3-6C9F08557899}.Release|Any CPU.Build.0 = Release|Any CPU + {4A754162-490A-4F15-8F0B-899825BF81E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A754162-490A-4F15-8F0B-899825BF81E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A754162-490A-4F15-8F0B-899825BF81E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A754162-490A-4F15-8F0B-899825BF81E9}.Release|Any CPU.Build.0 = Release|Any CPU + {5C3CD3EA-1A14-4858-AC9D-A9FBF9AB37FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C3CD3EA-1A14-4858-AC9D-A9FBF9AB37FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C3CD3EA-1A14-4858-AC9D-A9FBF9AB37FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C3CD3EA-1A14-4858-AC9D-A9FBF9AB37FA}.Release|Any CPU.Build.0 = Release|Any CPU + {EEF9B93B-ABB4-41EE-BF16-08F472813D92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEF9B93B-ABB4-41EE-BF16-08F472813D92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEF9B93B-ABB4-41EE-BF16-08F472813D92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEF9B93B-ABB4-41EE-BF16-08F472813D92}.Release|Any CPU.Build.0 = Release|Any CPU + {382E5761-DBC8-4729-ABC1-358FF4DE1731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {382E5761-DBC8-4729-ABC1-358FF4DE1731}.Debug|Any CPU.Build.0 = Debug|Any CPU + {382E5761-DBC8-4729-ABC1-358FF4DE1731}.Release|Any CPU.ActiveCfg = Release|Any CPU + {382E5761-DBC8-4729-ABC1-358FF4DE1731}.Release|Any CPU.Build.0 = Release|Any CPU + {439B1504-61A7-497B-9145-BAE6FA303100}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {439B1504-61A7-497B-9145-BAE6FA303100}.Debug|Any CPU.Build.0 = Debug|Any CPU + {439B1504-61A7-497B-9145-BAE6FA303100}.Release|Any CPU.ActiveCfg = Release|Any CPU + {439B1504-61A7-497B-9145-BAE6FA303100}.Release|Any CPU.Build.0 = Release|Any CPU + {001F148D-8AC4-4294-B45D-8480C111FA75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {001F148D-8AC4-4294-B45D-8480C111FA75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {001F148D-8AC4-4294-B45D-8480C111FA75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {001F148D-8AC4-4294-B45D-8480C111FA75}.Release|Any CPU.Build.0 = Release|Any CPU + {47E5DFF9-CC6D-41CC-9953-5EA6A03E7E2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47E5DFF9-CC6D-41CC-9953-5EA6A03E7E2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47E5DFF9-CC6D-41CC-9953-5EA6A03E7E2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47E5DFF9-CC6D-41CC-9953-5EA6A03E7E2E}.Release|Any CPU.Build.0 = Release|Any CPU + {3E11FBD4-DA05-4418-9AAA-BAE7E0069A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E11FBD4-DA05-4418-9AAA-BAE7E0069A26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E11FBD4-DA05-4418-9AAA-BAE7E0069A26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E11FBD4-DA05-4418-9AAA-BAE7E0069A26}.Release|Any CPU.Build.0 = Release|Any CPU + {9B34679B-ED3F-45B5-A532-FDE38EFF3E62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B34679B-ED3F-45B5-A532-FDE38EFF3E62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B34679B-ED3F-45B5-A532-FDE38EFF3E62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B34679B-ED3F-45B5-A532-FDE38EFF3E62}.Release|Any CPU.Build.0 = Release|Any CPU + {0A9EF588-5765-4097-A235-C3427BFF6A6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A9EF588-5765-4097-A235-C3427BFF6A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A9EF588-5765-4097-A235-C3427BFF6A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A9EF588-5765-4097-A235-C3427BFF6A6D}.Release|Any CPU.Build.0 = Release|Any CPU + {AB0DC86C-1020-4EF7-9286-2367CD0F5A9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB0DC86C-1020-4EF7-9286-2367CD0F5A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB0DC86C-1020-4EF7-9286-2367CD0F5A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB0DC86C-1020-4EF7-9286-2367CD0F5A9D}.Release|Any CPU.Build.0 = Release|Any CPU + {33986658-70F9-4A3F-9061-2B0E668CEEB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33986658-70F9-4A3F-9061-2B0E668CEEB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33986658-70F9-4A3F-9061-2B0E668CEEB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33986658-70F9-4A3F-9061-2B0E668CEEB6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {C7D3696D-4B48-465E-AEE9-5124ACED816F} = {E65BEF82-5698-437E-847B-FFA6BD340DB2} - {641D5831-29F7-4E30-ADA3-2B278479F4C3} = {E65BEF82-5698-437E-847B-FFA6BD340DB2} - {EC25300E-BE2E-4987-B7A1-AC8AA9380F2D} = {D4DE45EE-E027-43ED-BB50-BF5136664DD4} - {3696BCB5-5C0A-4060-B079-579A9D8D2A6E} = {EC25300E-BE2E-4987-B7A1-AC8AA9380F2D} - {AA82A51C-5725-44D4-9265-6A05F8AC4DD2} = {EC25300E-BE2E-4987-B7A1-AC8AA9380F2D} - {1DFAC371-9AF7-427B-9483-2A5140EBB620} = {EC25300E-BE2E-4987-B7A1-AC8AA9380F2D} - {B08D111D-B003-419C-AE8A-145FE1571105} = {EC25300E-BE2E-4987-B7A1-AC8AA9380F2D} - {46F79B66-C7ED-4F03-9169-BB7FC5F9F552} = {EC25300E-BE2E-4987-B7A1-AC8AA9380F2D} - {57E1E4D9-98FE-424E-AC8D-47C50537CDC8} = {B72BE68D-0106-46A6-A281-D912E90703C3} - {2D0FCE69-11D5-410C-9879-B3969C4BCF9A} = {57E1E4D9-98FE-424E-AC8D-47C50537CDC8} - {00EE6191-1D03-460D-A6B8-41C5A13C8CBE} = {B72BE68D-0106-46A6-A281-D912E90703C3} - {D9BF7615-A829-4CE4-A790-DB79F6C0595A} = {00EE6191-1D03-460D-A6B8-41C5A13C8CBE} - {9839B241-AB3D-4E9B-B41C-DE3B8DD93942} = {00EE6191-1D03-460D-A6B8-41C5A13C8CBE} - {D3CC172A-6019-4202-8AF3-7B9B979F2760} = {00EE6191-1D03-460D-A6B8-41C5A13C8CBE} - {40BAFDCF-0608-4668-B73F-FC542F1CFD26} = {00EE6191-1D03-460D-A6B8-41C5A13C8CBE} - {C0B6DF50-9333-49AF-9142-51974051630C} = {00EE6191-1D03-460D-A6B8-41C5A13C8CBE} - {D735030A-0EF5-4CB5-809B-FF434BCEDA4F} = {079C8990-4FA9-468B-8067-79E100741DE8} - {F2F21161-AC70-4D04-AE19-6A601745E59C} = {D735030A-0EF5-4CB5-809B-FF434BCEDA4F} - {FD01570B-0EA5-4B29-8E23-FA1E55AE892B} = {079C8990-4FA9-468B-8067-79E100741DE8} - {8B2020A5-F742-4467-BAAC-B7C6257FE9F6} = {FD01570B-0EA5-4B29-8E23-FA1E55AE892B} - {8F253C80-A291-4D9E-9B6D-AB6EB4BB7CFA} = {FD01570B-0EA5-4B29-8E23-FA1E55AE892B} - {2C5F0523-5D96-4CF2-9484-817FB00B86C6} = {FD01570B-0EA5-4B29-8E23-FA1E55AE892B} - {6F9FB955-89AE-484B-8720-334FF714A4A3} = {FD01570B-0EA5-4B29-8E23-FA1E55AE892B} - {E188FFD0-FEA7-4313-8974-5F1363B207E6} = {319BC3FC-1248-45FA-8534-0335BEA81214} - {A7E1CE21-AA1C-48C6-BA21-81F61AD00F31} = {E188FFD0-FEA7-4313-8974-5F1363B207E6} - {D85044CE-2EFE-4A9D-9B6E-7616B852EFA6} = {319BC3FC-1248-45FA-8534-0335BEA81214} - {FA6C583D-261E-4E7D-9D03-9C3AE0DB1134} = {D85044CE-2EFE-4A9D-9B6E-7616B852EFA6} - {A0E551F7-D4C3-4A0F-AD69-C1CA4AC3F88B} = {D85044CE-2EFE-4A9D-9B6E-7616B852EFA6} - {E1896DA1-3A31-44A1-8304-AD52A1632070} = {D85044CE-2EFE-4A9D-9B6E-7616B852EFA6} - {E6447958-3E7D-4FB7-8185-20B5497F5FB5} = {D85044CE-2EFE-4A9D-9B6E-7616B852EFA6} + {2E740634-8267-4EA5-9C18-9C89487E7A0F} = {8D623CEF-6CAA-4220-A85D-C1768B1602B4} + {86D9DEF0-38DC-4559-8E1F-93687B4B9291} = {8D623CEF-6CAA-4220-A85D-C1768B1602B4} + {D8A3298A-2E52-4DE1-84B2-97D2ED1A3680} = {A91C63E4-FAE1-4D99-9BD0-880A5202BAD3} + {9A6CD5DF-72C7-4997-894A-EE2294EE1917} = {D8A3298A-2E52-4DE1-84B2-97D2ED1A3680} + {DF0F7F3F-08A8-4393-8846-900BC1D518B0} = {D8A3298A-2E52-4DE1-84B2-97D2ED1A3680} + {3F75D64C-9B03-4FC1-A46E-11F6A5881EE7} = {D8A3298A-2E52-4DE1-84B2-97D2ED1A3680} + {F8A61614-D1C9-4EB2-979B-A9AABDBBCD58} = {D8A3298A-2E52-4DE1-84B2-97D2ED1A3680} + {4B66FB5F-B6AC-4097-BFFA-C613967F34EC} = {50C4F989-1DC5-4085-B0DA-6ABE54D57CE3} + {35374A82-8786-4817-A4F1-1CF317C16597} = {4B66FB5F-B6AC-4097-BFFA-C613967F34EC} + {613127B5-4792-4540-A58F-A415ED4A4BC9} = {50C4F989-1DC5-4085-B0DA-6ABE54D57CE3} + {8B9A75A4-1A4D-43F4-9EBB-A8A5937452B4} = {613127B5-4792-4540-A58F-A415ED4A4BC9} + {E91C4C31-2C6C-4100-9388-B35857919DF6} = {613127B5-4792-4540-A58F-A415ED4A4BC9} + {F40D7457-0273-4B86-AA99-C313BFB6F6E7} = {613127B5-4792-4540-A58F-A415ED4A4BC9} + {0A9442B1-53A1-4B26-B033-A893ADF6BCEE} = {613127B5-4792-4540-A58F-A415ED4A4BC9} + {8C094A65-8558-4A18-8BBC-DBD1DD35A686} = {613127B5-4792-4540-A58F-A415ED4A4BC9} + {3117A4F1-7B05-45FC-A0E1-11230D16C81E} = {A4C9FED3-73F2-4A05-91BC-142B785F3950} + {02157519-DF43-4CF7-BE6B-ED2B4FF80AE4} = {3117A4F1-7B05-45FC-A0E1-11230D16C81E} + {0400C5C6-540C-4605-B88F-31F33B7FFB86} = {A4C9FED3-73F2-4A05-91BC-142B785F3950} + {B179A8B9-504C-477D-90FA-ACFF227324CE} = {0400C5C6-540C-4605-B88F-31F33B7FFB86} + {4F58EDA0-3365-4FF6-9BE3-6C9F08557899} = {0400C5C6-540C-4605-B88F-31F33B7FFB86} + {4A754162-490A-4F15-8F0B-899825BF81E9} = {0400C5C6-540C-4605-B88F-31F33B7FFB86} + {5C3CD3EA-1A14-4858-AC9D-A9FBF9AB37FA} = {0400C5C6-540C-4605-B88F-31F33B7FFB86} + {33B15287-39C5-4A0B-A7C5-B27CD258DE2A} = {0203A6FF-E1E7-4E8A-BB56-6378848CFA4B} + {EEF9B93B-ABB4-41EE-BF16-08F472813D92} = {33B15287-39C5-4A0B-A7C5-B27CD258DE2A} + {91E2E3B6-E17D-4F53-9424-0D7AD55C49D3} = {0203A6FF-E1E7-4E8A-BB56-6378848CFA4B} + {382E5761-DBC8-4729-ABC1-358FF4DE1731} = {91E2E3B6-E17D-4F53-9424-0D7AD55C49D3} + {439B1504-61A7-497B-9145-BAE6FA303100} = {91E2E3B6-E17D-4F53-9424-0D7AD55C49D3} + {001F148D-8AC4-4294-B45D-8480C111FA75} = {91E2E3B6-E17D-4F53-9424-0D7AD55C49D3} + {47E5DFF9-CC6D-41CC-9953-5EA6A03E7E2E} = {91E2E3B6-E17D-4F53-9424-0D7AD55C49D3} + {4129CE38-CE3C-473E-9E66-3F5729896349} = {E289E7B1-492E-412E-A093-4577015078EF} + {3E11FBD4-DA05-4418-9AAA-BAE7E0069A26} = {4129CE38-CE3C-473E-9E66-3F5729896349} + {9B34679B-ED3F-45B5-A532-FDE38EFF3E62} = {4129CE38-CE3C-473E-9E66-3F5729896349} + {0A9EF588-5765-4097-A235-C3427BFF6A6D} = {4129CE38-CE3C-473E-9E66-3F5729896349} + {AB0DC86C-1020-4EF7-9286-2367CD0F5A9D} = {4129CE38-CE3C-473E-9E66-3F5729896349} + {C1F05FCE-5D03-40D9-A714-2993DDBAE03F} = {E289E7B1-492E-412E-A093-4577015078EF} + {33986658-70F9-4A3F-9061-2B0E668CEEB6} = {C1F05FCE-5D03-40D9-A714-2993DDBAE03F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {CEEB046B-CF4C-42DB-9AD3-6F50A0D68CC1} + SolutionGuid = {E1CFB179-5B18-4F5E-817A-3C705DD3773A} EndGlobalSection EndGlobal diff --git a/apps/merchant-web/src/app/page.tsx b/apps/merchant-web/src/app/page.tsx index d304d0c8..a762311c 100644 --- a/apps/merchant-web/src/app/page.tsx +++ b/apps/merchant-web/src/app/page.tsx @@ -33,9 +33,9 @@ export default function Home() { product.brand, product.isLiked, product.images, + product.rating, product.productVariants, product.categories, - product.productReviews ) ) ); diff --git a/apps/merchant-web/src/app/product-details/[id]/page.tsx b/apps/merchant-web/src/app/product-details/[id]/page.tsx index 7bbd4a37..91341966 100644 --- a/apps/merchant-web/src/app/product-details/[id]/page.tsx +++ b/apps/merchant-web/src/app/product-details/[id]/page.tsx @@ -8,6 +8,7 @@ import axiosInstance from "@/request/AxiosConfig"; import { ShoppingItemProvider } from "@/contexts/ShoppingItemContext"; import { ProductDetail } from "@/components/product-detail/ProductDetail"; +import { ReviewsProvider } from "@/contexts/ReviewsContext"; export default function ProductDetails() { const params = useParams(); @@ -34,7 +35,9 @@ export default function ProductDetails() { return product ? ( - + + + ) : (

Loading product details...

diff --git a/apps/merchant-web/src/commons/entities/concretes/Product.ts b/apps/merchant-web/src/commons/entities/concretes/Product.ts index a1f398d3..2c2673db 100644 --- a/apps/merchant-web/src/commons/entities/concretes/Product.ts +++ b/apps/merchant-web/src/commons/entities/concretes/Product.ts @@ -2,7 +2,6 @@ import { UUID } from "crypto"; import ProductVariant from "./ProductVariant"; import Category from "./Category"; import Image from "./Image"; -import ProductReview from "./ProductReview"; import {boolean} from "zod"; @@ -15,9 +14,9 @@ export default class Product { brand: string; isLiked: boolean; images: Array; + rating: number; productVariants: Array; categories: Array; - productReviews: Array; lowStockThreshold?: number; constructor( @@ -29,9 +28,9 @@ export default class Product { brand: string, isLiked: boolean, images: Array, + rating: number, productVariants: Array, categories: Array, - productReviews: Array, lowStockThreshold?: number ) { this.id = id; @@ -43,8 +42,8 @@ export default class Product { this.brand = brand; this.categories = categories; this.images = images; + this.rating = rating; this.productVariants = productVariants; - this.productReviews = productReviews; this.categories = categories; this.lowStockThreshold = lowStockThreshold; } diff --git a/apps/merchant-web/src/components/RatingSelector.tsx b/apps/merchant-web/src/components/RatingSelector.tsx index 4d8c8fbc..e0afe801 100644 --- a/apps/merchant-web/src/components/RatingSelector.tsx +++ b/apps/merchant-web/src/components/RatingSelector.tsx @@ -4,67 +4,132 @@ import { MdOutlineStarBorder, MdOutlineStarHalf, } from "react-icons/md"; -import { useState } from "react"; +import {useState} from "react"; interface RatingSelectorProps { rating: number; + setRating: (number) => void; + handleChange?: () => void; + showTotalInfo?: boolean; + totalReviews?: number; + horizontalAlignment?: boolean; + isEditable?: boolean; } export default function RatingSelector({ - rating, + rating, handleChange, setRating, showTotalInfo = true, totalReviews, horizontalAlignment = true, isEditable = true }: Readonly) { - const [currentRating, setCurrentRating] = useState(rating); const size = 24; const color = "#FFC633"; - const fullStars = Math.floor(currentRating); - const hasHalfStar = currentRating % 1 !== 0; + const [hoverRating, setHoverRating] = useState(0); - const handleStarClick = (index: number) => { - const newRating = index + 1; - setCurrentRating(newRating); + const fullStars = Math.floor(hoverRating || rating); + const hasHalfStar = (hoverRating || rating) % 1 !== 0; + + const handleStarClick = (index) => { + if (isEditable) { + const newRating = index + 1; + setRating(newRating); + } + }; + + const handleMouseEnter = (index) => { + if (isEditable) setHoverRating(index + 1); + }; + + const handleMouseLeave = () => { + setHoverRating(0); }; return ( -
- {Array.from({ length: 5 }).map((_, index) => { - if (index < fullStars) { - return ( - handleStarClick(index)} - /> - ); - } else if (index === fullStars && hasHalfStar) { - return ( - handleStarClick(index)} - /> - ); - } else { - return ( - handleStarClick(index)} - /> - ); - } - })} -
); } diff --git a/apps/merchant-web/src/components/catalog/ProductsView.tsx b/apps/merchant-web/src/components/catalog/ProductsView.tsx index 1f4a2405..d52a9b06 100644 --- a/apps/merchant-web/src/components/catalog/ProductsView.tsx +++ b/apps/merchant-web/src/components/catalog/ProductsView.tsx @@ -51,9 +51,9 @@ export default function ProductsView({ fetchConfig, view }: Readonly) { product.brand, product.isLiked, product.images, + product.rating, product.productVariants, product.categories, - product.productReviews ) ) ); diff --git a/apps/merchant-web/src/components/contact-us/ContactUsForm.tsx b/apps/merchant-web/src/components/contact-us/ContactUsForm.tsx index c92a0b9a..7b96338f 100644 --- a/apps/merchant-web/src/components/contact-us/ContactUsForm.tsx +++ b/apps/merchant-web/src/components/contact-us/ContactUsForm.tsx @@ -8,17 +8,7 @@ import { ContactFormData } from "@/schemes/contact-us-form/ContactUsDto"; import styles from "@/styles/contact-us/ContactUsForm.module.css" import {createContactUsMessage} from "@/request/ContactUsMessageRequest"; import {ContactFormSchema, defaultContactData} from "@/schemes/contact-us-form/ContactFormSchema"; -import { z } from "zod"; - -const validateWithZod = (schema: z.ZodSchema) => (values: any) => { - try { - schema.parse(values); - return {}; - } catch (error) { - const zodErrors = (error as z.ZodError).flatten(); - return zodErrors.fieldErrors; - } -}; +import {validateWithZod} from "@/utils/ZodFunctions"; const ContactForm: React.FC = () => { const formik: FormikProps = useFormik({ @@ -107,4 +97,4 @@ const ContactForm: React.FC = () => { ); }; -export default ContactForm; \ No newline at end of file +export default ContactForm; diff --git a/apps/merchant-web/src/components/general/ProductCard.tsx b/apps/merchant-web/src/components/general/ProductCard.tsx index 0725cdd9..4e39ca55 100644 --- a/apps/merchant-web/src/components/general/ProductCard.tsx +++ b/apps/merchant-web/src/components/general/ProductCard.tsx @@ -72,7 +72,7 @@ export const ProductCard = ({ product, type, onDislike }: Props) => { $ {product.price}

- 0 + {product.rating}

diff --git a/apps/merchant-web/src/components/product-detail/ProductDetail.tsx b/apps/merchant-web/src/components/product-detail/ProductDetail.tsx index 308623d6..3c6d2fa6 100644 --- a/apps/merchant-web/src/components/product-detail/ProductDetail.tsx +++ b/apps/merchant-web/src/components/product-detail/ProductDetail.tsx @@ -1,7 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import RatingSelector from "@/components/RatingSelector"; import ImagesSection, { Image, } from "@/components/product-details/ImagesSection"; @@ -13,6 +12,9 @@ import axiosInstance from "@/request/AxiosConfig"; import { useShoppingCart } from "@/contexts/ShoppingCartContext"; import { AttributeSelector } from "./AttributeSelector"; import styles from "@/styles/products/ProductDetails.module.css"; +import ReviewModal from "@/components/reviews/ReviewModal"; +import ReviewCard from "@/components/reviews/ReviewCard"; +import ReviewsSection from "@/components/product-detail/ReviewsSection"; export const ProductDetail = () => { const [isFavorite, setIsFavorite] = useState(false); @@ -107,62 +109,65 @@ export const ProductDetail = () => { } return ( -
- -
-

{product.name}

- - - {price > 0 && } -

{product.description}

-
- {attributes && attributes.length > 0 ? ( - attributes.map((attribute) => { - return ( -
-

Select {attribute.name}

- { - chooseAttribute(attribute.name, value); - }} - /> -
- ); - }) - ) : ( -

No variants available

- )} - {error && ( -
+
+
); }; diff --git a/apps/merchant-web/src/components/product-detail/ReviewsSection.tsx b/apps/merchant-web/src/components/product-detail/ReviewsSection.tsx new file mode 100644 index 00000000..22e661e8 --- /dev/null +++ b/apps/merchant-web/src/components/product-detail/ReviewsSection.tsx @@ -0,0 +1,27 @@ +import {useReviewsContext} from "@/contexts/ReviewsContext"; +import ReviewCard from "@/components/reviews/ReviewCard"; +import ReviewSectionStyle from "@/styles/reviews/ReviewSection.module.css" +import PageSelector from "@/components/PageSelector"; +import ReviewModal from "@/components/reviews/ReviewModal"; + +export default function ReviewsSection() { + const {reviews, isLoading, totalReviews, page, setPage} = useReviewsContext(); + return ( +
+
+

All reviews ({totalReviews})

+ +
+
+ {reviews?.reviews.items.map((item, index) => ( + + + ))} +
+ {reviews == null &&

It looks like no one has shared their thoughts about this product yet. Why not be the first to leave a review and help others make their choice?

} + {reviews?.reviews.totalPages > 1 && } +
+ ) +} diff --git a/apps/merchant-web/src/components/reviews/ReviewCard.tsx b/apps/merchant-web/src/components/reviews/ReviewCard.tsx new file mode 100644 index 00000000..4b3a4123 --- /dev/null +++ b/apps/merchant-web/src/components/reviews/ReviewCard.tsx @@ -0,0 +1,66 @@ +import RatingSelector from "@/components/RatingSelector"; +import ReviewCardStyle from "@/styles/reviews/ReviewsCard.module.css" +import {Skeleton} from "@nextui-org/skeleton"; +import {FormatDateLiterary} from "@/utils/DateFormatter"; + +interface Props { + review: Review + isLoading: boolean, +} +export default function ReviewCard({review, isLoading}: Readonly) { + const showSkeleton = () => { + return ( +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ + +
+ + +
+ + +
+ +
+ ) + } + + const card = () => { + return ( +
+ {}} + showTotalInfo={false}> + +

{review.clientName}

+ {review.comment && review.comment.trim() !== "" && ( +

"{review.comment}"

+ )} + Posted on {FormatDateLiterary(review.createdAt)} +
+ ) + } + + return isLoading ? showSkeleton() : card(); +} diff --git a/apps/merchant-web/src/components/reviews/ReviewModal.tsx b/apps/merchant-web/src/components/reviews/ReviewModal.tsx new file mode 100644 index 00000000..6a6ea4b0 --- /dev/null +++ b/apps/merchant-web/src/components/reviews/ReviewModal.tsx @@ -0,0 +1,136 @@ +import {Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, useDisclosure} from "@nextui-org/react"; +import {useState} from "react"; +import {toast} from "sonner"; +import {useRouter} from "next/navigation"; +import {FormikProps, useFormik} from "formik"; +import RatingSelector from "@/components/RatingSelector"; +import {EditableInput} from "@/components/atoms/inputs/EditableInput"; +import {defaultReviewFormData, ReviewFormData, ReviewFormSchema} from "@/schemes/reviews/ReviewFormSchema"; +import {validateWithZod} from "@/utils/ZodFunctions"; +import ModalStyle from "@/styles/store-catalog/Modal.module.css"; +import axiosInstance from "@/request/AxiosConfig"; +import {useAuth} from "@/commons/context/AuthContext"; +import {useReviewsContext} from "@/contexts/ReviewsContext"; +import ReviewSectionStyle from "@/styles/reviews/ReviewSection.module.css"; + +interface Props { + type?: "button" | "rating-selector" +} + +export default function ReviewModal({type = "rating-selector"}: Readonly) { + const [ratingForm, setRatingForm] = useState(0); + const {productId, rating, totalReviews, refreshReviews, isAbleToAdd} = useReviewsContext(); + const {isOpen, onOpen, onOpenChange, onClose} = useDisclosure(); + const auth = useAuth() + const router = useRouter(); + + const formik: FormikProps = useFormik({ + initialValues: defaultReviewFormData, + validate: validateWithZod(ReviewFormSchema), + onSubmit: values => { + if (auth.user?.userId == null) { + router.replace("/login"); + } else { + axiosInstance.post(`/review/ProductReview/${productId}`, { + clientId: auth.user?.userId, + clientName: auth.user?.displayName, + rating: ratingForm, + comment: values.comment + }).catch(e => { + console.log(e); + e.response.data.errors.map(error => { + toast.error(error); + }) + toast.error(e.response.data.message); + }) + .finally(() => { + refreshReviews(); + onClose(); + }); + } + } + }); + + const getOpenType = () => { + switch (type) { + case "button": + return isAbleToAdd ? ( + Add review + ) : <>; + case "rating-selector": + return ( + {}} + totalReviews={totalReviews} + horizontalAlignment={true} + showTotalInfo={true} + isEditable={isAbleToAdd} + > + ); + default: + return (<>); + } + }; + + return ( + <> + {getOpenType()} + + + +

Add your review...

+
+ +
+ +
+ + +
+ + + + +
+
+ + ) +} diff --git a/apps/merchant-web/src/contexts/ReviewsContext.tsx b/apps/merchant-web/src/contexts/ReviewsContext.tsx new file mode 100644 index 00000000..b384c363 --- /dev/null +++ b/apps/merchant-web/src/contexts/ReviewsContext.tsx @@ -0,0 +1,97 @@ +import {createContext, ReactNode, useContext, useEffect, useMemo, useState} from "react"; +import axiosInstance from "@/request/AxiosConfig"; +import {toast} from "sonner"; +import {useAuth} from "@/commons/context/AuthContext"; + +interface Type { + isAbleToAdd: boolean; + isLoading: boolean; + productId: string; + rating: number; + totalReviews: number; + reviews: Reviews | undefined; + page: number, + setRating: (number) => void; + refreshReviews: () => void; + setPage: (number) => void; + setIsLoading: (boolean) => void; + setIsAbleToAdd:(boolean) => void; +} + +interface ReviewsProviderProps { + children: ReactNode; + productId: string; +} + +export const ReviewsContext = createContext(undefined); + +export const useReviewsContext = () => { + const context = useContext(ReviewsContext); + if (!context) { + throw new Error( + "useFiltersContext must be used within a FiltersProvider" + ); + } + return context; +}; + +export const ReviewsProvider = ({children, productId}: ReviewsProviderProps) => { + const authContext = useAuth(); + const pageSize = 6; + const [isAbleToAdd, setIsAbleToAdd] = useState(true); + const [page, setPage] = useState(0); + const [rating, setRating] = useState(0); + const [reviews, setReviews] = useState(undefined); + const [totalReviews, setTotalReviews] = useState(0); + const [refresh, setRefresh] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + setIsLoading(true); + loadReviews(); + }, [refresh, page]); + + const refreshReviews = () => { + setRefresh(!refresh); + } + + const loadReviews = () => { + axiosInstance.get(`/review/ProductReview/${productId}?page=${page + 1}&pageSize=${pageSize}&clientId=${authContext.user?.userId ?? ""}`) + .then(response => response.data) + .then(data => { + setRating(data.data.averageRating); + setTotalReviews(data.data.reviews.totalItems); + setReviews(data.data); + setIsAbleToAdd(data.data.clientAbleToAdd); + setTimeout(() => setIsLoading(false), 300); + }) + .catch(e => { + if (e.response.status != 404) { + toast.error(e.message); + } + }) + } + + const objValue = useMemo(() => ({ + isAbleToAdd, + isLoading, + productId, + rating, + totalReviews, + reviews, + page, + setRating, + refreshReviews, + setPage, + setIsLoading, + setIsAbleToAdd + }), [productId, rating, reviews, totalReviews, isLoading, page, isAbleToAdd]); + + return ( + + {children} + + ); +} diff --git a/apps/merchant-web/src/models/Reviews.ts b/apps/merchant-web/src/models/Reviews.ts new file mode 100644 index 00000000..070844ae --- /dev/null +++ b/apps/merchant-web/src/models/Reviews.ts @@ -0,0 +1,20 @@ + +interface Reviews { + productId: string; + averageRating: number; + reviews: { + items: Array + totalCount: number; + page: number; + pageSize: number; + totalItems: number; + totalPages: number; + } +} + +interface Review { + clientName: string; + rating: number; + comment: string; + createdAt: string; +} diff --git a/apps/merchant-web/src/schemes/reviews/ReviewFormSchema.ts b/apps/merchant-web/src/schemes/reviews/ReviewFormSchema.ts new file mode 100644 index 00000000..d3bb2707 --- /dev/null +++ b/apps/merchant-web/src/schemes/reviews/ReviewFormSchema.ts @@ -0,0 +1,12 @@ +import z from "zod" + +export const ReviewFormSchema = z.object({ + comment: z.string() + .max(500, "Your opinion could only contain 500 characters") +}); + +export type ReviewFormData = z.infer; + +export const defaultReviewFormData: ReviewFormData = { + comment: "" +} diff --git a/apps/merchant-web/src/styles/reviews/ReviewSection.module.css b/apps/merchant-web/src/styles/reviews/ReviewSection.module.css new file mode 100644 index 00000000..67adeb8d --- /dev/null +++ b/apps/merchant-web/src/styles/reviews/ReviewSection.module.css @@ -0,0 +1,49 @@ + +.gridContainer { + display: grid; + grid-template-columns: repeat(2, 1fr); + justify-items: center; + gap: 40px; + width: 100%; +} + +.addButton { + font-size: 14px; + font-weight: 300; + border-radius: 8px; +} + +.addButton:hover { + text-decoration: underline; + color: var(--primary-400); + cursor: pointer; +} + +.titleContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +@media (max-width: 820px) { + .gridContainer { + grid-template-columns: 1fr; + } +} + +.container { + padding: 0 24px; + margin-top: 40px; + margin-bottom: 24px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.noReviewsMessage { + text-align: center; + margin: 24px; + padding-bottom: 24px; +} diff --git a/apps/merchant-web/src/styles/reviews/ReviewsCard.module.css b/apps/merchant-web/src/styles/reviews/ReviewsCard.module.css new file mode 100644 index 00000000..7b2b6ed7 --- /dev/null +++ b/apps/merchant-web/src/styles/reviews/ReviewsCard.module.css @@ -0,0 +1,41 @@ + +.container { + border: 1px solid var(--foreground-secondary); + width: 45vw; + min-width: 320px; + min-height: 150px; + border-radius: 12px; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 12px; +} + +.skeleton { + background-color: var(--gray-200); + width: 45vw; + height: 150px; + border-radius: 12px; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 10px; +} + +.roundedElement { + border-radius: 8px; +} + +.name { + font-size: 24px; + font-weight: 400; +} + +.dateStyle { + margin-top: 8px; + font-size: 14px; +} + +.paragraph { + font-size: 14px; +} diff --git a/apps/merchant-web/src/utils/DateFormatter.ts b/apps/merchant-web/src/utils/DateFormatter.ts new file mode 100644 index 00000000..dbd43e1a --- /dev/null +++ b/apps/merchant-web/src/utils/DateFormatter.ts @@ -0,0 +1,8 @@ +export const FormatDateLiterary = (isoDate) => { + const date = new Date(isoDate); + return new Intl.DateTimeFormat("en-US", { + month: "long", + day: "numeric", + year: "numeric" + }).format(date); +} diff --git a/apps/merchant-web/src/utils/ZodFunctions.ts b/apps/merchant-web/src/utils/ZodFunctions.ts new file mode 100644 index 00000000..289ba099 --- /dev/null +++ b/apps/merchant-web/src/utils/ZodFunctions.ts @@ -0,0 +1,11 @@ +import {z} from "zod"; + +export const validateWithZod = (schema: z.ZodSchema) => (values: any) => { + try { + schema.parse(values); + return {}; + } catch (error) { + const zodErrors = (error as z.ZodError).flatten(); + return zodErrors.fieldErrors; + } +};