using System.Net; using FluentValidation; using Prefab.Domain.Exceptions; using Prefab.Endpoints; using Prefab.Shared.Catalog; using Prefab.Shared.Catalog.Products; using Prefab.Web.Client.Models.Catalog; using Prefab.Web.Client.Models.Home; using Prefab.Web.Client.Models.Shared; using Prefab.Web.Client.Services; using Prefab.Web.Client.ViewModels.Catalog; using UrlBuilder = Prefab.Web.UrlPolicy.UrlPolicy; namespace Prefab.Web.Gateway; public static class Home { public sealed class Endpoints : IEndpointRegistrar { public void MapEndpoints(IEndpointRouteBuilder endpoints) { endpoints.MapGet( "/bff/home", async (Service service, CancellationToken cancellationToken) => { var result = await service.Get(cancellationToken); var status = result.Problem?.StatusCode ?? (result.IsSuccess ? StatusCodes.Status200OK : StatusCodes.Status500InternalServerError); return Results.Json(result, statusCode: status); }) .WithTags(nameof(Home)); } } public sealed class Service(IModuleClient moduleClient, IProductListingService listingService) : IHomePageService { private const int MaxCategories = 8; private const int MaxFeaturedProducts = 8; public async Task> Get(CancellationToken cancellationToken) { try { var response = await moduleClient.Product.GetCategoryTree(depth: 3, cancellationToken); var nodes = response.Categories ?? []; var leafNodes = Flatten(nodes) .Where(node => node.IsLeaf && !string.IsNullOrWhiteSpace(node.Slug)) .ToList(); var categories = new List(MaxCategories); var featuredProducts = new List(MaxFeaturedProducts); var seenCategoryIds = new HashSet(); var seenProductKeys = new HashSet(StringComparer.OrdinalIgnoreCase); await CollectAsync( OrderCandidates(leafNodes.Where(node => node.IsFeatured)), categories, featuredProducts, seenCategoryIds, seenProductKeys, cancellationToken); if (categories.Count < MaxCategories || featuredProducts.Count < MaxFeaturedProducts) { await CollectAsync( OrderCandidates(leafNodes.Where(node => !node.IsFeatured)), categories, featuredProducts, seenCategoryIds, seenProductKeys, cancellationToken); } var model = new HomePageModel { LatestCategories = categories, FeaturedProducts = featuredProducts }; return Result.Success(model); } 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 home page content.", exception); return Result.Failure(problem); } } private async Task CollectAsync( IEnumerable candidates, ICollection categories, ICollection featuredProducts, ISet seenCategoryIds, ISet seenProductKeys, CancellationToken cancellationToken) { foreach (var candidate in candidates) { if (categories.Count >= MaxCategories && featuredProducts.Count >= MaxFeaturedProducts) { break; } Result? listingResult = null; try { listingResult = await listingService.GetCategoryProducts(candidate.Slug, cancellationToken); } catch (DomainException) { continue; } if (!listingResult.IsSuccess || listingResult.Value is not { Products.Count: > 0 } listing) { continue; } if (categories.Count < MaxCategories && seenCategoryIds.Add(candidate.Id)) { categories.Add(new CategoryCardModel { Title = candidate.Name, Url = UrlBuilder.BrowseProducts(candidate.Slug), ImageUrl = string.IsNullOrWhiteSpace(candidate.HeroImageUrl) ? null : candidate.HeroImageUrl, SecondaryText = FormatProductCount(listing.Total) }); } if (featuredProducts.Count >= MaxFeaturedProducts) { continue; } foreach (var product in listing.Products) { var key = GetProductKey(product); if (!seenProductKeys.Add(key)) { continue; } featuredProducts.Add(product); if (featuredProducts.Count >= MaxFeaturedProducts) { break; } } } } private static string GetProductKey(ProductCardModel product) { if (!string.IsNullOrWhiteSpace(product.Sku)) { return product.Sku!; } if (!string.IsNullOrWhiteSpace(product.Url)) { return product.Url!; } return product.Title; } private static IEnumerable OrderCandidates( IEnumerable candidates) => candidates .OrderBy(candidate => candidate.DisplayOrder) .ThenBy(candidate => candidate.Name, StringComparer.OrdinalIgnoreCase); private static string FormatProductCount(int total) => total == 1 ? "1 Product" : $"{total} Products"; private static IEnumerable Flatten( IEnumerable nodes) { foreach (var node in nodes) { yield return node; if (node.Children is { Count: > 0 }) { foreach (var child in Flatten(node.Children)) { yield return child; } } } } } }