| | | 1 | | using System.Text.Json.Serialization; |
| | | 2 | | |
| | | 3 | | namespace Services.ApiServices.InterpartsEngine; |
| | | 4 | | |
| | | 5 | | /// <summary>Ответ <c>GET /search/brands?article=…</c>.</summary> |
| | | 6 | | public sealed class EngineBrandsResponse |
| | | 7 | | { |
| | | 8 | | [JsonPropertyName("article")] public string Article { get; set; } = string.Empty; |
| | | 9 | | [JsonPropertyName("brands")] public List<EngineBrand> Brands { get; set; } = []; |
| | | 10 | | } |
| | | 11 | | |
| | | 12 | | public sealed class EngineBrand |
| | | 13 | | { |
| | | 14 | | [JsonPropertyName("brand")] public string Brand { get; set; } = string.Empty; |
| | | 15 | | /// <summary>Название детали, может быть пустым.</summary> |
| | | 16 | | [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; |
| | | 17 | | } |
| | | 18 | | |
| | | 19 | | /// <summary>Ответ <c>GET /search/offers?article=…&brand=…</c>: запрошенная деталь и аналоги вперемешку.</summary> |
| | | 20 | | public sealed class EngineOffersResponse |
| | | 21 | | { |
| | | 22 | | [JsonPropertyName("article")] public string Article { get; set; } = string.Empty; |
| | | 23 | | [JsonPropertyName("brand")] public string Brand { get; set; } = string.Empty; |
| | | 24 | | [JsonPropertyName("offers")] public List<EngineOffer> Offers { get; set; } = []; |
| | | 25 | | |
| | | 26 | | [JsonIgnore] public IEnumerable<EngineOffer> Requested => Offers.Where(o => !o.IsAnalog); |
| | | 27 | | [JsonIgnore] public IEnumerable<EngineOffer> Analogs => Offers.Where(o => o.IsAnalog); |
| | | 28 | | |
| | | 29 | | /// <summary>Ближайший срок жизни <c>offer_id</c> в ответе; null — предложений нет.</summary> |
| | | 30 | | [JsonIgnore] public DateTimeOffset? EarliestExpiresAt => Offers.Count == 0 ? null : Offers.Min(o => o.ExpiresAt); |
| | | 31 | | } |
| | | 32 | | |
| | | 33 | | /// <summary> |
| | | 34 | | /// Предложение движка. Цена — итоговая для покупателя по тарифу учётки ключа, в копейках; поставщик и |
| | | 35 | | /// себестоимость движок не отдаёт. <see cref="OfferId"/> живёт до <see cref="ExpiresAt"/> (~7 минут), хранить его нельз |
| | | 36 | | /// </summary> |
| | | 37 | | public sealed class EngineOffer |
| | | 38 | | { |
| | | 39 | | [JsonPropertyName("offer_id")] public string OfferId { get; set; } = string.Empty; |
| | | 40 | | [JsonPropertyName("article")] public string Article { get; set; } = string.Empty; |
| | | 41 | | /// <summary>Нормализованный артикул (без пробелов и дефисов); в контракте v0.3 его нет.</summary> |
| | | 42 | | [JsonPropertyName("search_code")] public string? SearchCode { get; set; } |
| | | 43 | | [JsonPropertyName("brand")] public string Brand { get; set; } = string.Empty; |
| | | 44 | | [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; |
| | | 45 | | /// <summary><c>requested</c> или <c>analog</c>.</summary> |
| | | 46 | | [JsonPropertyName("match")] public string Match { get; set; } = string.Empty; |
| | | 47 | | [JsonPropertyName("price")] public long PriceKopecks { get; set; } |
| | | 48 | | [JsonPropertyName("currency")] public string Currency { get; set; } = "RUB"; |
| | | 49 | | /// <summary>0 — нет в наличии, null — количество неизвестно («наличие уточняется»).</summary> |
| | | 50 | | [JsonPropertyName("quantity_available")] public int? QuantityAvailable { get; set; } |
| | | 51 | | [JsonPropertyName("min_order_qty")] public int MinOrderQty { get; set; } = 1; |
| | | 52 | | /// <summary>null — срок неизвестен («срок уточняется»).</summary> |
| | | 53 | | [JsonPropertyName("delivery_days_min")] public int? DeliveryDaysMin { get; set; } |
| | | 54 | | [JsonPropertyName("delivery_days_max")] public int? DeliveryDaysMax { get; set; } |
| | | 55 | | /// <summary>Подпись вместо поставщика; сейчас всегда пустая.</summary> |
| | | 56 | | [JsonPropertyName("warehouse_label")] public string? WarehouseLabel { get; set; } |
| | | 57 | | [JsonPropertyName("image_url")] public string? ImageUrl { get; set; } |
| | | 58 | | [JsonPropertyName("expires_at")] public DateTimeOffset ExpiresAt { get; set; } |
| | | 59 | | |
| | | 60 | | [JsonIgnore] public bool IsAnalog => string.Equals(Match, "analog", StringComparison.OrdinalIgnoreCase); |
| | | 61 | | [JsonIgnore] public decimal PriceRub => PriceKopecks / 100m; |
| | | 62 | | [JsonIgnore] public bool InStock => QuantityAvailable is null or > 0; |
| | | 63 | | |
| | | 64 | | public bool IsExpired(DateTimeOffset now) => ExpiresAt <= now; |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | /// <summary>Формат ошибки движка: <c>{"error":{"code":"…","message":"…","details":{}}}</c>.</summary> |
| | | 68 | | internal sealed class EngineErrorEnvelope |
| | | 69 | | { |
| | 14 | 70 | | [JsonPropertyName("error")] public EngineErrorBody? Error { get; set; } |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | internal sealed class EngineErrorBody |
| | | 74 | | { |
| | | 75 | | [JsonPropertyName("code")] public string? Code { get; set; } |
| | | 76 | | [JsonPropertyName("message")] public string? Message { get; set; } |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | public enum InterpartsEngineStatus |
| | | 80 | | { |
| | | 81 | | Ok, |
| | | 82 | | /// <summary>Движок такой <c>offer_id</c> не выдавал (404).</summary> |
| | | 83 | | NotFound, |
| | | 84 | | /// <summary>Предложение устарело или исчезло (410) — нужен новый поиск.</summary> |
| | | 85 | | Expired, |
| | | 86 | | Error |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | /// <summary>Итог запроса к движку без исключений наружу (кроме отмены вызывающим).</summary> |
| | | 90 | | public sealed record InterpartsEngineResult<T>( |
| | | 91 | | InterpartsEngineStatus Status, |
| | | 92 | | T? Value = default, |
| | | 93 | | string? ErrorCode = null, |
| | | 94 | | string? Message = null, |
| | | 95 | | bool Retry = false) |
| | | 96 | | where T : class |
| | | 97 | | { |
| | | 98 | | public bool IsOk => Status == InterpartsEngineStatus.Ok && Value is not null; |
| | | 99 | | |
| | | 100 | | public static InterpartsEngineResult<T> Ok(T value) => new(InterpartsEngineStatus.Ok, value); |
| | | 101 | | public static InterpartsEngineResult<T> NotFound(string? message) => new(InterpartsEngineStatus.NotFound, null, "NOT |
| | | 102 | | public static InterpartsEngineResult<T> Expired(string? message) => new(InterpartsEngineStatus.Expired, null, "OFFER |
| | | 103 | | public static InterpartsEngineResult<T> Failure(string code, string message, bool retry) => new(InterpartsEngineStat |
| | | 104 | | } |