208 lines
8.4 KiB
C#
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) : IProductDisplayService
|
|
{
|
|
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);
|
|
}
|