| | | 1 | | using Services.ApiServices.OzonApi.Filters; |
| | | 2 | | |
| | | 3 | | namespace Services.ApiServices.OzonApi; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Сопоставление ответа Ozon POST /v1/posting/fbs/split с частями заказа (part1/part2). |
| | | 7 | | /// </summary> |
| | | 8 | | public static class OzonSplitPostingMapper |
| | | 9 | | { |
| | | 10 | | /// <summary> |
| | | 11 | | /// Сопоставляет ответ Ozon split с частями заказа <paramref name="part1"/> и <paramref name="part2"/>. |
| | | 12 | | /// </summary> |
| | | 13 | | /// <remarks> |
| | | 14 | | /// <para>При двух дочерних posting сопоставление по quantity; если не удалось — порядок из ответа API.</para> |
| | | 15 | | /// <para>При одном дочернем posting и наличии <c>parent_posting</c>: |
| | | 16 | | /// сопоставление по quantity parent/child с part1/part2 (в обоих порядках). |
| | | 17 | | /// Если quantities не совпадают с ожидаемыми part1/part2 — выбрасывается |
| | | 18 | | /// <see cref="InvalidOperationException"/> (без тихого fallback), чтобы не записать |
| | | 19 | | /// неверные номера отправлений в БД.</para> |
| | | 20 | | /// </remarks> |
| | | 21 | | public static (string posting1, string posting2) Map( |
| | | 22 | | FbsSplitResponse? result, |
| | | 23 | | int part1, |
| | | 24 | | int part2) |
| | | 25 | | { |
| | 4 | 26 | | if (result == null) |
| | | 27 | | { |
| | 0 | 28 | | throw new InvalidOperationException("Ozon API вернул пустой ответ на разделение отправления."); |
| | | 29 | | } |
| | | 30 | | |
| | 4 | 31 | | var childPostings = (result.Postings ?? new List<FbsSplitResultPosting>()) |
| | 4 | 32 | | .Where(p => !string.IsNullOrWhiteSpace(p.PostingNumber)) |
| | 4 | 33 | | .Select(p => new SplitPostingCandidate( |
| | 4 | 34 | | p.PostingNumber!.Trim(), |
| | 8 | 35 | | p.Products?.Sum(x => x.Quantity) ?? 0)) |
| | 4 | 36 | | .ToList(); |
| | | 37 | | |
| | 4 | 38 | | if (childPostings.Count >= 2) |
| | | 39 | | { |
| | 1 | 40 | | var byQty = TryMatchByQuantity(childPostings, part1, part2); |
| | 1 | 41 | | if (byQty != null) |
| | | 42 | | { |
| | 1 | 43 | | return byQty.Value; |
| | | 44 | | } |
| | | 45 | | |
| | 0 | 46 | | return (childPostings[0].PostingNumber, childPostings[1].PostingNumber); |
| | | 47 | | } |
| | | 48 | | |
| | 3 | 49 | | if (childPostings.Count == 1) |
| | | 50 | | { |
| | 2 | 51 | | var parentNumber = result.ParentPosting?.PostingNumber?.Trim(); |
| | 4 | 52 | | var parentQty = result.ParentPosting?.Products?.Sum(x => x.Quantity) ?? 0; |
| | 2 | 53 | | var child = childPostings[0]; |
| | | 54 | | |
| | 2 | 55 | | if (!string.IsNullOrEmpty(parentNumber)) |
| | | 56 | | { |
| | 2 | 57 | | if (parentQty == part1 && child.Quantity == part2) |
| | | 58 | | { |
| | 1 | 59 | | return (parentNumber, child.PostingNumber); |
| | | 60 | | } |
| | | 61 | | |
| | 1 | 62 | | if (parentQty == part2 && child.Quantity == part1) |
| | | 63 | | { |
| | 0 | 64 | | return (child.PostingNumber, parentNumber); |
| | | 65 | | } |
| | | 66 | | |
| | 1 | 67 | | throw new InvalidOperationException( |
| | 1 | 68 | | $"Не удалось сопоставить quantities: parent={parentQty}, child={child.Quantity}, " + |
| | 1 | 69 | | $"ожидалось part1={part1}, part2={part2}."); |
| | | 70 | | } |
| | | 71 | | |
| | 0 | 72 | | throw new InvalidOperationException( |
| | 0 | 73 | | "Ozon API вернул одно дочернее отправление без parent_posting — невозможно сопоставить обе части."); |
| | | 74 | | } |
| | | 75 | | |
| | 1 | 76 | | throw new InvalidOperationException("Ozon API вернул пустые номера отправлений."); |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | private static (string, string)? TryMatchByQuantity( |
| | | 80 | | List<SplitPostingCandidate> postings, |
| | | 81 | | int part1, |
| | | 82 | | int part2) |
| | | 83 | | { |
| | 2 | 84 | | var match1 = postings.FirstOrDefault(p => p.Quantity == part1); |
| | 3 | 85 | | var match2 = postings.FirstOrDefault(p => p.Quantity == part2 && p.PostingNumber != match1?.PostingNumber); |
| | | 86 | | |
| | 1 | 87 | | if (match1 != null && match2 != null) |
| | | 88 | | { |
| | 1 | 89 | | return (match1.PostingNumber, match2.PostingNumber); |
| | | 90 | | } |
| | | 91 | | |
| | 0 | 92 | | return null; |
| | | 93 | | } |
| | | 94 | | |
| | 16 | 95 | | private sealed record SplitPostingCandidate(string PostingNumber, int Quantity); |
| | | 96 | | } |