fix: внутренний ШК 2x (не 02/ведущий-0) + разовый прогон «Проверки данных» при старте

Найдено Mac-Claude на стенде:
1. Генератор внутреннего EAN-13 давал «02…» (ведущий 0 → зона UPC-A, может совпасть
   с реальным товаром). InternalBarcode.PickPrefix теперь берёт 2x из restricted-
   circulation 20–29 (без ведущего нуля), не пересекаясь с весовыми префиксами орг;
   «02» — только крайний резерв. Юнит-тест обновлён.
2. «Проверка данных» на Демо показывала все 0 и lastCheckedAt=null — ночной cron ещё
   не отрабатывал после пересева. Добавлен разовый BackgroundJob.Enqueue(DataQualityJob)
   при старте: после деплоя/пересева счётчики считаются сразу по всем организациям.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 12:45:17 +05:00
parent 3f416e71ad
commit 848e747379
3 changed files with 19 additions and 7 deletions

View file

@ -169,6 +169,11 @@ public Task StartAsync(CancellationToken ct)
cronExpression: cronDataQuality, cronExpression: cronDataQuality,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Разовый прогон при старте: чтобы после деплоя/пересева Демо (и любой новой
// организации) счётчики «Проверки данных» были посчитаны сразу, не дожидаясь
// ночного cron (иначе панель/бейдж показывают 0 и lastCheckedAt=null).
BackgroundJob.Enqueue<DataQualityJob>(j => j.RunAsync(CancellationToken.None));
return Task.CompletedTask; return Task.CompletedTask;
} }

View file

@ -23,8 +23,12 @@ public static int Ean13CheckDigit(string body12)
return (10 - sum % 10) % 10; return (10 - sum % 10) % 10;
} }
/// <summary>Подобрать 2-значный префикс, не входящий в весовые/ценовые префиксы орг. /// <summary>Подобрать 2-значный префикс внутреннего кода. GS1 restricted circulation
/// Приоритет: 02 → свободный из 20–29 → свободный из 90–99 → 02 (как крайний).</summary> /// (внутримагазинные) — это 02 и 20–29. НЕ используем «02»: с ведущим нулём 13-значный
/// код выглядит как UPC-A (зона США) и может совпасть с реальным товаром. Берём 2x
/// (20–29) — настоящие коды такие префиксы не используют — не пересекаясь с весовыми/
/// ценовыми префиксами организации (если разбор этикеток включён). Приоритет:
/// свободный из 20–29 → свободный из 90–99 → 02 (крайний случай).</summary>
public static int PickPrefix(WeightedBarcodeSettings settings) public static int PickPrefix(WeightedBarcodeSettings settings)
{ {
var forbidden = new HashSet<int>(); var forbidden = new HashSet<int>();
@ -33,10 +37,9 @@ public static int PickPrefix(WeightedBarcodeSettings settings)
foreach (var p in settings.WeightPrefixes) forbidden.Add(p); foreach (var p in settings.WeightPrefixes) forbidden.Add(p);
foreach (var p in settings.PricePrefixes) forbidden.Add(p); foreach (var p in settings.PricePrefixes) forbidden.Add(p);
} }
if (!forbidden.Contains(2)) return 2;
for (int p = 20; p <= 29; p++) if (!forbidden.Contains(p)) return p; for (int p = 20; p <= 29; p++) if (!forbidden.Contains(p)) return p;
for (int p = 90; p <= 99; p++) if (!forbidden.Contains(p)) return p; for (int p = 90; p <= 99; p++) if (!forbidden.Contains(p)) return p;
return 2; return 2; // всё занято (весьма редкий случай) — крайний резерв
} }
/// <summary>Сгенерировать внутренний EAN-13. <paramref name="rnd"/> — источник /// <summary>Сгенерировать внутренний EAN-13. <paramref name="rnd"/> — источник

View file

@ -20,11 +20,15 @@ public void Generated_is_valid_ean13_with_correct_check_digit()
} }
[Fact] [Fact]
public void Default_prefix_is_02_and_not_parsed_as_weighted() public void Default_prefix_is_2x_not_leading_zero()
{ {
var code = InternalBarcode.Generate(WeightedBarcodeSettings.Default); var code = InternalBarcode.Generate(WeightedBarcodeSettings.Default);
code.Should().StartWith("02"); // Внутренний код — зона restricted circulation 20–29 (не «02»: с ведущим нулём
// Внутренний код НЕ должен распознаваться как весовая/ценовая этикетка. // 13-значный код выглядит как UPC-A и может совпасть с реальным товаром).
code.Should().StartWith("2");
code[1].Should().BeInRange('0', '9');
code[0].Should().NotBe('0');
// По умолчанию (разбор этикеток выключен) как весовая не распознаётся.
WeightedBarcode.Parse(code, WeightedBarcodeSettings.Default).IsWeighted.Should().BeFalse(); WeightedBarcode.Parse(code, WeightedBarcodeSettings.Default).IsWeighted.Should().BeFalse();
} }