Commit graph

118 commits

Author SHA1 Message Date
nns e39e4fe1d5 feat(registry+pricing): «Реестр товаров» + схема цен (закупочная/себестоимость/продажная)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
РЕЕСТР ТОВАРОВ (глобальный системный шаблон карточек):
- Домен ProductRegistry{Group,Card,Barcode} (Entity, без OrganizationId),
  миграция Phase47; штрихкод уникален в реестре (ключ новизны).
- Наполнение «из МойСклад заново» (команда суперадмина, повторяемая): читает
  entity/product (архивные/модификации/комплекты/услуги — считает и пропускает),
  группы из productFolder (путь), штрихкоды всех типов; buyPrice→закупочная+
  себестоимость, salePrices→продажная; пересоздаёт только MoySklad-карточки,
  Shop-карточки не трогает. МойСклад только чтение (пагинация 1000, 429-бэкофф).
- Фоновое пополнение из каталогов магазинов-участников (recurring, флаг в
  PlatformSettings, по умолчанию ВЫКЛ): новые штрихкоды → карточки; фильтр
  мусора (неверная контр. цифра EAN-13/8, пустое имя); существующие не трогает.
- Копия реестра в каталог нового магазина при регистрации — фоновым заданием
  (Hangfire) сразу после signup; баннер «Каталог загружается…», повторно не
  копирует. Остатков нет (0).
- Флаг организации ContributesToRegistry (демо/автотест — ВЫКЛ миграцией).
- SuperAdmin раздел «Реестр товаров»: поиск/правка/удаление карточек, тоггл
  фонового пополнения (причина+аудит), кнопка наполнения из МойСклад, флаги
  участия орг. Только SuperAdmin.

СХЕМА ЦЕН (решение владельца 12.10): три цены в карточке.
- Закупочная: ReferencePrice→PurchasePrice (миграция Phase48 RENAME COLUMN,
  данные сохранены). Каждая проведённая приёмка пишет закупочную = цена строки;
  unpost возвращает из предыдущей проведённой приёмки. API отдаёт purchasePrice
  и временно referencePrice (алиас для iOS).
- Себестоимость (Cost): только чтение, скользящее среднее (как было).
- Продажная (бывш. «розничная»): меняется приёмкой только при ручной правке в
  строке. Системный тип цены «Розничная цена»→«Продажная цена» (миграция+сидер),
  подписи в UI переименованы.
- Убраны: автозаполнение эталонной при первой приёмке, ReferencePriceRefreshJob
  (30-дневная перезапись), понятие «эталонная» в UI.

Тесты: SupplyPricingSchemeTests (приёмка без правок → продажная не меняется,
закупочная = цена строки, себестоимость средняя; правка продажной в строке;
unpost → закупочная вернулась) — 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 19:19:47 +05:00
nns 42e4f630b1 feat(superadmin): переключатель подгрузки из МойСклад + состояние прогона
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Суперадмин управляет дельта-подгрузкой МойСклад на стенд флагом в «Настройках
платформы» (по умолчанию ВЫКЛ), не трогая systemd-таймер руками.

- PlatformSettings: MoySkladDeltaSyncEnabled (bool, default false) + состояние
  последнего прогона (startedAt/finishedAt/ok/migrated/error); миграция Phase46
  (ADD COLUMN IF NOT EXISTS, идемпотентно).
- Super-admin API: GET/PUT /platform-settings/moysklad-delta — тоггл с reason,
  пишется в SuperAdminAuditLog; GET отдаёт состояние прогона. Только SuperAdmin.
- Admin API (для скрипта, роль Admin): GET /admin/moysklad/delta-sync (флаг),
  POST /admin/moysklad/delta-sync/report (итог прогона → PlatformSettings).
- Серверная защита: import-supplies при delta=true и выключенном флаге → 409
  с понятной причиной. Ручной импорт из мастера (без delta=true) не затронут.
- delta_sync.py: сначала спрашивает флаг — при «Выкл» одна строка в лог и выход
  без изменения lastSync (повторное включение догонит пропущенное); помечает
  запросы delta=true; в конце репортит итог прогона суперадмину.
- SuperAdminPlatformSettingsPage: блок «Подгрузка из МойСклад» — тоггл Вкл/Выкл
  с причиной, подпись, состояние последнего прогона.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 17:08:41 +05:00
nns a2183e9532 perf(stock-report): покрывающий индекс движений (Phase45) — отчёт остатков 2.4с→1.1с на реальном объёме
Some checks failed
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:29:27 +05:00
nns 1ee3c9b598 fix(migration): import — ChangeTracker.Clear при сбое товара (устранить каскад потери прогона)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:48:18 +05:00
nns 150947c5fb fix(migration): импорт переносит дубль-ШК и дубль-артикул карточки (дизамбиг артикула, пары товар-ШК)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- create: артикул-коллизия → суффикс msId (перенести карточку, не падать на unique)
- create/update: ШК не уникален → добавляем код каждой карточке (пары товар|код против
  повторов), дубли ловит «Проверка данных»

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:21:03 +05:00
nns b1bdf5c228 fix(migration): не схлопывать разные карточки МойСклада по артикулу + актуализировать объяснение сверки
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:11:06 +05:00
nns 9d5c23d32c fix(migration): штрихкод не уникален (не сливать карточки), долг по контрагенту, выравнивание остатков
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- Phase44: снята UNIQUE с (OrganizationId,Code) product_barcodes — «минимум как МойСклад»;
  импорт больше НЕ сливает/не пропускает карточки с дублем ШК (MergeByBarcode default=false,
  dupeInInput не пропускает). Дубли ловит «Проверка данных» (dup-barcode, уже есть).
- SupplierDebts: долг на уровне КОНТРАГЕНТА = Σприёмок − Σвозвратов − ΣисходящихРКО + Σвходящих
  + корректировки (учёт paymentout/cashout/paymentin/cashin по CounterpartyId).
- AlignStockToMoySkladAsync (type=align-stock): корректирующие Initial-движения на разницу
  (МС report/stock/all − наш остаток) → остаток точно = МойСклад, инвариант stock=Σдвижений.
- GetStockAllAsync в клиенте; MsStockRow DTO.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:00:34 +05:00
nns 81398fadad fix(migration): экранировать filter=updated>= целиком; дельта-драйвер — точный повторный опрос job + audit tolerant
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- StreamDocumentsAsync: Uri.EscapeDataString всего выражения updated>=<дата> (>= → %3E%3D),
  не только даты — иначе часть форм 400/неоднозначна
- delta_sync: повторное чтение job после Succeeded (устранить гонку счётчиков), audit в try
- reconcile: розница демо/возвраты раздельно, URL-энкод фильтра чеков по месяцам

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:12:59 +05:00
nns 39d0243ba3 feat(migration): денежные документы (платежи/ордера/внесения/выплаты) + плуг балансов счетов
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- ImportMoneyDocsAsync: paymentout/paymentin/cashin/cashout/retaildrawercashin/out
  → MoneyTransaction (история), agent→контрагент по externalId, суммы ÷100, МСК→UTC
- EnsureMoneyAccountsAsync: касса + расчётный счёт орг (идемпотентно)
- SetMoneyOpeningBalancesAsync: OpeningBalance = report/money/byaccount − Σпроводок,
  текущий баланс счёта точно совпадает с МойСкладом
- GetMoneyByAccountAsync в MoySkladClient; MsMoneyByAccountRow, paymentPurpose в DTO
- контроллер: type=money (все деньги+плуг), type=balances, одиночные денежные типы

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 16:32:22 +05:00
nns 5d11dd7434 feat(migration): возвраты розницы + инвентаризация + дельта-синхронизация (updated>=)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- ImportRetailReturnsAsync (retailsalesreturn): связь с чеком по externalId demand, +остаток
- ImportInventoriesAsync (inventory): Completed без движений, BookQty/ActualQty из calculated/quantity
- Все 7 импортёров принимают updatedFromMsk → filter=updated>= (МСК) для дельты
- Контроллер: ?updatedFrom=<МСК> прокинут во все типы

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 16:18:32 +05:00
nns 9506f9edc8 perf(migration): резюм документов по offset (не перечитывать уже перенесённое)
startOffset у всех документных импортов + эндпоинт ?offset=N: при продолжении
пропускаем уже перенесённые через offset API (а не перечитывая сотни страниц).
Идемпотентный skip по msId держит границу.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:52:28 +05:00
nns 0c9d20a469 feat(migration): мигратор оприходований/списаний/возвратов поставщику/розничных продаж
По проверенному шаблону приёмок (идемпотентно по msId, резюмируемо, МСК→UTC, тиыны,
дизамбигуация номеров, SaveChanges по документу, движения остатков):
- enter (+остаток), loss (−остаток, WriteOff), purchasereturn (−остаток, SupplierReturn),
  retaildemand (−остаток, RetailSale; оплата нал/безнал из cashSum/noCashSum, кассир=owner).
- Общие помощники LoadMaps/Aggregate. Эндпоинт POST /import-supplies?type=enter|loss|
  purchasereturn|retaildemand (фон, job с нужным kind).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:34:36 +05:00
nns 025d468492 fix(migration): дизамбигуация повторяющихся номеров приёмок (МойСклад дублирует по годам)
Номера МойСклада повторяются по годам, а у нас (OrganizationId, Number) уникален →
дубли роняли импорт. Сохраняем оригинальный номер где можно, при коллизии —
«<номер>-<год>[-N]». Оригинал всегда в MoySkladExternalId.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:08:15 +05:00
nns 955c3f5e77 fix(migration): очищать ChangeTracker при ошибке документа (стоп каскада сбоев)
Сбой SaveChanges одного документа оставлял сущности tracked → все последующие
падали (каскад), импорт застревал. Теперь в catch — ChangeTracker.Clear().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:53:05 +05:00
nns 6fdc9e527a fix(migration): приёмки — сохранение по одному документу + агрегация позиций по товару
Батч из 50 приёмок с общим товаром давал дубль строки stocks (ApplyMovementAsync
создавал новую строку, не видя ещё не сохранённую). Теперь: SaveChanges после КАЖДОГО
документа (следующий видит закоммиченные остатки) + агрегация позиций по товару в
документе (средневзвешенная цена) + ChangeTracker.Clear (не копим граф на 7k докумов).
Идемпотентно по msId — повторный запуск продолжает с места.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:31:27 +05:00
nns 10cd9f989b fix(migration): суммы документов МойСклада как decimal (приходят «900000.0») + GET статуса job
МойСклад отдаёт sum/cashSum/price как число с точкой (900000.0) → long не парсился
(«JSON value could not be converted to System.Int64»). Поля DTO → decimal. Плюс
GET /api/admin/moysklad/jobs/{id} для наблюдения за прогрессом/ошибками переноса.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:19:52 +05:00
nns f2a1493ea0 fix(migration): лёгкий expand для документов (positions, не positions.assortment) + таймаут 130с
Тяжёлый expand=positions.assortment раскрывал весь товар в каждой позиции → страница
гигантская и зависала. assortment.meta.href в позиции есть и без раскрытия. Таймаут
HttpClient МойСклада поднят до 130с (страница иногда > 100с).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:07:54 +05:00
nns 2ed9f04cb2 feat(migration): документный мигратор МойСклад (приёмки) + externalId на документах
Part 2 (документы) — проверенный шаблон на приёмках:
- Phase41: MoySkladExternalId у counterparties/stores/units/retail_points/employees и
  документов (supplies/enters/losses/inventories/supplier_returns/retail_sales/
  cash_shifts/money_transactions) + индексы. Свойства на доменных сущностях.
- MoySkladClient.StreamDocumentsAsync: документы в хронологии (order=moment,asc), expand
  позиций/связей (limit=100), обработка 429/сбоев.
- MoySkladDocumentImportService.ImportSuppliesAsync: идемпотентно по msId, резюмируемо
  (skip перенесённых), moment МСК→UTC, суммы из тиынов, поставщик по externalId контрагента,
  товары по externalId, проведение → движения остатков (StockService). Журнал ошибок.
- Импорт контрагентов теперь пишет MoySkladExternalId (нужно для ссылок в документах).
- Эндпоинт POST /api/admin/moysklad/import-supplies (фон, job). Остальные типы — по шаблону.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:53:52 +05:00
nns d0fee10e96 fix(barcode): весовой разбор выкл по умолчанию; поиск по ШК — точное совпадение
Реальный сценарий владельца: весов с печатью этикеток нет, штрихкоды весовых —
обычные коды «27xxxxx» в карточке. Исправлены два бага:
- поиск по ШК матчил весовой PLU по АРТИКУЛУ (вода арт.12345 → 1,5 кг) — убрано,
  поиск по штрихкоду больше нигде не матчит по артикулу;
- разбор весовых этикеток был дефолт-ON (WeightedBarcodeSettings.Enabled,
  Organization.WeightBarcodeEnabled) и ловил обычные коды — теперь дефолт OFF.

- ByBarcode: сначала точное совпадение штрихкода (вкл. короткие «27xxxxx»),
  разбор этикетки — только если включён в орг И точного ШК нет.
- Phase40: WeightBarcodeEnabled DEFAULT false + выключение у существующих орг;
  WeightedCodeFormat('short'|'ean13')/WeightedCodePrefix(27)/WeightedCodeDigits(5).
- Генератор ШК нового весового товара: короткий «<prefix>+N цифр» (27xxxxx, next-free)
  или EAN-13 по настройке; уникальность.
- Настройки орг отдают/принимают формат кода весовых.
- Sync отдаёт unitShort («шт»/«кг»/«л») рядом с unitCode (ОКЕИ) — shared UnitShort.
- Тесты: MobileApi (exact-only по умолчанию; не матчит артикул; PLU как ШК при вкл) 9/9;
  unit WeightedBarcode/UnitShort 16/16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:02:43 +05:00
nns 9dc8f6e044 fix(inventory): записывать автора движения (CauseUserId) для всех документов
CauseUserId в уведомлениях о минусах был null: StockService писал автора только
если вызывающий явно передал CreatedByUserId в черновике, а большинство
документов (касса/списание/перемещение/приёмка/…) его не передавали. Теперь
автор берётся из черновика, иначе — из текущего пользователя (ITenantContext.UserId)
в единой точке ApplyMovementAsync, поэтому пишется и в StockMovement, и в алерт
для всех типов документов. Тест: минус от списания → causeUserId = текущий юзер.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 09:52:33 +05:00
nns 261a720b52 feat(data-quality): фоновая «Проверка данных» — бэкенд (п.1,3,4)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Hangfire DataQualityJob (ночью 02:20 UTC) считает по орг. счётчики проблем: без группы
(корень/«Без группы»), без цены продажи, без штрихкода (кроме услуг), дубль штрихкода,
возможный дубль имя+единица; товары в минусе — ссылкой на центр (не дублируем). Результаты
в data_quality_results (Phase43), живые детальные списки — по запросу.
API /api/data-quality: сводка, {check}/products, recompute, dismiss («это разные товары»),
bulk set-group/set-price/generate-barcodes, PUT settings (отключение проверок). unknown-cost
отложен до FIFO; PLU убран (поправка владельца). Integration-тест: счётчики/список/bulk/dismiss.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 08:11:57 +05:00
nns 560d52e8cd feat(notifications): Web Push (VAPID) — браузерный push о минусах
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Канал Web Push для центра уведомлений (item 5). Ключи VAPID генерируются один раз
и хранятся в platform_settings (без ручной настройки секретов); публичный отдаётся
фронту. Таблица web_push_subscriptions (Phase42) + /api/web-push/{public-key,
subscribe,unsubscribe}. Отправка через web-push-csharp из фонового джоба минусов
(best-effort, протухшие подписки чистятся). SW: обработчики push/notificationclick
(am-v3→am-v4), клик открывает нужный путь. Кнопка «Вкл. push» в колокольчике.
Работает в т.ч. в iOS Safari для PWA на домашнем экране.

Тесты: integration — публичный ключ стабилен, подписка идемпотентна, 400/unsubscribe
(4/4 группы); unit 299/299.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 03:49:32 +05:00
nns 7e4cdf4c4f fix(tests): починка юнит-харнесса, вскрытая компиляцией UnitTests
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Два предсуществующих бага, незаметных пока UnitTests не компилировался (с pass 1
конструктор StockService требовал новый параметр):

- MoneyConfigurations: CHECK ck_money_link_single_target использовал Postgres-only
  `::int` — SQLite (EnsureCreated в юнит-тестах) падал 'unrecognized token ":"'.
  Переписан на портируемый CASE WHEN (семантика та же, что в миграции Phase33a).
- ApiReferenceDocsJob: regex прав требовал `[RequiresPermission(...)]` в отдельных
  скобках и пропускал комбинированную форму `[HttpGet, RequiresPermission("X")]`
  (её используют в т.ч. контроллеры negative-stock). Убраны обязательные скобки.

Юнит-сьют снова зелёный: 297/297.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 03:23:35 +05:00
nns 29a951a6ff feat(notifications): центр уведомлений + агрегация/эскалация минусов + каналы
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Персистентные уведомления (таблица notifications, Phase41) + REST
/api/notifications (список/счётчик/read/read-all) → центр уведомлений в вебе
(колокольчик, API-backed) и мобильном API.

Фоновый джоб NegativeStockNotificationJob (cron */5): агрегирует новые минусы
в ОДНО уведомление на склад (дебаунс по NotifiedAt + интервал орг., по умолч.
15 мин), уважает тихие часы (Asia/Almaty); эскалация просроченных (>N часов,
по умолч. 24) владельцу/админам. Получатели — админы, владелец, кладовщики
(право InventoryEdit). Каналы: SignalR (живой колокольчик), Telegram (владельцу),
push-заглушка.

Настройки организации: NegStockNotify{Enabled,IntervalMinutes,QuietFrom/ToHour,
EscalateHours} + GET/PUT /api/organization/settings + секция в настройках веба.
Блок «Просроченные минусы» на дашборде.

Тесты: unit тихих часов 3/3; integration — джоб создаёт уведомление админу,
дебаунс, read-all 3/3 (вся группа NegativeStockTests зелёная).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 02:56:59 +05:00
nns 3547e823c3 feat(notifications): задел push под iOS — device_tokens + IPushSender (заглушка)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Таблица токенов устройств (device_tokens, Phase40) + POST/DELETE /api/me/device-tokens
для регистрации iOS/Android/Web push-токенов. Контракт IPushSender.SendAsync; в DI
включён StubPushSender (пишет в лог) — реальный APNs/FCM/VAPID подменит заглушку, когда
появятся ключи, без правок вызывающего кода. Описано в docs/mobile-api.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 02:27:05 +05:00
nns 9ba1edb236 feat(stock): разрешение продажи в минус + предупреждения об отрицательных остатках
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Решение владельца 08.10: продавать/списывать в минус РАЗРЕШЕНО, но с постоянным
предупреждением и требованием провести инвентаризацию.

- Снята блокировка «недостаточно остатка» (409 oversell): розница, касса (POS-синк),
  опт-отгрузка, списание, перемещение, возврат поставщику, отмена приёмки. Деньги
  (минус счёта) — по-прежнему запрещены (другое правило).
- NegativeStockAlert (сущность + миграция Phase39): создаётся/обновляется при уходе
  остатка < 0 (единая точка — StockService), авто-закрытие при возврате ≥ 0. Частичный
  уникальный индекс: один открытый алерт на (org, store, product). Причина: тип/номер
  документа/автор/время.
- API /api/negative-stock: список (с подсказкой «возможно, не проведена приёмка П-…»),
  /count (для баннеров), /recount (черновик инвентаризации по минусам склада). Отчёт
  «Остатки» — фильтр negativeOnly.
- Веб: постоянный баннер «N товаров в минусе — нужна инвентаризация» (сворачивается,
  крестиком не закрыть) + «Пересчитать эти товары»; в остатках — красная пометка и
  фильтр «Только минусы».
- Тесты: integration 2/2 (минус разрешён → алерт → recount → авто-закрытие; фильтр);
  full-chain шаг 9/13 переписаны (oversell/отмена приёмки → проведено + предупреждение).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:18:29 +05:00
nns 0d44cdabaf feat(inventory): модель МойСклада — инвентаризация не двигает остатки
Замечание владельца: инвентаризация стояла «Проведено» без оприходования/списания.
Переделано по МойСкладу (docs/inventory-moysklad-reference.md):

- InventoryStatus Draft→Completed (было Posted; значение 1 то же). Инвентаризация
  остатки НЕ двигает: endpoint complete замораживает строки без движений.
- create-enter (излишки diff>0) / create-loss (недостача diff<0) — создают черновики
  Оприходования/Списания со строками разницы + ссылка InventoryDocId. Повторно нельзя (409).
- Кнопки «Применить результаты» НЕТ (поправка Mac-Claude — не одобрено владельцем).
- unpost («вернуть в черновик») отменяет проведение и удаляет связанные документы.
- post/unpost — временная совместимость для мобильного «FM Склад» (post = завершить +
  создать и провести оба документа); помечено в docs/mobile-api.md.
- Enter/Loss: поле InventoryDocId (+ миграция Phase38) + в DTO «на основании инвентаризации №…».
- InventoryResult (application) — чистая разбивка diff→излишки/недостача (unit-тесты:
  разница/сумма/только посчитанные строки/округление).
- InventoryMigrationController (dry-run/apply): старые Posted+InventoryAdjustment →
  связанные проведённые документы переметкой движений (не задваивает), с проверкой
  неизменности остатков. Скрипт deploy/migrate-inventory-to-docs.sh (backup + запуск).
- Демо-сидеры: инвентаризации теперь Completed без прямых движений.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:34:05 +05:00
nns 03bdf4d4f1 rebrand: письма и серверные строки ALL MARKET
- EmailTemplates (invite/low-stock/weekly-summary): шапка с текстовым
  вордмарком «all market» (бренд-цвета, всегда рендерится — без внешних
  картинок и привязки к домену) + подпись «ALL MARKET — система учёта торговли».
- Имя отправителя по умолчанию, темы/тела системных писем, экспорт-README,
  дневная сводка, тест SMTP → ALL MARKET.
- Цвета писем и домены не менялись.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:34:38 +05:00
nns ac55da8d06 feat(pos): избранные товары (плитки) + переключатель тёмной темы
- Избранное: Product.IsFavorite/FavoriteOrder (миграция Phase37), в POS-синке
  (ProductSyncDto) и в карточке товара (ProductInput). Касса: PosStore хранит
  флаг (+ALTER для старых БД) и GetFavorites(); на экране продажи — сворачиваемая
  сетка плиток «быстрых товаров» (товары без штрихкода: хлеб, овощи на вес),
  тап добавляет в чек. Unit-тест GetFavorites (порядок/фильтр). 44 POS unit зелёные.
- Тёмная тема — опция: кнопка-переключатель в шапке (ToggleTheme), выбор
  сохраняется на устройстве (kv device.theme), применяется при старте. На тех же
  токенах (ThemeDictionaries Light/Dark). По умолчанию — светлая. Кадр «тёмная тема» в галерее.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:35:56 +05:00
nns 6b959ac59d feat(pos): быстрый вход кассира по PIN (grant_type=pin) + lockout
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- Сервер: Employee.PinHash (PBKDF2, миграция Phase36); POST
  /api/organization/employees/{id}/pin — админ задаёт/снимает PIN (4–6 цифр,
  уникален среди активных сотрудников на пересекающихся точках). Кастомный
  OpenIddict-грант grant_type=pin (retail_point_id + pin): находит кассира,
  привязанного к точке, по PIN и выдаёт токены. Брутфорс-защита — in-memory по
  точке (5 неверных → блок 5 мин). Клиент-сидер досоздаёт gt:pin существующим
  клиентам (без пересоздания). Пароль остаётся запасным.
- Касса: PIN-пад на зарегистрированном устройстве (маскированный ввод, 0–9/⌫/
  Войти), переключение на вход по логину; AuthService.PinLoginAsync.
- Тесты: unit PinHasher (9), integration (3): вход по PIN выдаёт рабочий токен;
  5 неверных → блок; дубль PIN на точке → 409. + скриншот PIN-пада в галерее.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:38:36 +05:00
nns 5173eaa07c feat(catalog): штрихкоды товара — добавление + весовые этикетки
- POST /api/catalog/products/{id}/barcodes {code, kind}: добавить штрихкод к
  товару, уникальность в организации, 409 «штрихкод уже у товара X» (+название),
  идемпотентно при повторе тому же товару, автоопределение типа по длине.
- Весовые этикетки (внутримагазинные EAN-13, как в МойСкладе): общий парсер
  food-market.shared/Barcodes/WeightedBarcode (настраиваемые префиксы веса/цены,
  длина PLU); касса pos.core делегирует туда. Настройки организации
  (WeightBarcode*: enabled/префиксы/длина кода) + миграция Phase35.
- GET /api/catalog/products/by-barcode/{value} разбирает весовую этикетку →
  товар + количество (вес) / встроенная цена; обычный ШК — как прежде.
- GET/PUT /api/organization/settings отдаёт/принимает настройки (блок весовых
  применяется только если передан — round-trip-safe для текущей веб-формы).
- Офлайн-каталог ProductSyncDto: явный isWeighed (packaging==2) для мобильного/кассы.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 05:39:20 +05:00
nns 1e6d7fb2ab fix(money): движения смены попадают в остатки счетов + запрет отрицательного остатка
Раньше продажи/возвраты/внесения/выплаты/расхождение смены не влияли на остаток
кассы (касса уходила в −, эквайринг оставался 0). Теперь при закрытии смены
MoneyService.MaterializeShiftMovementsAsync создаёт Posted-движения money_transactions
по типам (Category): продажи нал/карта, возвраты нал/карта, внесение, выплата,
излишек/недостача — привязанные к смене, идемпотентно. Остаток кассы/эквайринга и
отчёт «Деньги» теперь верны автоматически (всё читает money_transactions).

Отрицательный остаток: расход (CashOut/PaymentOut/TransferOut) больше остатка счёта
и выплата из смены больше наличных смены → 409 insufficient-funds; настройка
Organization.AllowNegativeBalance (по умолч. выкл., меняет только Admin).

Миграция Phase34 (idempotent): money_transactions.Category/.CashShiftId,
organizations.AllowNegativeBalance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 03:11:09 +05:00
nns 8785fd60d1 fix(money): открытие смены на неактивной/непривязанной точке продаж → отказ
OpenShiftAsync проверяет RetailPoint.IsActive (retail-point-inactive) и привязку
кассира к точке (retail-point-not-assigned; у кого привязок нет — на любой точке).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:10:11 +05:00
nns deef65d819 test(money): unit (MoneyMath 25) + integration (7 на Postgres) + MoneyMath refactor
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- MoneyMath: вынесены чистые формулы (статус оплаты, ожидаемая наличность,
  разница смены, остаток счёта, долг поставщика, защита от переплаты);
  сервис/SuppliesController/MoneyReportsController теперь зовут их.
- UnitTests/MoneyMathTests: 25 кейсов на формулы + money-флаги RolePermissions.
- IntegrationTests/MoneyModuleTests (реальный Postgres, 7 [Fact]):
  мультитенантность (Б не видит счета А), инвариант остатка, частичная→полная
  оплата приёмки, переплата→409, долг поставщика, продажа без смены→409 +
  после open→ok, расчёт expected/difference при закрытии смены.
- ApiActor: helpers CreateRetailPoint/CreateMoneyAccount/CreatePostedSupply.
- tests/e2e/scrape-moysklad.ts — скрейпер для docs/money-moysklad-reference.md.

Прогон: UnitTests 25/25 ✓, IntegrationTests money 7/7 ✓ (33s, Testcontainers).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 22:11:19 +05:00
nns a25b5b3277 feat(money): бэкенд модуля «Деньги» для простого режима (пп.0–6)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Сверка с МойСкладом (docs/money-moysklad-reference.md) + домен/инфра/API:

- Счета и кассы (MoneyAccount: Cash/Safe/Bank/Acquiring, начальный остаток,
  привязка к магазину, IsDefault/IsActive, инвариант CurrentBalance).
- Денежные документы (MoneyTransaction: PaymentIn/Out, CashIn/Out, переводы
  парой TransferOut↔TransferIn; статьи расходов ExpenseItem; проведение/отмена
  в Serializable-транзакции через MoneyService).
- Оплата поставщикам: Supply/SupplierReturn.PayedSum + MoneyTransactionLink,
  SuppliesController отдаёт PaymentStatus + фильтр paymentStatus.
- Взаиморасчёты: CounterpartyAdjustment + MoneyReportsController.SupplierDebts.
- Смены кассы: CashShift + CashDrawerOperation (open/close, expected↔actual,
  Z-отчёт), запрет розничной продажи без открытой смены в RetailSalesController.
- Отчёт «Деньги»: balances/movement с проверкой инварианта остатков.

Права: RolePermissions +MoneyView/MoneyEdit/CashShiftOperate; сидер раздаёт
кассиру CashShiftOperate+MoneyView, кладовщику — нет; 5 системных статей
расходов на новую орг. Миграция Phase33a (ручная, с [Migration]+[DbContext]).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 21:52:04 +05:00
nns e45600df4b feat(invoices+catalog): порт улучшений 06.10.2026 из sklad
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Основное:

* Program.cs: SuppressImplicitRequiredAttributeForNonNullableReferenceTypes=true
  + InvalidModelStateResponseFactory → ValidationProblemDetails + Serilog Warning.
  Пустые необязательные строковые поля в InvoiceLine (Note, BarcodeSource и т.п.)
  больше не роняют запрос в 400. humanizeError на фронте уже умеет этот формат.
* InvoiceModels.cs: Name/Source/FilePath → string?.
* Validators.cs: InvoiceSaveLinesValidator (Lines NotNull), InvoiceApplyValidator
  (Guid.Empty на Store/Currency = ошибка, SupplierId опционален).

Служебный «Поставщик не указан» (адаптация sklad §11 под multi-tenant):

* Phase32b миграция добавляет Counterparty.IsPlaceholder bool + partial index
  по IsPlaceholder=true.
* InvoicesController.Apply: supplierId опционален; если не указан —
  ResolvePlaceholderCounterpartyAsync ищет/создаёт одного служебного per org.
  Имя из накладной в комментарий документа НЕ пишется.
* CounterpartiesController: служебный всегда последним в List сортировках,
  Update и Delete при IsPlaceholder → 409 Conflict.
* CounterpartyDto.IsPlaceholder + Mapster-config обновлены.
* Автоподбора/автозаведения контрагентов нигде не было — специально проверили.

Applied-накладная readOnly (§10):

* InvoiceDetailDto: добавлены supplyNumber + processedAt.
* InvoiceReviewPage: applied-режим — плашка «Черновик приёмки № … создан …»
  со ссылкой на приёмку, шапка и строки readOnly, скан-блок и apply-кнопка
  скрыты. После успешного apply — invalidateQueries, экран сразу readOnly.

Текст ошибки apply (§5) и список накладных (§6):

* ApplyDialog: error → detail → title → (код), __silent на axios.
* Invoices list: parseServerDate подшивает «Z» если его нет, «Загружена DD.MM.YY HH:MM»
  в бейдже статуса (видимо в десктоп-таблице и в мобильной карточке — у нас общая).

Тесты + проверки:

* tests/food-market.IntegrationTests/InvoiceApplySklad0610Tests.cs — 5 кейсов:
  SaveLines со всеми null, apply без поставщика с idempotency по placeholder'у,
  Delete/Update placeholder → 409, applied detail содержит processedAt +
  hasFiles=false, apply с Guid.Empty storeId → ValidationProblemDetails.
  Все 5 ✓.
* stage retest на https://test.admin.food-market.kz: upload+SaveLines(null-fields)+
  apply(без поставщика)+apply(с ИП Бегимбаев)+DELETE placeholder → весь flow
  зелёный, служебный один на орг, hasFiles=false после apply.
* quality-watchdog step 9 (menu-walk): 3/3 passed (3.7m).
* Фото-накладная через Claude CLI не гонял — пайплайн apply→DeleteStored
  закрыт через hasFiles=false в тестах и реальный stage smoke.

docs/supply-inventory.md — шаг 7 расписан под необязательного поставщика,
новый шаг 8 (applied → readOnly), миграция Phase32b в таблице «Файлы».

docs/sklad-0610-progress.md — финал 9/9 с подробным журналом решений.

Пред-существующие (не трогал):

* OrgAuditLogTests.Counterparty_update_diff_contains_before_after → 403 от
  [RequiresAdvancedMode] на /api/admin/audit-log (commit 921bca0, s29);
* ApiReferenceDocsJobTests.Scans_endpoints_with_doubly_nested_generic_return —
  регекс-тюнинг доков.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-07 19:50:03 +05:00
nns f434e87d9b fix(hotfix): invoice500 + фоновые 403 на reload + regression e2e
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Owner caught on stage (2026-06-07 18:57):
1) POST /api/purchases/invoices/upload → 500: migration Phase31a создала
   invoice_uploads.UpdatedAt как NOT NULL, но конвенция проекта — DateTime?
   nullable + стамп только для Modified в AppDbContext.StampTimestamps.
   EF на INSERT отправлял UpdatedAt=NULL → 23502. Фикс: Phase31b DROP NOT NULL
   + Phase31b для invoice_aliases.
2) Фоновые 403 → тост «Нет доступа» при F5 на любой странице:
   - SavedPresets фетчил /api/user/presets (RequiresAdvancedMode) из любой
     страницы с фильтрами в простом режиме → 403;
   - useOnboardingProgress фетчил /api/organization/employees (Admin only)
     для кладовщика/кассира на '/' → 403.
   Фикс:
   - api.ts: GET-403 silent, тост только на действия юзера (POST/PUT/DELETE);
   - SavedPresets gated на advancedMode (useMe + enabled);
   - useOnboardingProgress canSeeEmployees-gate.
3) tests/food-market.IntegrationTests/InvoiceUploadHttpTests.cs — HTTP-прогон
   POST /upload через Testcontainers-Postgres (дыра: s31 pipeline tests
   проверяли matcher, но ни разу не инсертили в invoice_uploads).
4) tests/e2e/scenarios/stage-ui-menu-walk.spec.ts — regress: каждая демо-роль,
   каждый пункт меню, F5, assert 0 unexpected 4xx/5xx + 0 error-тостов +
   0 console.error. В quality-watchdog step 9 с 4-часовой каденцией.
5) tests/e2e/scenarios/stage-ui-invoice-upload.spec.ts — demo-keeper
   загружает реальное фото накладной (tests/fixtures/invoice-sample.jpg,
   сгенерирована Playwright-ом из HTML, 1800×2400 JPEG) → ждёт recognition
   → экран проверки. При не-настроенном Claude CLI тест fail'ится с ясным
   сообщением (это тоже баг).

Прогон локально 4/4 (InvoicePipelineTests 2/2 + InvoiceUploadHttpTests 2/2).
docs/hotfix-invoice500-403-progress.md — ход работы 1-4/5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-05 00:21:41 +05:00
nns 864d492c93 feat(s32): MoySklad-импорт — upsert по external id, merge по barcode, skip archived, UOM-mapping
Для полной заливки каталога МойСклада в «Демо-магазин» на стейдже
переделал импорт по требованию «идемпотентный + без дублей».

Схема:
- Phase32a_MoySkladExternalId: VARCHAR(64) на products и product_groups
  + partial-unique index (OrgId, MoySkladExternalId) WHERE IS NOT NULL.
  NULL разрешён — товары, созданные вручную, без MS id.

Клиент (MoySkladClient):
- Auto-detect Basic vs Bearer: МойСклад принимает оба, но сторонние
  проекты (sklad на prod-vm) используют Base64(login:password). Строку
  распознаём как base64 с ':' в декоде → ставим Basic, иначе Bearer.
- StreamProductsAsync принимает includeArchived (default true для
  back-compat; импортный путь передаёт false).
- GetAllUomsAsync — грузит справочник единиц измерения один раз.

Сервис (MoySkladImportService):
- MoySkladImportOptions: OverwriteExisting / SkipArchived / MergeByBarcode.
- Resolution order: MS id → barcode → article. Первый найденный
  апдейтится, не плодит дубль.
- Группы — upsert по MS id; pre-существующие группы без MS id с тем же
  именем в том же родителе — подхватываются (MS id проставляется).
- Товары из архива MoySklad (archived=true) полностью пропускаются
  (счётчик SkippedArchivedInSource в результате).
- Barcode-дубли внутри MoySklad: первый встреченный выигрывает, остальные
  пропускаются с записью в Errors (счётчик SkippedDuplicateBarcodeInSource).
- UOM: MS uom.id → MS uom.code → наш локальный Code. Весовые товары
  (weighed=true) всегда ставятся в «кг».
- Расширенный MoySkladImportResult: Created / MergedByBarcode /
  SkippedArchivedInSource / SkippedDuplicateBarcodeInSource / GroupsUpdated.

Контроллер (MoySkladImportController):
- ImportRequest получает SkipArchived (default true), MergeByBarcode
  (default true). Токен по-прежнему принимается в body и НЕ сохраняется
  в Organizations.MoySkladToken, если передан в теле запроса.
- Финальный Message прогресса собирает все счётчики.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-04 23:22:14 +05:00
nns 11b43aefe5 feat(s31): приёмка по фото — upload → Claude CLI → match → Supply Draft (1/8)
Backend-pipeline по образцу ~/ref/sklad/sklad/Invoices/, но документы
создаются в НАШЕЙ базе (Supply), не в МойСкладе.

Домен (food-market.domain/Purchases/InvoiceUpload.cs):
- InvoiceUpload: файл + статус (pending/parsing/needs_review/applied/failed)
  + распознанная шапка + ParsedJson (jsonb) + ссылка на созданный Supply.
- InvoiceAlias: запомненное соответствие (SupplierKey, SourceCode|
  SourceNameNorm) → ProductId. Два частично-уникальных индекса (filter
  SourceCode/SourceNameNorm IS NOT NULL).

Миграция Phase31a (идемпотентная ADD/CREATE IF NOT EXISTS; [Migration]+
[DbContext]).

Infrastructure/Invoices/:
- IInvoiceExtractor + ParsedInvoice/InvoiceLine DTO.
- ClaudeCliInvoiceExtractor — порт из sklad: 'claude -p --output-format
  json', промпт один-в-один (копировать все строки, null если не читается,
  barcode != supplier_code, сверка с «Итого»). Поддерживает local и
  Invoices:ClaudeCli:RemoteHost (ssh+scp+cleanup).
- InvoiceMatcher — barcode → alias → none. По названию НЕ ищем
  (sklad-урок: 2-5 ошибок из 10, подмена хуже пустой строки).
- InvoiceProcessor + InvoiceWorker (BackgroundService):
  pending → parsing → needs_review | failed. SweepAsync раз в час
  удаляет файлы старше Invoices:KeepFilesDays (default 7); строки
  остаются. Multi-tenant через IgnoreQueryFilters + явный OrgId.

Controller api/purchases/invoices:
- POST /upload (SuppliesEdit, 60 MB max): файлы в uploads/invoices/{yyyy-
  MM}/, запись в pending.
- GET /, GET /{id} — список + детали со строками.
- GET /{id}/file — отдать файл.
- DELETE /{id} — удалить запись + файл.
- PUT /{id}/lines — правки шапки + строк + авто-upsert InvoiceAlias
  для каждой строки с MatchedBy=manual|alias (два ключа: по code и по
  name_norm).
- POST /{id}/apply — создать Supply (Draft) с данным поставщиком/
  складом/валютой, статус → applied, SupplyId ссылается, файл удаляется.
- GET /product-by-barcode, /counterparty-search — помощь UI.

Build зелёный. Web + тесты — следующими шагами.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-04 21:30:02 +05:00
nns 4342318c2c feat(s29): Organization.IsAdvancedMode + миграция Phase29a (шаг 1/8)
Simple/Advanced mode: по умолчанию у всех организаций «простой» режим
(IsAdvancedMode=false) — виден только базовый розничный учёт. Включить
«расширенный» режим может только SuperAdmin через отдельный API; в
настройках магазина переключателя нет.

Данные скрытых модулей при переключении НЕ удаляются; при повторном
включении всё снова видно.

- Organization.IsAdvancedMode (bool, default false) + XML-док
- EF-миграция Phase29a — идемпотентный ADD COLUMN IF NOT EXISTS DEFAULT false
- AppDbContextModelSnapshot обновлён
- Применено на dev-БД: 5/5 организаций → IsAdvancedMode=false

docs/simple-mode-progress.md — чек-лист + журнал.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-04 11:28:28 +05:00
nns 1af4290313 fix(s22): 1C-CSV detect charset из Content-Type + UpdatedAt в migration
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
- Parse1cCsv ловил Windows-1251 на UTF-8 bodies без BOM. Теперь
  смотрит Content-Type charset первым, потом BOM, потом WIN-1251.
- Migration Phase22a_OrgExports забыл UpdatedAt колонку (Entity base
  имеет её). ADD COLUMN IF NOT EXISTS внутри миграции для уже
  созданных таблиц.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 23:09:09 +05:00
nns aa83f82dc5 feat(s22): data tooling — export/import + schema docs + anon dump (7 пунктов)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
1. GDPR org export — domain OrgExport + Phase22a миграция, OrgExportJob
   собирает ZIP с JSON по каждой сущности через IObjectStorage,
   DownloadToken 64-hex + 24h TTL + email-notify.
   POST /api/org/export, GET /api/org/export[/{id}], GET download/{token}.

2. 1C CSV import — POST /api/catalog/products/import/1c-csv:
   Windows-1251/UTF-8 BOM auto-detect, разделитель ;/, русские заголовки
   (Артикул/Наименование/Единица/Цена/Группа/Штрихкод) или английские.
   Нормализация unit-кодов (шт/кг/г/л/мл/упак). Делегирует на ImportCsv
   (транзакция, multi-tenant). docs/imports.md.

3. deploy/anonymize-prod.sh — pg_dump прода → restore во временную БД →
   UPDATE PII (email→user{N}@example.kz, phone→+7700111{N:04}, password→
   тестовый hash, BIN/IIN синтетические, MoySkladToken=NULL, аудиты
   TRUNCATE) → pg_dump → gz файл.

4. DbSchemaDocsJob (weekly вс 05:00 UTC) — information_schema → md с
   таблицами + колонками + FK + mermaid ER-диаграммой (топ-20 таблиц).
   Сохраняет в content-root db-schema-generated.md.

5. POST /api/admin/audit-log/export?format=csv|jsonl — streaming через
   AsAsyncEnumerable. UTF-8 BOM для CSV, JSONL для grep'a. Multi-tenant.

6. GET /api/moysklad/sync-status — агрегат по import_jobs:
   { configured, lastSuccessAt, errorCountLast7Days, pendingCount,
     byKind: { products: KindStatus, counterparties: KindStatus } }.
   Stub если MoySkladToken=null.

7. docs/ARCHITECTURE.md — финальный итог 22 спринтов:
   - Sprint 13-22 changes-сводка
   - «Реализовано полностью» секция
   - «Scaffolding» таблица с указанием что нужно от user'а
   - «Не реализовано» секция (прод, SSO callback, KZ-перевод, POS-тест)
   - Актуальная файловая структура

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 23:00:54 +05:00
nns 6940aa40df feat(s19): bulk-операции + presets + power-user UX (7 пунктов)
Some checks failed
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
1. Bulk-обновление товаров — Product.IsArchived + IsAvailableForSale
   (Phase19a миграция), POST /api/catalog/products/bulk-update {ids, op, params}
   с операциями price-adjust (% / абсолют), change-group, archive/unarchive,
   toggle-sale. Одной транзакцией, multi-tenant через query-filter.
   Frontend: checkbox-колонка, sticky bulk-bar, модалка.

2. SavedPresets — domain UserPreset (Phase19b: jsonb ConfigJson,
   unique по OrgId+UserId+PageKey+Name). /api/user/presets CRUD per-user.
   <SavedPresets> компонент с chip-bar и сохранением. Применено к /reports/
   sales/stock/profit + /catalog/products.

3. QuickActionsPalette — Cmd+J открывает отдельную палитру с 14 действиями
   + история topa-10 в localStorage.fm.quickActions.recent. ↑↓/Enter/Esc
   keyboard nav. Cmd+K (поиск) и Cmd+J (действия) — разные палитры.

4. Inline-edit цены — PATCH /api/catalog/products/{id}/price новый endpoint
   с RoundIfNeeded. <InlinePriceCell> с dblclick → input, optimistic update
   + revert при ошибке.

5. CSV import товаров — POST /api/catalog/products/import-csv (rows[]).
   Клиент парсит CSV (auto-detect разделитель ,/;), сервер commit'ит
   транзакцией. autoCreateGroup для новых групп. <ProductsCsvImport>
   модалка с preview и подсветкой ошибочных строк.

6. CSV/XLSX export — endpoint'ы /export на 5 контроллерах (products,
   counterparties, stock, retail-sales, supplies). Reuse существующего
   ReportExport.Csv/Xlsx. <ExportButton> dropdown с двумя форматами.

7. Keyboard-first nav в DataTable — ↑↓/Home/End/Enter/Space/Delete props
   keyboardNav/onSelect/onDelete. Подсветка focused-строки. Документация
   в src/help/keyboard-shortcuts.md + 2 новые HelpTopic'a.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 21:08:48 +05:00
nns e13dd6937f perf(s14): индексы + N+1 fix + bundle -50% + WebP variants + pool + Hangfire timing
Sprint 14 — производительность с реальными замерами до/после.

Ключевые цифры:
- Sales-report SQL: 9.53ms → 7.09ms mean (-25%) после N+1 fix +
  индексов.
- Initial JS bundle: 1456 KB → 706 KB raw (-51%); gzip 389 KB →
  196 KB (-50%) через React.lazy на 30 редких страниц + Recharts.
- Lighthouse /login: Perf 89, A11y 92, BP 100 (target ≥85/90/90 ✓).

Подробности по каждому пункту + методология замеров — в
docs/sprint14-progress.md.

Что сделано:
1. Phase14a_PerfIndexes — composite (Org,Status,Date), partial
   (WHERE Status=1 AND NOT IsReturn) + INCLUDE, и composite
   stock_movements(Org,OccurredAt).
2. SalesReportController.FetchAsync — раньше каждая строка
   результата делала CASE WHEN ELSE (SELECT ... LIMIT 1)
   correlated subquery на RetailPoint.Name и User.FullName.
   Заменено на 2 IN-batch'a + dictionary lookup в C#.
3. App.tsx React.lazy для отчётов, audit-log, loyalty, super-admin,
   settings, all rare edit pages. Recharts вынесен в lazy chunk
   Dashboard'а (KPI рендерятся сразу).
4. SixLabors.ImageSharp v3.1.6 + ImageVariantService — генерирует
   thumb 256/medium 800 WebP@80 при загрузке. UploadsController
   ?size=thumb|medium с fallback. React <ProductImage> — <picture>
   + srcset.
5. ApplyDefaultPoolConfig на старте: Max=100, Min=10 (грей пул),
   Idle=300, Max Auto Prepare=20.
6. Lighthouse на /login /forgot-password /reset-password —
   все три проходят пороги.
7. JobTimingFilter + HangfireGlobalFilterRegistrar — каждый
   recurring job логирует длительность; >30s = Warning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 13:21:39 +05:00
nns 0d3ef81f72 feat(s11): ОФД-scaffolding — IFiscalProvider + 4 провайдера + UI/тесты
Sprint 11 — каркас для интеграции с операторами фискальных данных РК.
Реальные ApiKey'и появятся у user'а позже; задача — построить такой
фрейм, чтобы подключение оператора сводилось к вписыванию кредов в UI
без правок кода/деплоя.

Что сделано:
- IFiscalProvider (Application/Common/Fiscal) + FiscalResult,
  FiscalProviderKind (None/Mock/Webkassa/Kassa24/OfdSolo),
  IFiscalProviderFactory, FiscalNotConfiguredException.
- 4 реализации в Infrastructure/Fiscal:
  • MockFiscalProvider — фейк MOCK-<8hex> через 300мс, идемпотентный
    по Sale.Id (используется dev/stage и интеграционными тестами);
  • WebkassaProvider — полный HTTP-pipeline Authorize→Check, парсинг
    JSON-ответа, NDS-в-ставке, retry-safe через ExternalCheckNumber;
  • Kassa24Provider / OfdSoloProvider — скелет с тем же контрактом,
    RegisterAsync бросает FiscalNotConfiguredException (нужны
    спецификации API от user'а, NDA-only).
- Миграция Phase11a: 5 колонок в retail_sales (FiscalNumber, QrCode,
  Url, ProviderTxId, ProviderKind) + 5 в organizations (FiscalProvider
  NOT NULL default 0, ApiKey/Secret encrypted, CashboxUniqueNumber,
  ApiBaseUrl). Default 0 = обратная совместимость, существующие чеки
  и продажи без фискализации работают как раньше.
- RetailSalesController.Post — TryFiscalizeAsync после commit'а
  stock-транзакции. Best-effort: сетевые/HTTP-ошибки логируются, чек
  остаётся проведённым. Идемпотентность по IsNullOrEmpty(FiscalNumber).
- OrgFiscalSettingsController: GET/PUT настройки + GET /providers
  (опции для select'а) + POST /test-send (фейк-чек к выбранному
  провайдеру, не сохраняет в БД).
- UI: FiscalSection в OrganizationSettingsPage с password-input'ами
  для ApiKey/Secret (шифруются DataProtection.purpose=foodmarket.fiscal,
  в GET — только has-* флаги), спец-значение "__clear__" для снятия,
  кнопка «Тестовая отправка».
- Тесты: 11 unit (Mock 5 + Webkassa payload 6) + 3 integration
  (Mock сохраняет FiscalNumber, test-send даёт MOCK-номер, None
  не фискализует).
- docs/ofd-integration.md — гид с архитектурой, шагами подключения
  Webkassa (полный pap), TODO для Касса24/ОФД-Соло, безопасностью
  кредов, retry-сценариями.

Все 68 unit + 8 integration в Fiscal/Loyalty/RetailOversell — зелёные.
Web vite build — зелёный.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:27:17 +05:00
nns 91128a7ed0 feat(loyalty+promotions): P2-12 + P2-13 — лояльность и промокоды (Sprint 9 п.1-2)
Some checks failed
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
Domain:
- LoyaltyProgram { Type=Percentage|FixedAmount|PointsAccrual, Rate,
  MinSubtotal, IsActive } — org-scoped.
- LoyaltyCard { ProgramId, CounterpartyId, CardNumber unique per org,
  Balance, IsBlocked }.
- Promotion { Type=Percent|FixedDiscount, Value, Scope=All|ProductGroups|
  Products, Code unique per org, period, ProductGroupIds/ProductIds (jsonb) }.
- RetailSale: LoyaltyCardId, LoyaltyBonusApplied, LoyaltyPointsAccrued,
  PromotionId, PromotionCode (snapshot), PromotionDiscount.

EF:
- SalesConfigurations: indexes, FK Restrict, jsonb-converters для Guid-
  списков Promotion (ValueComparer для change-tracker).
- Phase9b миграция: 3 таблицы + 6 колонок на retail_sales.
- RolePermissions: LoyaltyManage, PromotionsManage добавлены (попадают
  в All() для Admin).

API:
- /api/loyalty/programs CRUD (Get/List/Create/Update/Delete; запрет delete
  при существующих картах → 409).
- /api/loyalty/cards CRUD + /issue + /{id}/block + /{id}/unblock + /lookup
  (POS использует при оплате — 404 если нет, 409 если blocked/inactive).
- /api/promotions CRUD; код уникален per org (БД-индекс + 23505 → 409).
- RetailSale.Create/Update: новые поля input.LoyaltyCardNumber +
  input.PromotionCode. Метод ApplyLoyaltyAndPromotionAsync:
  • Lookup карты, проверка active/blocked/MinSubtotal.
  • Расчёт скидки или баллов в зависимости от Type.
  • Lookup промокода, проверка периода/MinSaleAmount/scope.
  • MatchingSubtotal для Scope=ProductGroups/Products считаем по
    input.Lines (sale.Lines ещё пустой в этот момент).
  • Финальный Total = Subtotal - DiscountTotal - LoyaltyBonusApplied
    - PromotionDiscount, max(0).
- RetailSale.Post: начисление баллов на LoyaltyCard.Balance (внутри
  транзакции, чтобы rollback не оставил orphan баллы).

UI:
- /loyalty/programs — list + create/edit modal с Type/Rate/MinSubtotal.
- /loyalty/cards — list + issue modal (Program select + AsyncSelect
  counterparty + CardNumber).
- /promotions — list + create/edit modal (Type/Value/период/MinSaleAmount/Code).
- Sidebar: новый блок «Продажи» с пунктами Промокоды/Программы/Карты
  (Admin-only).
- i18n: ru.json + en.json пополнены nav-ключами.

Тесты:
- LoyaltyFlowTests (3/3 ✓): percentage уменьшает Total на 10%, points-accrual
  пополняет Balance после Post, multi-tenant lookup→404 чужой org.
- PromotionFlowTests (2/2 ✓): SALE20 уменьшает Total на 20%, невалидный
  код→400 с понятной message и field=promotionCode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 21:06:10 +05:00
nns 3088237ea7 feat(telegram): OwnerDailySummaryJob + bot binding (P2-14)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Backend:
- Organization.OwnerTelegramChatId (long?) — миграция Phase9a.
- TelegramOptions / TelegramBotClient (Telegram Bot API sendMessage).
  Disabled-mode когда токен пустой (Dev/CI). HTML parse_mode.
- OwnerDailySummaryJob.RunAsync — пробегает по org с привязанным chatId,
  рендерит сводку (выручка вчера, чеков, средний чек, топ-3 по выручке,
  low-stock 5 строк) и шлёт. Best-effort на каждой org.
  RenderSummaryAsync — publicный для тестов.
- HangfireJobsConfigurator: cron "0 6 * * *" UTC = 09:00 МСК.
- TelegramBindingController: GET /status (botEnabled, username, chatId,
  deepLink), PUT /bind (тестовое сообщение → проверка chatId → save),
  DELETE (unbind).

Конфиг:
- Telegram:BotToken — env Telegram__BotToken.
- Telegram:BotUsername — для deep-link.

UI:
- OrganizationSettings.TelegramSection: показывает статус (bot enabled?
  bound?), deep-link к боту, пошаговая инструкция (start → userinfobot →
  ввести chat_id → проверка). Toast на привязку/отвязку через
  meta.successMessage.

Тесты:
- TelegramOwnerSummaryTests: рендер содержит org_name, метрики, HTML. ✓ 1/1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 19:50:33 +05:00
nns d54e1cb968 fix(catalog): EF8 nav-collection bug в Products.Update + unique IX на Article
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
1. Products.Update: добавление нового barcode'а к существующему товару
   валилось с DbUpdateConcurrencyException 'Товар изменён в другом окне',
   хотя никакой конкурентной правки не было. Тот же EF8-баг, который в
   TD-6 чинили на Supplies/Demands/RetailSales: nav-collection.Add +
   client-side Id путает EF, UPDATE родителя получает 0 affected. Чиним
   тем же паттерном: ExecuteDelete старых ProductBarcodes/ProductPrices,
   DbSet.Add новых. Воспроизводится: создать товар с 1 barcode, PUT с
   2 barcodes → 409. После фикса → 204.

2. IX_products_OrganizationId_Article был обычным (не уникальным), хотя
   контроллер ловил нарушение по имени индекса и возвращал 'Артикул уже
   занят'. Catch-блок никогда не срабатывал. Делаем индекс уникальным
   миграцией Phase8d. Перед созданием — нумеруем дубликаты по существующим
   данным (если есть). NULL/пустые article остаются distinct (Postgres
   NULL semantics).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 16:46:10 +05:00
nns ec0cff7fc4 feat(concurrency): RowVersion на документах через Postgres xmin (TD-6)
Optimistic concurrency через системную колонку Postgres xmin — никакой
дополнительной колонки и миграции не нужно, xmin есть у каждой таблицы
и автоматически обновляется при UPDATE.

Конфигурация:
- IVersionedEntity (маркер) + uint Xmin на Supply, Demand, RetailSale,
  Transfer, InventoryDoc.
- e.UseXminAsConcurrencyToken() в EF-конфиге для каждой — создаёт shadow
  property "xmin" с IsConcurrencyToken + ValueGeneratedOnAddOrUpdate.
- e.Ignore(x => x.Xmin): .NET-property живёт только для транспорта в DTO,
  не маппится в БД (xmin тащим shadow'ом).
- GetInternal в SuppliesController читает xmin через
  EF.Property<uint>(s, "xmin") в LINQ-проекции и складывает в DTO.

Wire-up:
- SuppliesController.Update принимает input.Xmin (uint?), сверяет с
  shadow xmin загруженного supply через EF.Entry().Property("xmin").
  Несовпадение → 409 с code=concurrency_conflict. null/0 от клиента →
  legacy compat, проверки нет.
- SaveOrFkErrorAsync ловит DbUpdateConcurrencyException → 409 (двойная
  защита: и явная сверка, и EF auto-check в SaveChanges).

Bonus: Supply.Update перешёл на тот же паттерн что Demand/RetailSale —
ExecuteDelete старых строк + AddRange новых напрямую в DbSet. Старый
RemoveRange-then-Add через nav-collection ломал EF concurrency check
(UPDATE supply_lines одной из старых строк падал 0 affected внутри той
же SaveChanges-транзакции).

Тесты: 2 интеграционных:
- two parallel updates with same xmin → один 204, другой 409; retry
  с новым xmin тоже 204.
- legacy clients без xmin → PUT работает без concurrency-проверки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:33:01 +05:00
nns b963adfa2e feat(import-jobs): persisted ImportJobRegistry в БД (TD-5)
Раньше прогресс фоновых импортов жил в ConcurrentDictionary внутри
Singleton-сервиса: рестарт процесса терял всю историю, активные
джобы навсегда оставались в статусе Running.

Теперь:
- Domain.Integrations.ImportJob (TenantEntity) — таблица import_jobs,
  миграция Phase8c_ImportJobs (jsonb для ErrorsJson, индексы по
  OrgId+StartedAt / OrgId+Status / FinishedAt).
- ImportJobRegistry рефакторен: Create() пишет строку немедленно,
  SaveAsync() обновляет, Get/RecentlyFinished читают из БД. API
  совместимое со старой in-memory версией — MoySkladImportService
  и контроллеры не меняются.
- MoySkladImportController.RunInBackgroundAsync теперь:
  * Periodic flush через Timer каждые 2 секунды — UI видит
    реальный progress (Stage/Created/Total), а не Create-snapshot;
  * Финальный flush в finally — обязательный для terminal state.
- AdminCleanupController.WipeAllAsync — то же финальное сохранение.
- SkipAudit=true для import-job записей — служебные, в OrgAuditLog
  не пишем.

Tenant-isolation: query-filter работает прозрачно, B не видит джоб A.

Тесты: 3 интеграционных (survives across scope, RecentlyFinished
читает из БД, tenant-isolation).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 16:45:08 +05:00