using foodmarket.Domain.Catalog; using foodmarket.Domain.Common; namespace foodmarket.Domain.Organizations; public class Organization : Entity { public string Name { get; set; } = null!; public string CountryCode { get; set; } = "KZ"; public string? Bin { get; set; } public string? Address { get; set; } public string? Phone { get; set; } public string? Email { get; set; } public bool IsActive { get; set; } = true; /// Архивирована ли организация. Архивные не видны пользователям /// но данные сохраняются. Из архива можно восстановить или (после 30 дней) /// удалить навсегда — этим управляет SuperAdmin. public bool IsArchived { get; set; } public DateTime? ArchivedAt { get; set; } /// Account owner (главный владелец, не путать с админами роли). /// Это конкретный AppUser, который считается «хозяином» организации; /// SuperAdmin может сменить через отдельный action c reason в audit-log. public Guid? AccountOwnerUserId { get; set; } /// Персональный API-токен MoySklad. Храним per-organization чтобы /// пользователю не нужно было вводить его каждый раз при импорте. public string? MoySkladToken { get; set; } /// Chat ID владельца org в Telegram. Привязка делается через /// deep-link на бота `food-market` — бот шлёт код подтверждения в /// telegram, юзер вводит код в OrganizationSettings, мы сохраняем /// chatId. По этому ChatId Hangfire-джоб OwnerDailySummaryJob шлёт /// ежедневную сводку (выручка вчера, топ-3, low-stock). /// null — привязки нет; ноль допустимым не считаем. public long? OwnerTelegramChatId { get; set; } /// Валюта организации по умолчанию. Если MultiCurrencyEnabled=false, /// в UI выбор валюты скрыт — всё в этой валюте. public Guid? DefaultCurrencyId { get; set; } public Currency? DefaultCurrency { get; set; } /// Разрешены ли продажи/закупки в нескольких валютах. По умолчанию /// false — тогда UI не предлагает выбор валюты, всё в DefaultCurrency. public bool MultiCurrencyEnabled { get; set; } /// Показывать ли пользователю галку «В том числе НДС» на форме товара. /// Если false (по умолчанию) — магазин работает с одной ставкой НДС и галка /// скрыта, все товары считаются с НДС. Если true — можно для отдельных товаров /// (хлеб, медикаменты) снимать галку. public bool ShowVatEnabledOnProduct { get; set; } /// Показывать ли на форме товара и в фильтрах галку «Услуга». /// Большинство магазинов продают только физические товары — флаг выключен /// по умолчанию, чтобы не захламлять UI. public bool ShowServiceOnProduct { get; set; } /// Показывать ли на форме товара и в фильтрах галку «Маркируемый». /// Маркировка требуется только в нишевых категориях (алкоголь, лекарства, /// табак) — по умолчанию выключено. public bool ShowMarkedOnProduct { get; set; } /// Показывать ли поля «Минимальный остаток» / «Максимальный остаток» /// на карточке товара и одноимённую колонку в списке. Нужно в основном /// торговым сетям со свободным местом на полке — по умолчанию выключено. public bool ShowMinMaxStock { get; set; } /// Разрешить ли цены с дробной частью (две цифры после запятой). /// По умолчанию false — большинству KZ-магазинов хватает круглых тенге; /// если включено, MoneyInput работает с шагом 0.01 и форматирует с .00, /// иначе шаг 1 и значения округляются до целого даже при попытке прислать /// дробное через API. public bool AllowFractionalPrices { get; set; } /// Разрешить ли уходить счетам/кассам в минус. По умолчанию выкл.: /// расход больше остатка (оплата, выплата из смены) отклоняется с 409. /// Менять может только администратор организации. public bool AllowNegativeBalance { get; set; } /// Показывать ли в карточке товара поле «Эталонная цена». /// Default: true. public bool ShowReferencePriceOnProduct { get; set; } = true; /// Показывать ли в карточке товара поле «Страна происхождения». /// Default: false. Большинству KZ-магазинов это поле не нужно; /// включать для торговцев импортом или маркируемой продукцией. public bool ShowCountryOfOriginOnProduct { get; set; } /// Показывать ли в карточке товара поле «Описание». Default: /// false. Описания ведут единицы магазинов; обычно текстовая колонка /// просто захламляет карточку. public bool ShowDescriptionOnProduct { get; set; } /// Расширенный режим для организации. По умолчанию false /// — виден только «простой» набор функций (базовый розничный учёт). /// Включить может только SuperAdmin через /// PUT /api/superadmin/organizations/{id}/advanced-mode; в /// настройках магазина переключателя нет. При выключении данные /// скрытых модулей не удаляются; при повторном включении всё видно. /// Серверная защита — атрибут [RequiresAdvancedMode] на /// контроллерах скрытых модулей. public bool IsAdvancedMode { get; set; } // ─ Весовые этикетки (внутримагазинные EAN-13, как в МойСкладе) ───── /// Разбирать ли весовые этикетки (EAN-13 с префиксом из списков). /// Default FALSE (у владельца весов с печатью этикеток нет; штрихкоды весовых — /// обычные коды в карточке). Включается явно; разбор применяется только после /// проверки точного совпадения штрихкода. public bool WeightBarcodeEnabled { get; set; } = false; /// Префиксы (2 цифры) весовых этикеток, где зашит ВЕС. CSV, напр. /// «20,21,22,23,24». null → дефолт 20–24. public string? WeightBarcodeWeightPrefixes { get; set; } /// Префиксы весовых этикеток, где зашита ЦЕНА. CSV, напр. /// «25,26,27,28,29». null → дефолт 25–29. public string? WeightBarcodePricePrefixes { get; set; } /// Длина кода товара (PLU) внутри весовой этикетки. Default 5. public int WeightBarcodeCodeDigits { get; set; } = 5; /// Формат кода, который генерируется для НОВОГО весового товара: /// «ean13» — внутренний EAN-13 (как у штучных), «short» — короткий код /// «<префикс> + N цифр» (у владельца: 27 + 5 цифр). Default «short». public string WeightedCodeFormat { get; set; } = "short"; /// Префикс короткого кода весового товара (2 цифры). Default 27 (как у /// владельца). Следующий свободный N-значный номер добавляется автоматически. public int WeightedCodePrefix { get; set; } = 27; /// Сколько цифр в номере после префикса короткого кода весового товара. /// Default 5 (итого 7 знаков: «27xxxxx»). public int WeightedCodeDigits { get; set; } = 5; // ─ Уведомления об отрицательных остатках (идея владельца 08.10) ──────── /// Слать ли уведомления «товары ушли в минус» (центр уведомлений, /// Telegram, push). Default true. Выключение — только администратор. public bool NegStockNotifyEnabled { get; set; } = true; /// Агрегация: не чаще одного уведомления на склад раз в N минут. /// Default 15. Защита от спама при серии продаж в минус. public int NegStockNotifyIntervalMinutes { get; set; } = 15; /// Тихие часы — час начала (0–23, время Asia/Almaty, UTC+5). /// В интервале [From,To) уведомления копятся, но не шлются. null — тихих /// часов нет. Если From==To — считается, что тихих часов нет. public int? NegStockQuietFromHour { get; set; } /// Тихие часы — час конца (0–23). Интервал может переходить через /// полночь (напр. From=22, To=8). public int? NegStockQuietToHour { get; set; } /// Эскалация: если минус не закрыт за N часов — повторное /// уведомление владельцу/админам + блок «Просроченные минусы». Default 24. public int NegStockEscalateHours { get; set; } = 24; /// Отключённые проверки «Проверки данных» (CSV кодов: no-group, /// no-sale-price, no-barcode, dup-barcode, dup-name-unit). null/пусто — все включены. public string? DataQualityDisabledChecks { get; set; } // ─ Sprint 11: ОФД (фискализация чеков) ──────────────────────────── // Каждая организация выбирает СВОЕГО ОФД-оператора. Креды per-tenant: // у одной фирмы может быть Webkassa, у другой — Касса24. ApiKey/Secret // шифруются через DataProtection (purpose=`foodmarket.fiscal`) — в // открытом виде в API-ответах не возвращаются (только has-* флаги). /// Тип ОФД-провайдера. 0=None (не фискализировать — дефолт), /// 1=Mock, 2=Webkassa, 3=Kassa24, 4=OfdSolo. Значения — см. /// FiscalProviderKind в application-слое. public int FiscalProvider { get; set; } /// Зашифрованный ApiKey оператора (base64 через DataProtection, /// purpose=`foodmarket.fiscal`). Никогда не отдаётся в API-ответах. public string? FiscalApiKeyEncrypted { get; set; } /// Зашифрованный ApiSecret оператора (если требуется — у /// Webkassa, например, токен-логин, у Касса24 — отдельный secret). public string? FiscalApiSecretEncrypted { get; set; } /// Уникальный номер кассы у оператора. У Webkassa называется /// CashboxUniqueNumber, передаётся в каждом чеке. public string? FiscalCashboxUniqueNumber { get; set; } /// Опциональный override URL'а оператора (для тестового /// контура / sandbox'а). null → используется production URL из /// реализации провайдера. public string? FiscalApiBaseUrl { get; set; } // ─ Реестр товаров ───────────────────────────────────────────────────────── /// Пополняет ли каталог этого магазина общий «Реестр товаров» /// (фоновое задание берёт новые штрихкоды в реестр). По умолчанию true для /// зарегистрированных магазинов; демо/автотестовые выключаются. public bool ContributesToRegistry { get; set; } = true; /// Статус копии реестра в каталог магазина при регистрации: /// 0=не было, 1=идёт загрузка, 2=готово, 3=ошибка. Пока 1 — в админке /// показывается «Каталог из реестра загружается…». Повторно не копируется. public int RegistryCopyState { get; set; } public DateTime? RegistryCopiedAt { get; set; } }