This commit is contained in:
2025-10-27 21:39:50 -04:00
parent 31f723bea4
commit 2fecd5b315
22 changed files with 1198 additions and 14 deletions

View File

@@ -4,11 +4,13 @@ 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;
@@ -29,6 +31,18 @@ public static class Products
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));
}
}
@@ -117,4 +131,77 @@ public static class Products
};
}
}
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);
}

View File

@@ -37,7 +37,7 @@ public class Module : IModule
builder.Services.AddScoped<ICatalogDbContextFactory, CatalogDbContextFactory>();
builder.Services.AddHostedService<DataSeeder>();
//builder.Services.AddHostedService<DataSeeder>();
builder.Services.AddScoped<INotificationService, NotificationService>();
@@ -47,6 +47,7 @@ public class Module : IModule
builder.Services.AddScoped<ICategoriesPageService, Categories.PageService>();
builder.Services.AddScoped<Home.Service>();
builder.Services.AddScoped<Products.ListingService>();
builder.Services.AddScoped<Products.DetailService>();
builder.Services.AddScoped<IProductListingService>(sp => sp.GetRequiredService<Products.ListingService>());
builder.Services.AddScoped<IHomePageService>(sp => sp.GetRequiredService<Home.Service>());

View File

@@ -18,6 +18,14 @@ builder.AddPrefab();
var app = builder.Build();
app.UsePrefab();
// if (app.Environment.IsDevelopment())
// {
// using var scope = app.Services.CreateScope();
// var seeder = scope.ServiceProvider.GetRequiredService<Prefab.Catalog.Data.Seeder>();
// var db = scope.ServiceProvider.GetRequiredService<Prefab.Catalog.Data.IModuleDb>();
// await seeder.Execute(scope.ServiceProvider, CancellationToken.None);
// }
app.MapDefaultEndpoints();
// Configure the HTTP request pipeline.

View File

@@ -41,11 +41,11 @@ public static class UrlPolicy
/// <summary>
/// Builds a canonical catalog URL for navigating to a specific product detail page.
/// </summary>
/// <param name="productSlug">The slug of the product.</param>
public static string BrowseProduct(string productSlug)
/// <param name="slug">The slug of the product.</param>
public static string BrowseProduct(string slug)
{
ArgumentException.ThrowIfNullOrWhiteSpace(productSlug);
return $"/catalog/product?product-slug={Uri.EscapeDataString(productSlug)}";
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
return $"/browse/product?slug={Uri.EscapeDataString(slug)}";
}
/// <summary>

View File

@@ -6,8 +6,8 @@
}
},
"ConnectionStrings": {
"PrefabDb": "Server=127.0.0.1,14330;Initial Catalog=prefab-db;User ID=sa;Password=Nq7K3YR2;TrustServerCertificate=True;Encrypt=False;MultipleActiveResultSets=True",
"PrefabDbReadOnly": "Server=127.0.0.1,14330;Initial Catalog=prefab-db;User ID=sa;Password=Nq7K3YR2;TrustServerCertificate=True;Encrypt=False;MultipleActiveResultSets=True"
"PrefabDb": "Server=127.0.0.1,14331;Initial Catalog=prefab-db;User ID=sa;Password=Nq7K3YR2;TrustServerCertificate=True;Encrypt=False;MultipleActiveResultSets=True",
"PrefabDbReadOnly": "Server=127.0.0.1,14331;Initial Catalog=prefab-db;User ID=sa;Password=Nq7K3YR2;TrustServerCertificate=True;Encrypt=False;MultipleActiveResultSets=True"
},
"Prefab": {
"Catalog": {