using System.Net; using FluentValidation; using Prefab.Domain.Exceptions; using Prefab.Endpoints; using Prefab.Shared.Catalog; using Prefab.Shared.Catalog.Products; using Prefab.Shared; using Prefab.Web.Client.Models.Catalog; using Prefab.Web.Client.Models.Shared; using Prefab.Web.Client.ViewModels.Catalog; using Prefab.Web.Client.Services; using UrlBuilder = Prefab.Web.UrlPolicy.UrlPolicy; using CatalogNotFoundException = Prefab.Catalog.Domain.Exceptions.CatalogNotFoundException; namespace Prefab.Web.Gateway; public static class Products { public sealed class Endpoints : IEndpointRegistrar { public void MapEndpoints(IEndpointRouteBuilder endpoints) { endpoints.MapGet( "/bff/catalog/categories/{slug}/models", async (ListingService service, string slug, CancellationToken cancellationToken) => { var result = await service.GetCategoryProducts(slug, cancellationToken); var status = result.Problem?.StatusCode ?? (result.IsSuccess ? StatusCodes.Status200OK : StatusCodes.Status500InternalServerError); return Results.Json(result, statusCode: status); }) .WithTags(nameof(Products)); endpoints.MapGet( "/bff/catalog/products/{slug}", async (DetailService service, string slug, CancellationToken cancellationToken) => { var result = await service.Get(slug, cancellationToken); var status = result.Problem?.StatusCode ?? (result.IsSuccess ? StatusCodes.Status200OK : StatusCodes.Status500InternalServerError); return Results.Json(result, statusCode: status); }) .WithTags(nameof(Products)); } } public sealed class ListingService(IModuleClient moduleClient) : IProductListingService { public async Task> GetCategoryProducts(string categorySlug, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(categorySlug); try { var response = await moduleClient.Product.GetCategoryModels( categorySlug.Trim(), page: null, pageSize: null, sort: null, direction: null, cancellationToken); var listing = MapListing(response.Result); return Result.Success(listing); } catch (ValidationException validationException) { var problem = ResultProblem.FromValidation(validationException); return Result.Failure(problem); } catch (DomainException domainException) { var problem = ResultProblem.Unexpected(domainException.Message, domainException, HttpStatusCode.UnprocessableEntity); return Result.Failure(problem); } catch (Exception exception) { var problem = ResultProblem.Unexpected("Failed to retrieve category products.", exception); return Result.Failure(problem); } } private static ProductListingModel MapListing(GetCategoryModels.CategoryProductsResponse payload) { var category = payload.Category; var categoryModel = new ProductCategoryModel { Id = category.Id, Name = category.Name, Slug = category.Slug, Description = category.Description, HeroImageUrl = category.HeroImageUrl, Icon = category.Icon }; var cards = payload.Products .Select(product => new ProductCardModel { Id = product.Id, Title = product.Name, Url = UrlBuilder.BrowseProduct(product.Slug), Slug = product.Slug, PrimaryImageUrl = product.PrimaryImageUrl, CategoryName = category.Name, CategoryUrl = UrlBuilder.BrowseCategory(category.Slug), FromPrice = product.FromPrice.HasValue ? new MoneyModel { Amount = product.FromPrice.Value } : null, IsPriced = product.FromPrice.HasValue, OldPrice = null, IsOnSale = false, Rating = 0, ReviewCount = 0, Badges = new List(), LastModifiedOn = product.LastModifiedOn }) .ToList(); return new ProductListingModel { Category = categoryModel, Products = cards, Total = payload.Total, Page = payload.Page, PageSize = payload.PageSize }; } } public sealed class DetailService(IModuleClient moduleClient) : IProductDisplayService { public async Task> Get(string slug, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(slug)) { return Result.Failure(new ResultProblem { Title = "Invalid product slug.", Detail = "Product slug is required.", StatusCode = StatusCodes.Status400BadRequest }); } var normalizedSlug = slug.Trim(); try { var response = await moduleClient.Product.GetProductConfig(normalizedSlug, cancellationToken); var product = response.Product ?? throw new InvalidOperationException("Product payload was empty."); var model = ProductDisplayModel.From(product); return Result.Success(model); } catch (ValidationException validationException) { var problem = ResultProblem.FromValidation(validationException); return Result.Failure(problem); } catch (CatalogNotFoundException) { return Result.Failure(new ResultProblem { Title = "Product not found.", Detail = $"Product '{normalizedSlug}' was not found.", StatusCode = StatusCodes.Status404NotFound }); } catch (RemoteProblemException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return Result.Failure(new ResultProblem { Title = "Product not found.", Detail = $"Product '{normalizedSlug}' was not found.", StatusCode = StatusCodes.Status404NotFound }); } catch (DomainException domainException) { var problem = ResultProblem.Unexpected(domainException.Message, domainException, HttpStatusCode.UnprocessableEntity); return Result.Failure(problem); } catch (RemoteProblemException ex) { var problem = ResultProblem.Unexpected("Failed to retrieve product.", ex, ex.StatusCode); return Result.Failure(problem); } catch (Exception exception) { var problem = ResultProblem.Unexpected("Failed to retrieve product.", exception); return Result.Failure(problem); } } } } internal static class ProductClientExtensions { public static Task GetProductConfig( this IProductClient productClient, string slug, CancellationToken cancellationToken) => productClient.GetProductDetail(slug, cancellationToken); }