This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
namespace Prefab.Shared.Catalog.Products;
public abstract class GetCategory
{
public sealed record Request(string Slug);
public sealed record Response(GetCategoryModels.CategoryDetailDto Category);
}

View File

@@ -0,0 +1,50 @@
namespace Prefab.Shared.Catalog.Products;
public abstract class GetCategoryModels
{
public sealed record Request(
string Slug,
int? Page,
int? PageSize,
string? Sort,
string? Direction);
public sealed record Response(CategoryProductsResponse Result);
public sealed record CategoryProductsResponse(
CategoryDetailDto Category,
IReadOnlyList<ProductCardDto> Products,
int Total,
int Page,
int PageSize);
public sealed record CategoryDetailDto(
Guid Id,
string Name,
string Slug,
string? Description,
string? HeroImageUrl,
string? Icon,
bool IsLeaf,
IReadOnlyList<CategoryNodeDto> Children);
public sealed record CategoryNodeDto(
Guid Id,
string Name,
string Slug,
string? Description,
string? HeroImageUrl,
string? Icon,
int DisplayOrder,
bool IsFeatured,
bool IsLeaf,
IReadOnlyList<CategoryNodeDto> Children);
public sealed record ProductCardDto(
Guid Id,
string Name,
string Slug,
decimal? FromPrice,
string? PrimaryImageUrl,
DateTimeOffset LastModifiedOn);
}

View File

@@ -0,0 +1,8 @@
namespace Prefab.Shared.Catalog.Products;
public abstract class GetCategoryTree
{
public sealed record Request(int? Depth);
public sealed record Response(IReadOnlyList<GetCategoryModels.CategoryNodeDto> Categories);
}

View File

@@ -0,0 +1,77 @@
namespace Prefab.Shared.Catalog.Products;
public abstract class GetProductDetail
{
public enum OptionDataType
{
Choice = 0,
Number = 1
}
public enum PriceDeltaKind
{
Absolute = 0,
Percent = 1
}
public sealed record Response(ProductDetailDto Product);
public sealed record ProductDetailDto(
Guid Id,
string Name,
string Slug,
string? Description,
decimal? Price,
IReadOnlyList<OptionDefinitionDto> Options,
IReadOnlyList<SpecDto> Specs,
IReadOnlyDictionary<string, string>? GenericAttributes);
public sealed record OptionDefinitionDto(
Guid Id,
string Code,
string Name,
int DataType,
bool IsVariantAxis,
string? Unit,
decimal? Min,
decimal? Max,
decimal? Step,
decimal? PricePerUnit,
IReadOnlyList<OptionTierDto> Tiers,
IReadOnlyList<OptionValueDto> Values,
IReadOnlyList<RuleSetDto>? Rules);
public sealed record OptionValueDto(
Guid Id,
string Code,
string Label,
decimal? PriceDelta,
int PriceDeltaKind,
IReadOnlyList<RuleSetDto>? Rules);
public sealed record OptionTierDto(
decimal FromInclusive,
decimal? ToInclusive,
decimal UnitRate,
decimal? FlatDelta);
public sealed record RuleSetDto(
string Effect,
string Mode,
IReadOnlyList<ConditionDto> Conditions);
public sealed record ConditionDto(
string LeftOptionCode,
string Operator,
string? RightValueCode,
string? RightList,
decimal? RightNumber,
decimal? RightMin,
decimal? RightMax);
public sealed record SpecDto(
string Name,
string? Value,
decimal? NumericValue,
string? UnitCode);
}

View File

@@ -0,0 +1,19 @@
namespace Prefab.Shared.Catalog.Products;
public interface IProductClient
{
Task<GetCategoryTree.Response> GetCategoryTree(int? depth, CancellationToken cancellationToken);
Task<GetCategory.Response> GetCategory(string slug, CancellationToken cancellationToken);
Task<GetCategoryModels.Response> GetCategoryModels(string slug, int? page, int? pageSize, string? sort, string? direction, CancellationToken cancellationToken);
Task<GetProductDetail.Response> GetProductDetail(string slug, CancellationToken cancellationToken);
Task<QuotePrice.Response> QuotePrice(QuotePrice.Request request, CancellationToken cancellationToken);
}
public interface IPriceQuoteClient
{
Task<QuotePrice.Response> Quote(QuotePrice.Request request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,117 @@
using System.Net.Http.Json;
namespace Prefab.Shared.Catalog.Products;
public sealed class ProductClientHttp(HttpClient httpClient) : IProductClient, IPriceQuoteClient
{
public async Task<GetCategoryTree.Response> GetCategoryTree(int? depth, CancellationToken cancellationToken)
{
var trimmedDepth = depth is > 0 ? depth : null;
var path = "api/catalog/categories/tree";
if (trimmedDepth.HasValue)
{
path = $"{path}?depth={trimmedDepth.Value}";
}
using var response = await httpClient.GetAsync(path, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
throw new RemoteProblemException(response.StatusCode, body);
}
return await response.Content.ReadFromJsonAsync<GetCategoryTree.Response>(cancellationToken: cancellationToken)
?? throw new InvalidOperationException("Category tree response payload was empty.");
}
public async Task<GetCategory.Response> GetCategory(string slug, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
using var response = await httpClient.GetAsync($"api/catalog/categories/{slug.Trim()}", cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
throw new RemoteProblemException(response.StatusCode, body);
}
return await response.Content.ReadFromJsonAsync<GetCategory.Response>(cancellationToken: cancellationToken)
?? throw new InvalidOperationException("Category detail response payload was empty.");
}
public async Task<GetCategoryModels.Response> GetCategoryModels(string slug, int? page, int? pageSize, string? sort, string? direction, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
var trimmedSlug = slug.Trim();
var segments = new List<string>();
if (page is > 0)
{
segments.Add($"page={page.Value}");
}
if (pageSize is > 0)
{
segments.Add($"pageSize={pageSize.Value}");
}
if (!string.IsNullOrWhiteSpace(sort))
{
segments.Add($"sort={Uri.EscapeDataString(sort.Trim())}");
}
if (!string.IsNullOrWhiteSpace(direction))
{
segments.Add($"dir={Uri.EscapeDataString(direction.Trim())}");
}
var path = $"api/catalog/categories/{trimmedSlug}/models";
if (segments.Count > 0)
{
path = $"{path}?{string.Join('&', segments)}";
}
using var response = await httpClient.GetAsync(path, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
throw new RemoteProblemException(response.StatusCode, body);
}
return await response.Content.ReadFromJsonAsync<GetCategoryModels.Response>(cancellationToken: cancellationToken)
?? throw new InvalidOperationException("Category products response payload was empty.");
}
public async Task<GetProductDetail.Response> GetProductDetail(string slug, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
using var response = await httpClient.GetAsync($"api/catalog/products/{slug.Trim()}", cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
throw new RemoteProblemException(response.StatusCode, body);
}
return await response.Content.ReadFromJsonAsync<GetProductDetail.Response>(cancellationToken: cancellationToken)
?? throw new InvalidOperationException("Product response payload was empty.");
}
public Task<QuotePrice.Response> QuotePrice(QuotePrice.Request request, CancellationToken cancellationToken) =>
Quote(request, cancellationToken);
public async Task<QuotePrice.Response> Quote(QuotePrice.Request request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
using var response = await httpClient.PostAsJsonAsync("api/catalog/price-quotes", request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
throw new RemoteProblemException(response.StatusCode, body);
}
return await response.Content.ReadFromJsonAsync<QuotePrice.Response>(cancellationToken: cancellationToken)
?? throw new InvalidOperationException("Quote response payload was empty.");
}
}

View File

@@ -0,0 +1,26 @@
namespace Prefab.Shared.Catalog.Products;
public abstract class QuotePrice
{
public sealed record Request(
Guid? ProductId,
string? Sku,
IReadOnlyList<Selection> Selections)
{
public IReadOnlyList<Selection> Selections { get; init; } = Selections ?? Array.Empty<Selection>();
}
public sealed record Selection(
Guid OptionDefinitionId,
Guid? OptionValueId,
decimal? NumericValue);
public sealed record Response(
decimal UnitPrice,
IReadOnlyList<QuoteBreakdown> Breakdown);
public sealed record QuoteBreakdown(
string Option,
string Chosen,
decimal Delta);
}