| | | 1 | | using System.Text.RegularExpressions; |
| | | 2 | | |
| | | 3 | | namespace Services.ApiServices._1CApi.OData; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Собирает человекочитаемое сообщение об отказе 1С провести документ. |
| | | 7 | | /// |
| | | 8 | | /// Отказ проведения — ожидаемый бизнес-исход (чаще всего не хватает остатка), и его текст |
| | | 9 | | /// целиком уходит пользователю в алерт транзакции через <c>OneCResponse.PostingError</c>. |
| | | 10 | | /// Поэтому сообщение не должно содержать техники: имени OData-эндпоинта, HTTP-тела ответа |
| | | 11 | | /// и голой фразы 1С «Не удалось провести "…"!», по которой непонятно, что делать дальше. |
| | | 12 | | /// </summary> |
| | | 13 | | public static class OneCDocumentPostingMessage |
| | | 14 | | { |
| | | 15 | | /// <summary>Названия документов в винительном падеже: «1С не провела <i>перемещение запасов</i>».</summary> |
| | 1 | 16 | | private static readonly Dictionary<string, string> DocumentTitles = new(StringComparer.OrdinalIgnoreCase) |
| | 1 | 17 | | { |
| | 1 | 18 | | ["Document_ПеремещениеЗапасов"] = "перемещение запасов", |
| | 1 | 19 | | ["Document_РасходнаяНакладная"] = "расходную накладную", |
| | 1 | 20 | | ["Document_ПриходнаяНакладная"] = "приходную накладную", |
| | 1 | 21 | | ["Document_ИнвентаризацияЗапасов"] = "инвентаризацию запасов", |
| | 1 | 22 | | ["Document_ЗаказКлиента"] = "заказ клиента" |
| | 1 | 23 | | }; |
| | | 24 | | |
| | | 25 | | private const string DefaultTitle = "документ"; |
| | | 26 | | |
| | | 27 | | /// <summary> |
| | | 28 | | /// Типовой отказ 1С без объяснения причины: «Не удалось провести "Перемещение запасов 4043 от 07.09.2026"!». |
| | | 29 | | /// Имя документа в кавычках вытаскиваем и показываем отдельно. |
| | | 30 | | /// </summary> |
| | 1 | 31 | | private static readonly Regex GenericRefusalRegex = new( |
| | 1 | 32 | | @"^Не\s+удалось\s+провести\s+[«""'](?<name>.+?)[»""']\s*!?\s*$", |
| | 1 | 33 | | RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); |
| | | 34 | | |
| | | 35 | | /// <summary>1С не назвала причину: пусто или только «Не удалось провести "…"!».</summary> |
| | | 36 | | public static bool IsUninformative(string? detail) |
| | 4 | 37 | | => string.IsNullOrWhiteSpace(detail) || GenericRefusalRegex.IsMatch(detail.Trim()); |
| | | 38 | | |
| | | 39 | | /// <summary>Название документа 1С для текста сообщения.</summary> |
| | | 40 | | public static string DocumentTitle(string? endpoint) |
| | 22 | 41 | | => !string.IsNullOrWhiteSpace(endpoint) && DocumentTitles.TryGetValue(endpoint.Trim(), out var title) |
| | 22 | 42 | | ? title |
| | 22 | 43 | | : DefaultTitle; |
| | | 44 | | |
| | | 45 | | /// <summary> |
| | | 46 | | /// Формирует сообщение для пользователя по эндпоинту документа и разобранной причине от 1С. |
| | | 47 | | /// </summary> |
| | | 48 | | public static string Build(string? endpoint, string? detail) |
| | | 49 | | { |
| | 16 | 50 | | var title = DocumentTitle(endpoint); |
| | 16 | 51 | | var trimmed = detail?.Trim(); |
| | | 52 | | |
| | 16 | 53 | | if (string.IsNullOrEmpty(trimmed)) |
| | 3 | 54 | | return $"1С не провела {title}: причину 1С не сообщила. " + |
| | 3 | 55 | | "Документ сохранён непроведённым — откройте его в 1С и проведите вручную."; |
| | | 56 | | |
| | 13 | 57 | | var generic = GenericRefusalRegex.Match(trimmed); |
| | 13 | 58 | | if (generic.Success) |
| | 4 | 59 | | return $"1С не провела {title} «{generic.Groups["name"].Value.Trim()}»: подробностей 1С не вернула, " + |
| | 4 | 60 | | "чаще всего не хватает остатка на складе. " + |
| | 4 | 61 | | "Документ сохранён непроведённым — проверьте остатки в 1С и проведите его вручную."; |
| | | 62 | | |
| | 9 | 63 | | return $"1С не провела {title}: {trimmed}"; |
| | | 64 | | } |
| | | 65 | | } |