Files
prefab-page-detail/Prefab.Web/Gateway/Products.cs
2025-10-27 21:39:50 -04:00

208 lines
8.4 KiB
C#

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<Result<ProductListingModel>> 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<ProductListingModel>.Success(listing);
}
catch (ValidationException validationException)
{
var problem = ResultProblem.FromValidation(validationException);
return Result<ProductListingModel>.Failure(problem);
}
catch (DomainException domainException)
{
var problem = ResultProblem.Unexpected(domainException.Message, domainException, HttpStatusCode.UnprocessableEntity);
return Result<ProductListingModel>.Failure(problem);
}
catch (Exception exception)
{
var problem = ResultProblem.Unexpected("Failed to retrieve category products.", exception);
return Result<ProductListingModel>.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<string>(),
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)
{
public async Task<Result<ProductDisplayModel>> Get(string slug, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(slug))
{
return Result<ProductDisplayModel>.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<ProductDisplayModel>.Success(model);
}
catch (ValidationException validationException)
{
var problem = ResultProblem.FromValidation(validationException);
return Result<ProductDisplayModel>.Failure(problem);
}
catch (CatalogNotFoundException)
{
return Result<ProductDisplayModel>.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<ProductDisplayModel>.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<ProductDisplayModel>.Failure(problem);
}
catch (RemoteProblemException ex)
{
var problem = ResultProblem.Unexpected("Failed to retrieve product.", ex, ex.StatusCode);
return Result<ProductDisplayModel>.Failure(problem);
}
catch (Exception exception)
{
var problem = ResultProblem.Unexpected("Failed to retrieve product.", exception);
return Result<ProductDisplayModel>.Failure(problem);
}
}
}
}
internal static class ProductClientExtensions
{
public static Task<GetProductDetail.Response> GetProductConfig(
this IProductClient productClient,
string slug,
CancellationToken cancellationToken) =>
productClient.GetProductDetail(slug, cancellationToken);
}