food-market/src/food-market.shared/Barcodes/InternalBarcode.cs
nns 848e747379 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>
2026-10-09 12:45:17 +05:00

60 lines
3.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace foodmarket.Shared.Barcodes;
/// <summary>Генерация внутреннего (внутримагазинного) штрихкода EAN-13 для товаров
/// без заводского кода. По решению владельца штрихкод обязателен у ЛЮБОГО товара;
/// если пользователь/импорт не задал — генерируем здесь.
///
/// Префикс выбирается так, чтобы НЕ пересекаться с весовыми/ценовыми префиксами
/// организации (иначе внутренний код распознается кассой как весовая этикетка).
/// Предпочтительно «02» (GS1 restricted circulation — внутреннее обращение, не
/// конфликтует с заводскими), иначе первый свободный 2-значный префикс.
/// Контрольная цифра — стандартная EAN-13.</summary>
public static class InternalBarcode
{
/// <summary>Контрольная цифра EAN-13 по 12 цифрам тела.</summary>
public static int Ean13CheckDigit(string body12)
{
var sum = 0;
for (int i = 0; i < 12; i++)
{
var d = body12[i] - '0';
sum += i % 2 == 0 ? d : d * 3;
}
return (10 - sum % 10) % 10;
}
/// <summary>Подобрать 2-значный префикс внутреннего кода. GS1 restricted circulation
/// (внутримагазинные) — это 02 и 20–29. НЕ используем «02»: с ведущим нулём 13-значный
/// код выглядит как UPC-A (зона США) и может совпасть с реальным товаром. Берём 2x
/// (20–29) — настоящие коды такие префиксы не используют — не пересекаясь с весовыми/
/// ценовыми префиксами организации (если разбор этикеток включён). Приоритет:
/// свободный из 20–29 → свободный из 90–99 → 02 (крайний случай).</summary>
public static int PickPrefix(WeightedBarcodeSettings settings)
{
var forbidden = new HashSet<int>();
if (settings.Enabled)
{
foreach (var p in settings.WeightPrefixes) forbidden.Add(p);
foreach (var p in settings.PricePrefixes) forbidden.Add(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;
return 2; // всё занято (весьма редкий случай) — крайний резерв
}
/// <summary>Сгенерировать внутренний EAN-13. <paramref name="rnd"/> — источник
/// случайности (для детерминизма в тестах можно передать свой).</summary>
public static string Generate(WeightedBarcodeSettings? settings = null, Random? rnd = null)
{
settings ??= WeightedBarcodeSettings.Default;
rnd ??= Random.Shared;
var prefix = PickPrefix(settings);
var body = new char[12];
body[0] = (char)('0' + prefix / 10);
body[1] = (char)('0' + prefix % 10);
for (int i = 2; i < 12; i++) body[i] = (char)('0' + rnd.Next(0, 10));
var body12 = new string(body);
return body12 + Ean13CheckDigit(body12);
}
}