Init
This commit is contained in:
8
Prefab.Shared/Catalog/Products/GetCategory.cs
Normal file
8
Prefab.Shared/Catalog/Products/GetCategory.cs
Normal 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);
|
||||
}
|
||||
50
Prefab.Shared/Catalog/Products/GetCategoryModels.cs
Normal file
50
Prefab.Shared/Catalog/Products/GetCategoryModels.cs
Normal 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);
|
||||
}
|
||||
8
Prefab.Shared/Catalog/Products/GetCategoryTree.cs
Normal file
8
Prefab.Shared/Catalog/Products/GetCategoryTree.cs
Normal 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);
|
||||
}
|
||||
77
Prefab.Shared/Catalog/Products/GetProductDetail.cs
Normal file
77
Prefab.Shared/Catalog/Products/GetProductDetail.cs
Normal 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);
|
||||
}
|
||||
19
Prefab.Shared/Catalog/Products/IProductClient.cs
Normal file
19
Prefab.Shared/Catalog/Products/IProductClient.cs
Normal 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);
|
||||
}
|
||||
117
Prefab.Shared/Catalog/Products/ProductClientHttp.cs
Normal file
117
Prefab.Shared/Catalog/Products/ProductClientHttp.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
26
Prefab.Shared/Catalog/Products/QuotePrice.cs
Normal file
26
Prefab.Shared/Catalog/Products/QuotePrice.cs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user