| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using OzonDomains.Models.Partners; |
| | | 4 | | using OzonOrdersWeb.WebServices.Orders; |
| | | 5 | | using OzonRepositories.Context; |
| | | 6 | | using Services.ApiServices.OzonApi; |
| | | 7 | | using Services.DataServices; |
| | | 8 | | using Services.LabelServices; |
| | | 9 | | using Services.PartnerServices; |
| | | 10 | | |
| | | 11 | | namespace OzonOrdersWeb.WebServices.Partners; |
| | | 12 | | |
| | | 13 | | public interface IPartnerOrderActionsService |
| | | 14 | | { |
| | | 15 | | /// <summary>Что Ozon просит заполнить перед отгрузкой: страны-изготовители и данные экземпляров.</summary> |
| | | 16 | | Task<PartnerActionResult<PartnerShipRequirements>> GetRequirementsAsync(long partnerId, int orderId, CancellationTok |
| | | 17 | | |
| | | 18 | | Task<PartnerActionResult<IReadOnlyList<PartnerCountryOption>>> GetCountriesAsync(long partnerId, int orderId, string |
| | | 19 | | |
| | | 20 | | Task<PartnerActionResult> SetCountriesAsync(long partnerId, int orderId, IReadOnlyList<PartnerCountrySelection> sele |
| | | 21 | | |
| | | 22 | | Task<PartnerActionResult> SetExemplarsAsync(long partnerId, int orderId, IReadOnlyList<OzonExemplarInput> exemplars, |
| | | 23 | | |
| | | 24 | | /// <summary>Отгрузка: вызов Ozon API (/v4/posting/fbs/ship) по заказу, как «Отгрузить» в Studio2.</summary> |
| | | 25 | | Task<PartnerActionResult> ShipAsync(long partnerId, int orderId, CancellationToken ct = default); |
| | | 26 | | |
| | | 27 | | /// <summary>Печать этикетки как в Studio2: готовая из хранилища либо отгрузка (если нужна) и получение у Ozon.</sum |
| | | 28 | | Task<PartnerActionResult<PartnerPrintedLabel>> PrintLabelAsync(long partnerId, int orderId, CancellationToken ct = d |
| | | 29 | | } |
| | | 30 | | |
| | | 31 | | /// <summary> |
| | | 32 | | /// Действия партнёра над заказом Ozon. Вся работа с Ozon идёт через те же сервисы, что и в Studio2 |
| | | 33 | | /// (<see cref="IOrdersOzonShipService"/>, <see cref="IOzonLabelPrintService"/>, <see cref="IOzonExemplarService"/>), |
| | | 34 | | /// а этот класс только проверяет права и видимость заказа, выбирает клиента Ozon и пишет журнал. |
| | | 35 | | /// </summary> |
| | 7 | 36 | | public sealed class PartnerOrderActionsService( |
| | 7 | 37 | | IPartnerOrdersService visibility, |
| | 7 | 38 | | IPartnerPermissionService permissions, |
| | 7 | 39 | | IPartnerAuditService audit, |
| | 7 | 40 | | ApplicationDbContext db, |
| | 7 | 41 | | OrdersDataServices orderServices, |
| | 7 | 42 | | OzonClientServices clientServices, |
| | 7 | 43 | | IOzonExemplarService exemplars, |
| | 7 | 44 | | IOrdersOzonShipService shipService, |
| | 7 | 45 | | IOzonLabelPrintService labelPrint, |
| | 7 | 46 | | OzonJsonDataBuilder jsonBuilder, |
| | 7 | 47 | | OzonApiDataManager ozonApi, |
| | 7 | 48 | | ILabelFileStorage labelStorage, |
| | 7 | 49 | | ILabelExistenceService labelExistence, |
| | 7 | 50 | | ILabelPrintLogService labelPrintLog, |
| | 7 | 51 | | IOrdersClientStatusSyncService statusSync) : IPartnerOrderActionsService |
| | | 52 | | { |
| | 0 | 53 | | private sealed record Target(Order Order, OzonClient Client, string ApiPosting); |
| | | 54 | | |
| | | 55 | | public async Task<PartnerActionResult<PartnerShipRequirements>> GetRequirementsAsync( |
| | | 56 | | long partnerId, int orderId, CancellationToken ct = default) |
| | | 57 | | { |
| | 3 | 58 | | var (target, error) = await ResolveAsync(partnerId, orderId, ct, PartnerPermissionCode.Assemble, PartnerPermissi |
| | 6 | 59 | | if (target is null) return new(error!, null); |
| | | 60 | | |
| | | 61 | | try |
| | | 62 | | { |
| | 0 | 63 | | var api = ozonApi.SetClient(target.Client.DecryptClientId!, target.Client.DecryptApiKey!); |
| | 0 | 64 | | var products = await api.GetFbsProductsRequiringCountryAsync(target.ApiPosting); |
| | 0 | 65 | | var countries = products |
| | 0 | 66 | | .Select(p => new PartnerCountryRequirement( |
| | 0 | 67 | | p["sku"]?.ToObject<long>() ?? 0, |
| | 0 | 68 | | p["name"]?.ToObject<string>() ?? p["offer_id"]?.ToObject<string>() ?? "Товар", |
| | 0 | 69 | | p["offer_id"]?.ToObject<string>() ?? string.Empty)) |
| | 0 | 70 | | .Where(c => c.ProductId > 0).ToList(); |
| | | 71 | | |
| | 0 | 72 | | exemplars.SetClient(target.Client.DecryptClientId!, target.Client.DecryptApiKey!); |
| | 0 | 73 | | var items = await exemplars.GetRequirementsAsync([target.Order], ct); |
| | 0 | 74 | | var exemplarList = items.Select(i => new PartnerExemplarRequirement( |
| | 0 | 75 | | i.ShipmentNumber, i.Requirement.ProductId, i.ProductName, i.OfferId, i.Requirement.ExemplarId, |
| | 0 | 76 | | i.Requirement.Index, i.Requirement.Quantity, i.Requirement.NeedsMandatoryMark, i.Requirement.NeedsGtd, |
| | 0 | 77 | | i.Requirement.NeedsRnpt, i.Requirement.Unsupported)).ToList(); |
| | | 78 | | |
| | 0 | 79 | | return new(new PartnerActionResult(true, "Требования Ozon получены."), new PartnerShipRequirements(countries |
| | | 80 | | } |
| | 0 | 81 | | catch (Exception ex) |
| | | 82 | | { |
| | 0 | 83 | | return new(PartnerActionResult.Fail(PartnerActionError.Failed, $"Ozon не отдал требования: {ex.Message}"), n |
| | | 84 | | } |
| | 3 | 85 | | } |
| | | 86 | | |
| | | 87 | | public async Task<PartnerActionResult<IReadOnlyList<PartnerCountryOption>>> GetCountriesAsync( |
| | | 88 | | long partnerId, int orderId, string? search, CancellationToken ct = default) |
| | | 89 | | { |
| | 1 | 90 | | var (target, error) = await ResolveAsync(partnerId, orderId, ct, PartnerPermissionCode.Assemble, PartnerPermissi |
| | 2 | 91 | | if (target is null) return new(error!, null); |
| | | 92 | | |
| | | 93 | | try |
| | | 94 | | { |
| | 0 | 95 | | var api = ozonApi.SetClient(target.Client.DecryptClientId!, target.Client.DecryptApiKey!); |
| | 0 | 96 | | var countries = await api.GetFbsProductCountriesAsync(search); |
| | 0 | 97 | | IReadOnlyList<PartnerCountryOption> list = countries |
| | 0 | 98 | | .Select(c => new PartnerCountryOption( |
| | 0 | 99 | | c["name"]?.ToObject<string>() ?? string.Empty, |
| | 0 | 100 | | c["country_iso_code"]?.ToObject<string>() ?? string.Empty)) |
| | 0 | 101 | | .Where(c => !string.IsNullOrWhiteSpace(c.CountryIsoCode)) |
| | 0 | 102 | | .OrderBy(c => c.Name, StringComparer.Create(System.Globalization.CultureInfo.GetCultureInfo("ru-RU"), ig |
| | 0 | 103 | | .ToList(); |
| | 0 | 104 | | return new(new PartnerActionResult(true, "Справочник стран получен."), list); |
| | | 105 | | } |
| | 0 | 106 | | catch (Exception ex) |
| | | 107 | | { |
| | 0 | 108 | | return new(PartnerActionResult.Fail(PartnerActionError.Failed, $"Ozon не отдал список стран: {ex.Message}"), |
| | | 109 | | } |
| | 1 | 110 | | } |
| | | 111 | | |
| | | 112 | | public async Task<PartnerActionResult> SetCountriesAsync( |
| | | 113 | | long partnerId, int orderId, IReadOnlyList<PartnerCountrySelection> selections, CancellationToken ct = default) |
| | | 114 | | { |
| | 5 | 115 | | if (selections.Count == 0 || selections.Any(s => s.ProductId <= 0 || string.IsNullOrWhiteSpace(s.CountryIsoCode) |
| | 2 | 116 | | return PartnerActionResult.Fail(PartnerActionError.Failed, "Не заполнены страны-изготовители."); |
| | | 117 | | |
| | 1 | 118 | | var (target, error) = await ResolveAsync(partnerId, orderId, ct, PartnerPermissionCode.Assemble, PartnerPermissi |
| | 2 | 119 | | if (target is null) return error!; |
| | | 120 | | |
| | | 121 | | try |
| | | 122 | | { |
| | 0 | 123 | | var api = ozonApi.SetClient(target.Client.DecryptClientId!, target.Client.DecryptApiKey!); |
| | 0 | 124 | | foreach (var s in selections) |
| | 0 | 125 | | await api.SetFbsProductCountryAsync(target.ApiPosting, s.ProductId, s.CountryIsoCode); |
| | 0 | 126 | | } |
| | 0 | 127 | | catch (Exception ex) |
| | | 128 | | { |
| | 0 | 129 | | await AuditAsync(partnerId, "countries_set", target.Order, false, ex.Message, ct); |
| | 0 | 130 | | return PartnerActionResult.Fail(PartnerActionError.Failed, $"Ozon отклонил страну-изготовитель: {ex.Message} |
| | | 131 | | } |
| | | 132 | | |
| | 0 | 133 | | await AuditAsync(partnerId, "countries_set", target.Order, true, $"Товаров: {selections.Count}", ct); |
| | 0 | 134 | | return new PartnerActionResult(true, "Страны-изготовители сохранены."); |
| | 3 | 135 | | } |
| | | 136 | | |
| | | 137 | | public async Task<PartnerActionResult> SetExemplarsAsync( |
| | | 138 | | long partnerId, int orderId, IReadOnlyList<OzonExemplarInput> inputs, CancellationToken ct = default) |
| | | 139 | | { |
| | 3 | 140 | | if (inputs.Count == 0) |
| | 1 | 141 | | return PartnerActionResult.Fail(PartnerActionError.Failed, "Не переданы данные экземпляров."); |
| | | 142 | | |
| | 2 | 143 | | var (target, error) = await ResolveAsync(partnerId, orderId, ct, PartnerPermissionCode.Assemble, PartnerPermissi |
| | 4 | 144 | | if (target is null) return error!; |
| | | 145 | | |
| | | 146 | | try |
| | | 147 | | { |
| | 0 | 148 | | exemplars.SetClient(target.Client.DecryptClientId!, target.Client.DecryptApiKey!); |
| | 0 | 149 | | var result = await exemplars.SaveAsync(target.Order.ShipmentNumber!, inputs, ct); |
| | 0 | 150 | | if (!result.ShipAvailable) |
| | | 151 | | { |
| | 0 | 152 | | await AuditAsync(partnerId, "exemplars_set", target.Order, false, string.Join("; ", result.Reasons), ct) |
| | 0 | 153 | | return PartnerActionResult.Fail(PartnerActionError.Failed, |
| | 0 | 154 | | "Ozon не подтвердил готовность отправления к сборке.", result.Reasons); |
| | | 155 | | } |
| | 0 | 156 | | } |
| | 0 | 157 | | catch (Exception ex) |
| | | 158 | | { |
| | 0 | 159 | | await AuditAsync(partnerId, "exemplars_set", target.Order, false, ex.Message, ct); |
| | 0 | 160 | | return PartnerActionResult.Fail(PartnerActionError.Failed, $"Ozon отклонил данные экземпляров. {ex.Message}" |
| | | 161 | | } |
| | | 162 | | |
| | 0 | 163 | | await AuditAsync(partnerId, "exemplars_set", target.Order, true, $"Экземпляров: {inputs.Count}", ct); |
| | 0 | 164 | | return new PartnerActionResult(true, "Данные экземпляров сохранены, отправление готово к сборке."); |
| | 3 | 165 | | } |
| | | 166 | | |
| | | 167 | | public async Task<PartnerActionResult> ShipAsync(long partnerId, int orderId, CancellationToken ct = default) |
| | | 168 | | { |
| | 2 | 169 | | var (target, error) = await ResolveAsync(partnerId, orderId, ct, PartnerPermissionCode.Assemble); |
| | 4 | 170 | | if (target is null) return error!; |
| | | 171 | | |
| | 0 | 172 | | var result = await ShipCoreAsync(target, ct); |
| | 0 | 173 | | await AuditAsync(partnerId, "ship", target.Order, result.Success, result.Message, ct); |
| | 0 | 174 | | return result; |
| | 2 | 175 | | } |
| | | 176 | | |
| | | 177 | | public async Task<PartnerActionResult<PartnerPrintedLabel>> PrintLabelAsync(long partnerId, int orderId, Cancellatio |
| | | 178 | | { |
| | 1 | 179 | | var (target, error) = await ResolveAsync(partnerId, orderId, ct, PartnerPermissionCode.LabelPrint); |
| | 2 | 180 | | if (target is null) return new(error!, null); |
| | | 181 | | |
| | 0 | 182 | | var order = target.Order; |
| | 0 | 183 | | var clients = await clientServices.GetOzonClients(); |
| | | 184 | | |
| | | 185 | | // Шаг 1, как в Studio2: готовая этикетка из хранилища. Новую у Ozon не запрашиваем. |
| | 0 | 186 | | var existingName = await labelStorage.FindExistingFileNameAsync( |
| | 0 | 187 | | OzonPostingNumberHelper.GetLabelFileNameCandidates(order.ShipmentNumber), ct); |
| | 0 | 188 | | if (existingName is not null) |
| | | 189 | | { |
| | 0 | 190 | | var bytes = await labelStorage.TryDownloadAsync(existingName, ct); |
| | 0 | 191 | | if (bytes is { Length: > 0 }) |
| | | 192 | | { |
| | 0 | 193 | | await AuditAsync(partnerId, "label_print", order, true, "Из хранилища", ct); |
| | 0 | 194 | | await RecordPartnerLabelAsync(partnerId, order.Id, existingName, ct); |
| | 0 | 195 | | return new(new PartnerActionResult(true, "Этикетка из хранилища."), new PartnerPrintedLabel(bytes, exist |
| | | 196 | | } |
| | 0 | 197 | | } |
| | | 198 | | |
| | | 199 | | try |
| | | 200 | | { |
| | 0 | 201 | | jsonBuilder.SetClient(target.Client.DecryptClientId!, target.Client.DecryptApiKey!); |
| | 0 | 202 | | var status = await jsonBuilder.GetProductSatatus(target.ApiPosting); |
| | | 203 | | |
| | 0 | 204 | | if (OzonLabelStatusHelper.IsOzonStatusLabelGenerationBlocked(status)) |
| | | 205 | | { |
| | 0 | 206 | | var blocked = $"Статус Ozon «{OzonLabelStatusHelper.GetOzonStatusDisplayName(status)}»: Ozon не выдаёт э |
| | 0 | 207 | | await AuditAsync(partnerId, "label_print", order, false, blocked, ct); |
| | 0 | 208 | | return new(PartnerActionResult.Fail(PartnerActionError.Failed, blocked), null); |
| | | 209 | | } |
| | | 210 | | |
| | | 211 | | // Шаг 2, как в Studio2: если отправление ещё не собрано, перед печатью его нужно отгрузить. |
| | | 212 | | // Отгрузка меняет состояние заказа на Ozon, поэтому для неё нужна своя возможность партнёра. |
| | 0 | 213 | | var autoShipped = false; |
| | 0 | 214 | | if (status is "awaiting_packaging" or "not_accepted") |
| | | 215 | | { |
| | 0 | 216 | | if (!await permissions.HasAsync(partnerId, PartnerPermissionCode.Assemble, ct)) |
| | | 217 | | { |
| | | 218 | | const string text = "Отправление ещё не собрано, а для печати его нужно отгрузить. " + |
| | | 219 | | "У вас нет возможности «Сборка и отгрузка»: попросите владельца включить её или |
| | 0 | 220 | | await AuditAsync(partnerId, "label_print", order, false, text, ct); |
| | 0 | 221 | | return new(PartnerActionResult.Fail(PartnerActionError.Forbidden, text), null); |
| | | 222 | | } |
| | | 223 | | |
| | 0 | 224 | | var shipped = await ShipCoreAsync(target, ct); |
| | 0 | 225 | | await AuditAsync(partnerId, "ship", order, shipped.Success, $"Перед печатью: {shipped.Message}", ct); |
| | 0 | 226 | | if (!shipped.Success) return new(shipped, null); |
| | 0 | 227 | | autoShipped = true; |
| | 0 | 228 | | } |
| | | 229 | | |
| | | 230 | | // Шаг 3: получение этикетки у Ozon и сохранение в хранилище (вся логика, повторы и разбор ошибок — в сервис |
| | 0 | 231 | | var labels = new Dictionary<string, byte[]>(); |
| | 0 | 232 | | var generated = new List<string>(); |
| | 0 | 233 | | var failed = new List<string>(); |
| | 0 | 234 | | var stored = new List<string>(); |
| | 0 | 235 | | await labelPrint.GenerateOzonLabelsForOrdersAsync( |
| | 0 | 236 | | [order], clients, labels, generated, failed, new List<string>(), stored); |
| | | 237 | | |
| | 0 | 238 | | if (labels.Count == 0) |
| | | 239 | | { |
| | 0 | 240 | | var reason = failed.Count > 0 ? string.Join("; ", failed) : "Не удалось получить этикетку."; |
| | 0 | 241 | | await AuditAsync(partnerId, "label_print", order, false, reason, ct); |
| | 0 | 242 | | return new(PartnerActionResult.Fail(PartnerActionError.Failed, "Не удалось получить этикетку у Ozon.", f |
| | | 243 | | } |
| | | 244 | | |
| | 0 | 245 | | try { await labelPrint.EnsureScanitAsync([order], clients); } |
| | 0 | 246 | | catch { /* ScanIt не критичен для печати: подтянется при следующей печати или загрузке заказа */ } |
| | | 247 | | |
| | 0 | 248 | | labelExistence.SetShipmentExists(order.ShipmentNumber!, true); |
| | 0 | 249 | | var content = labels.Values.First(); |
| | 0 | 250 | | await AuditAsync(partnerId, "label_print", order, true, autoShipped ? "Отгружен и напечатан" : "Получена у O |
| | 0 | 251 | | if (stored.Contains(order.ShipmentNumber!, StringComparer.OrdinalIgnoreCase)) |
| | | 252 | | { |
| | 0 | 253 | | await RecordPartnerLabelAsync(partnerId, order.Id, $"{order.ShipmentNumber}.pdf", ct); |
| | | 254 | | } |
| | 0 | 255 | | return new(new PartnerActionResult(true, autoShipped ? "Заказ отгружен, этикетка получена." : "Этикетка полу |
| | 0 | 256 | | new PartnerPrintedLabel(content, $"{order.ShipmentNumber}.pdf", true, autoShipped)); |
| | | 257 | | } |
| | 0 | 258 | | catch (Exception ex) |
| | | 259 | | { |
| | 0 | 260 | | var failedList = new List<string>(); |
| | 0 | 261 | | await shipService.RecordFailedShipmentAsync(order, ex, failedList, ct); |
| | 0 | 262 | | var message = failedList.Count > 0 ? failedList[0] : ex.Message; |
| | 0 | 263 | | await AuditAsync(partnerId, "label_print", order, false, message, ct); |
| | 0 | 264 | | return new(PartnerActionResult.Fail(PartnerActionError.Failed, message), null); |
| | | 265 | | } |
| | 1 | 266 | | } |
| | | 267 | | |
| | | 268 | | /// <summary> |
| | | 269 | | /// Отгрузка по логике <c>UpdateOrdersStatus</c> из Studio2: studio-части одного отправления (posting/1, posting/2) |
| | | 270 | | /// уходят в Ozon одной отгрузкой по родительскому posting, остальные — обычной отгрузкой заказа. |
| | | 271 | | /// </summary> |
| | | 272 | | private async Task<PartnerActionResult> ShipCoreAsync(Target target, CancellationToken ct) |
| | | 273 | | { |
| | 0 | 274 | | var order = target.Order; |
| | | 275 | | try |
| | | 276 | | { |
| | 0 | 277 | | shipService.SetClient(target.Client.DecryptClientId!, target.Client.DecryptApiKey!); |
| | 0 | 278 | | jsonBuilder.SetClient(target.Client.DecryptClientId!, target.Client.DecryptApiKey!); |
| | | 279 | | |
| | 0 | 280 | | var siblings = await LoadStudioSiblingsAsync(target.ApiPosting, order, ct); |
| | | 281 | | string message; |
| | 0 | 282 | | if (siblings.Count > 1) |
| | | 283 | | { |
| | 0 | 284 | | message = await labelPrint.ProcessStudioLocalSplitGroupShipAsync(siblings, target.ApiPosting, shipServic |
| | | 285 | | } |
| | | 286 | | else |
| | | 287 | | { |
| | 0 | 288 | | message = await shipService.ProcessOrderStatusAsync(order, splitOption: null); |
| | | 289 | | } |
| | | 290 | | |
| | 0 | 291 | | await statusSync.SyncOrderStatusesAsync(siblings.Select(o => o.Id).DefaultIfEmpty(order.Id).ToList(), ct); |
| | | 292 | | |
| | 0 | 293 | | var status = await jsonBuilder.GetProductSatatus(target.ApiPosting); |
| | 0 | 294 | | var statusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(status); |
| | 0 | 295 | | if (OzonLabelStatusHelper.IsOzonStatusLabelGenerationBlocked(status)) |
| | | 296 | | { |
| | 0 | 297 | | return new PartnerActionResult(false, |
| | 0 | 298 | | $"Статус Ozon «{statusName}»: отгрузка недоступна.", PartnerActionError.Failed, null, status, status |
| | | 299 | | } |
| | | 300 | | |
| | 0 | 301 | | return new PartnerActionResult(true, message, PartnerActionError.None, null, status, statusName); |
| | | 302 | | } |
| | 0 | 303 | | catch (Exception ex) |
| | | 304 | | { |
| | 0 | 305 | | var failed = new List<string>(); |
| | 0 | 306 | | await shipService.RecordFailedShipmentAsync(order, ex, failed, ct); |
| | 0 | 307 | | return PartnerActionResult.Fail(PartnerActionError.Failed, failed.Count > 0 ? failed[0] : ex.Message); |
| | | 308 | | } |
| | 0 | 309 | | } |
| | | 310 | | |
| | | 311 | | private async Task<List<Order>> LoadStudioSiblingsAsync(string apiPosting, Order order, CancellationToken ct) |
| | | 312 | | { |
| | 0 | 313 | | if (!OzonPostingNumberHelper.IsStudioLocalSplitPosting(order.ShipmentNumber)) return [order]; |
| | | 314 | | |
| | 0 | 315 | | var prefix = apiPosting + "/"; |
| | 0 | 316 | | var ids = await db.Orders.AsNoTracking() |
| | 0 | 317 | | .Where(o => o.ShipmentNumber != null && o.ShipmentNumber.StartsWith(prefix)) |
| | 0 | 318 | | .Select(o => o.Id).ToArrayAsync(ct); |
| | 0 | 319 | | return ids.Length == 0 ? [order] : await orderServices.GetOrdersNoTracking(ids); |
| | 0 | 320 | | } |
| | | 321 | | |
| | | 322 | | /// <summary>Права, видимость заказа, Ozon-заказ и клиент Ozon. Любая из переданных возможностей даёт доступ.</summa |
| | | 323 | | private async Task<(Target? Target, PartnerActionResult? Error)> ResolveAsync( |
| | | 324 | | long partnerId, int orderId, CancellationToken ct, params PartnerPermissionCode[] anyOf) |
| | | 325 | | { |
| | 10 | 326 | | var allowed = false; |
| | 45 | 327 | | foreach (var code in anyOf) |
| | | 328 | | { |
| | 25 | 329 | | if (await permissions.HasAsync(partnerId, code, ct)) { allowed = true; break; } |
| | | 330 | | } |
| | | 331 | | |
| | 10 | 332 | | if (!allowed) |
| | | 333 | | { |
| | 13 | 334 | | var needed = string.Join(" или ", anyOf.Select(c => c switch |
| | 13 | 335 | | { |
| | 4 | 336 | | PartnerPermissionCode.Assemble => "«Сборка и отгрузка»", |
| | 4 | 337 | | PartnerPermissionCode.LabelPrint => "«Печать этикетки»", |
| | 0 | 338 | | _ => c.ToString() |
| | 13 | 339 | | })); |
| | 5 | 340 | | return (null, PartnerActionResult.Fail(PartnerActionError.Forbidden, $"Нужна возможность {needed}.")); |
| | | 341 | | } |
| | | 342 | | |
| | | 343 | | // Заказ вне правил партнёра и архивный — как будто его нет. |
| | 5 | 344 | | if (await visibility.GetAsync(partnerId, orderId, ct) is null) |
| | 5 | 345 | | return (null, PartnerActionResult.Fail(PartnerActionError.NotFound, "Заказ не найден.")); |
| | | 346 | | |
| | 0 | 347 | | var order = (await orderServices.GetOrdersNoTracking([orderId])).FirstOrDefault(); |
| | 0 | 348 | | if (order is null) |
| | 0 | 349 | | return (null, PartnerActionResult.Fail(PartnerActionError.NotFound, "Заказ не найден.")); |
| | | 350 | | |
| | 0 | 351 | | if (!OzonLabelOrderEligibility.IsOzonOrder(order.OzonClient?.ClientType, order.ShipmentNumber, order.OzonClient? |
| | 0 | 352 | | return (null, PartnerActionResult.Fail(PartnerActionError.NotOzonOrder, "Действие доступно только для заказо |
| | | 353 | | |
| | 0 | 354 | | var client = (await clientServices.GetOzonClients()).FirstOrDefault(c => c.ClientId == order.OzonClient?.ClientI |
| | 0 | 355 | | if (client is null || string.IsNullOrWhiteSpace(client.DecryptClientId) || string.IsNullOrWhiteSpace(client.Decr |
| | 0 | 356 | | return (null, PartnerActionResult.Fail(PartnerActionError.NoOzonClient, "Для заказа не настроен клиент Ozon. |
| | | 357 | | |
| | 0 | 358 | | return (new Target(order, client, OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber)), null); |
| | 10 | 359 | | } |
| | | 360 | | |
| | | 361 | | private Task AuditAsync(long partnerId, string action, Order order, bool success, string? details, CancellationToken |
| | 0 | 362 | | audit.WriteAsync(action, PartnerActorKind.Partner, $"partner:{partnerId}", partnerId, success, details, |
| | 0 | 363 | | order.ShipmentNumber, ct: ct); |
| | | 364 | | |
| | | 365 | | private async Task RecordPartnerLabelAsync(long partnerId, int orderId, string fileName, CancellationToken ct) |
| | | 366 | | { |
| | 0 | 367 | | var partnerName = await db.Partners.AsNoTracking() |
| | 0 | 368 | | .Where(x => x.Id == partnerId) |
| | 0 | 369 | | .Select(x => x.Name) |
| | 0 | 370 | | .FirstOrDefaultAsync(ct); |
| | 0 | 371 | | await labelPrintLog.RecordAsync( |
| | 0 | 372 | | [new LabelPrintLogEntry( |
| | 0 | 373 | | fileName, |
| | 0 | 374 | | orderId, |
| | 0 | 375 | | LabelPrintSource.PartnerApi, |
| | 0 | 376 | | LabelPrintActorKind.Partner, |
| | 0 | 377 | | partnerId.ToString(System.Globalization.CultureInfo.InvariantCulture), |
| | 0 | 378 | | string.IsNullOrWhiteSpace(partnerName) ? $"Партнёр #{partnerId}" : partnerName)], |
| | 0 | 379 | | ct); |
| | 0 | 380 | | } |
| | | 381 | | } |