РЕЕСТР ТОВАРОВ (глобальный системный шаблон карточек):
- Домен 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>
Автопроверка 52/52: scrollWidth≤clientWidth на всех плитках тарифа × 5 ширин.
Скриншоты k5-*.png, скрипт site-fixes-verify-5.mjs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Автопроверка 44/44 на ширинах 320/375/390/402 × главная/pricing/signup —
скриншоты k4-*.png и скрипт site-fixes-verify-4.mjs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проверка на test.food-market.kz: касса подаётся одинаково честно (скачивание
«скоро») на главной/about как на /pos; «₸» не переносится в тарифах (1280 и 390).
Автопроверка 5/5. Реквизиты не трогал.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проверка задачи 11: интеграционный тест (эндпоинт), SQL по реальному датасету
МойСклад (org FOOD MARKET: в наличии 4746 / нулевые 23418 / минус 2829 / всего
30993; группа «Кондитерские…» зеро+минус 3848), живой stage-API на сид-данных и
скриншоты реальной админки (три галочки, URL states=…). StockReportPage не
менялся. Прод не трогали. Нужен рабочий пароль SuperAdmin для скриншота именно
org FOOD MARKET.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Поручение владельца (задача 11): галочки состояния остатка объединяются (ИЛИ),
группа/склад/поиск сужают (И). Пусто → positive+negative (нулевые скрыты).
- API StockController: параметр states (positive|zero|negative; через запятую или
повтором) на /api/inventory/stock и /stock/export. Один WHERE:
(positive И qty>0) ИЛИ (zero И qty=0) ИЛИ (negative И qty<0) — по остатку
(COALESCE подзапрос; товар без строки Stock = 0). total/пагинация/сортировка
корректны. Старые includeZero/negativeOnly оставлены для совместимости
(includeZero ≡ все три; negativeOnly ≡ negative); states приоритетнее.
- Веб-админка StockPage: три галочки «В наличии» (>0), «Нулевые» (=0), «В минусе»
(<0) вместо прежних двух; состояние в URL (states=…), экспорт учитывает галочки.
Ссылки NegativeStockBanner/OverdueNegativeStockBlock/NotificationCenter на
минусовые → states=negative.
- Интеграционный тест: сочетания (zero; negative; positive; zero+negative;
+группа; все три = весь каталог; пусто = positive+negative; со складом) и
числовые тождества (positive+negative=пусто; positive+zero+negative=весь
каталог) + совместимость старых флагов. 1/1 зелёный (EF8 переводит предикат).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
На ноутбуке магазина автопоиск спотыкался о Bluetooth-порты (COM7/COM8 — Open/Write
висят), выбирал по умолчанию BT и не доходил до COM22 (USB-SERIAL CH340, где весы).
Парсер/протокол исправны (ручной выбор COM22 57600 работает) — баг только в поиске.
- ScaleAutodiscovery.DiscoverAsync: тирами по типу порта — USB-serial первыми,
неопознанные в середине, Bluetooth последними; на каждый порт ЖЁСТКИЙ таймаут
(Open обёрнут в задачу с гонкой против Task.Delay — SerialPort.Open на BT виснет);
порты перебираются параллельно (до 4), общий бюджет ~15 с; любая ошибка/таймаут →
следующий порт; по каждому порту строка в лог («COM22 CH340: ENQ→ACK, вес 0.000 кг»,
«COM7 Bluetooth: пропущен», «COM15: нет ответа»).
- HardwareProbe: Classify (CH340/Prolific/FTDI/CP210x → USB-serial; BTHENUM/Bluetooth
→ Bluetooth; ttyUSB/ttyACM → USB-serial), ClassifyPorts (без открытия портов),
OpenCloseBounded (жёсткий таймаут — листинг не виснет на BT). ListComPorts не
открывает Bluetooth-порты.
- HwCheck: по умолчанию выбирается НЕ Bluetooth-порт; окно показывает журнал
автопоиска по портам; список портов помечает [USB-serial]/[Bluetooth].
- Основная касса (экран «Оборудование») — та же DiscoverScaleAsync; --probe-hw не
открывает BT-порты.
- Из zip hardware-check убран ярлык «(с тестовой печатью).bat» (печать — отдельно
по слову владельца).
- Тесты: зависающий порт (Open 30 с) не задерживает поиск > таймаута, живой порт
вторым — найден; USB-serial проверяется раньше Bluetooth (BT не трогаем). 53/53.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Приоритет владельца: sysadmin проверяет весы/принтер/сканер на ноутбуке магазина
без входа в кассу. Отдельный exe (food-market.pos.hwcheck), ссылается ТОЛЬКО на
pos.core ради Hardware/* — не умеет быть кассой (нет логина, сервера, БД, токенов,
сетевых запросов).
- pos.core/Hardware/HardwareProbe.cs: детект COM-портов (занят/свободен, VID/PID,
описание) и принтеров Windows (имя/порт/в сети/число заданий) — только чтение,
очередь не трогаем. WMI под OperatingSystem.IsWindows(); на Linux — базовый COM.
- pos.core/Hardware/BarcodeScan.cs: контрольная цифра EAN-13/EAN-8 и оценка
«сканер vs ручной набор» по среднему интервалу между символами (чистая логика).
- food-market.pos.hwcheck: окно на Avalonia (код, без XAML), светлая тема, русский.
Весы (Найти/Прочитать/Запустить-Остановить, live-вес ~350 мс, сырой hex, ручной
выбор порта/скорости/протокола), COM-порты с пометкой «занят», принтеры (только
чтение), сканер (EAN + тип ввода), «Сохранить/Копировать отчёт». Тестовая печать
ВЫКЛ по умолчанию, включается только --allow-test-print.
- tests: EAN контрольная цифра (13/8, валид/битый) + классификация ввода. 50/50.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
По фактическому замеру с ноутбука магазина (11.10): весы Масса-К (USB-COM CH340)
отвечают только на 57600 8N1; рукопожатие ENQ(0x05)→ACK(0x06); запрос DC1(0x11) →
кадр SOH STX «флаг вес единица» CTRL ETX EOT.
- pos.core/Hardware/SerialScale.cs: ISerialTransport (абстракция для тестов) +
SystemSerialTransport (8N1, DTR/RTS), MassaK.ParseFrame/Handshake/ReadFrame/
ReadWeight. Control-байт НЕ валидируем (не подтверждён) — только сырой hex в лог.
Единицы kg по умолчанию, «г/g» → делим на 1000.
- pos.core/Hardware/ScaleDiscovery.cs: SerialScaleReader (+ стабильность по двум
одинаковым чтениям) и ScaleAutodiscovery — 57600 и Масса-К-кадр ПЕРВЫМИ, общий
бюджет, отдельно отмечаем занятые порты («порт занят», а не «весы не найдены»).
- pos: ComScale — тонкий адаптер к pos.core; AppServices хранит scale_proto и по
умолчанию MassaKFrame/57600; ProbeRunner (--probe-hw) печатает сырой hex кадра.
- tests: разбор реального нулевого кадра 01 02 53 20 20 30 2E 30 30 30 6B 67 71 03 04
и вариантов (1.235 / 12.500 / отрицательный / граммы / мусор до SOH / обрезанный /
без ACK) + рукопожатие на фейковом порту. 14/14 зелёных.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проверка на живом test.food-market.kz после деплоя: React #418 ушёл, дефолтный
«Бизнес» 10 000, чужих систем учёта 0 (Kaspi/маркетплейсы только «в разработке»),
типографика KB, sitemap со слэшами, мобильные правки (Android «Назад», ползунки
−/+, состав панели), честные /status и /kb/billing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs/site-fixes-2026-10-10/{index.html,REPORT.md,screenshots}; tools/site-fixes-verify.mjs
(Playwright-проверка: signup plan/конфигурация/цена, мобильное меню, без промисов/REPLACE_ME).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Доработаны хендлеры браузерного прогона (пагинация со сбросом поиска, обобщённое
создание/сохранение/удаление, период-фильтры, доп. сид: чек/приёмка/оприходование/
списание/инвентаризация/возврат). Экстрактор ловит строковый фолбэк заголовка колонки.
docs/uat-coverage.html — цифры по экрану/приоритету/общим. Цели (P1–P3 ≥95%, P4 ≥70%)
пока НЕ достигнуты (переход на ALL MARKET в приоритете) — остаток задокументирован в
docs/uat-coverage-progress.md, продолжим после перехода.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Найдено 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>
- tools/extract-controls.ts: статически из web/src/pages достаёт все интерактивные
элементы каждого экрана (поиск/фильтры/сортировки/пагинация/экспорт/тумблеры/
действия) → docs/uat-coverage.expected.json (36 экранов, 195 элементов; приоритеты).
- scenarios/stage-ui-uat-coverage.spec.ts: читает expected, в браузере задействует
каждый элемент (фиксирует запрос с параметром/действие), пишет reports/uat-covered.json.
- tools/build-coverage.ts: сводит → docs/uat-coverage.{json,html} с % по экрану/
приоритету/общим (цель P1–P3 ≥95%, P4 ≥70%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Часть 3 + автопоиск (данные sysadmin: касса МойСклад спамит «Порт не выбран»):
- Весовой товар при сканировании/плитке → вес с весов (если на связи), иначе
модалка ручного ввода веса. Сумма = цена/кг × вес; подпись «1,235 кг × 690 ₸/кг».
- ScaleMonitor (pos.core): опрос ТОЛЬКО по требованию (при весовом товаре), мягкая
деградация на ручной ввод, запись о потере/восстановлении связи РОВНО ОДИН раз
(не сотни «Таймаут»). Юнит-тесты (100 опросов → 1 запись).
- ScaleDiscovery (pos): автоскан всех COM-портов (вкл. USB-COM CH340/Prolific) ×
протоколы Масса-К/CAS × скорости; кнопка «Найти весы» в верхней панели; найденный
порт запоминается (kv) и восстанавливается при старте.
- UnitShort («шт»/«кг»/«л») протянут через sync→SQLite→PosProduct/CartLine; цена
весового показывается «690 ₸/кг» вместо кода ОКЕИ «690 ₸/796».
- Разбор этикеток в кассе — по настройкам орг (по умолчанию ВЫКЛ, точный штрихкод).
- Юнит-тесты 321/321 (обновлены PosBarcodeTests под дефолт-off).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Реальный сценарий владельца: весов с печатью этикеток нет, штрихкоды весовых —
обычные коды «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>
stage-ui-uat-mobile проходит по 16 спискам P1-P4 на 390px (каталог/контрагенты/
склад/приёмки/продажи/деньги/отчёты/сотрудники): без h-scroll, console-ошибок и 5xx.
Отчёт дополнен: UI-гейтинг по ролям (menu-walk 3/3), окно кассы Windows 11.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
После перехода остатков на агрегат «Все склады» при отсутствии storeId, stockOf
без storeId получал агрегатную строку (storeId=Guid.Empty) и не находил склад →
остатки читались как 0, что роняло приёмки/склад/инварианты. Теперь запрос с
storeId (+includeZero). full-chain снова 16/16 зелёный.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ValidationFilter (FluentValidation-путь): русский заголовок «Проверьте
правильность заполнения» через ModelErrorTranslator вместо дефолтного англ.
«One or more validation errors occurred» (сообщения полей уже были русскими).
- stage-ui-uat-mobile.spec.ts: P1-экраны на 390px без h-scroll/console/5xx +
проверка «Остатки» (артикул/штрихкод, тумблер «показать нулевые»).
- docs/uat-report-2026-10-09.md: матрица покрытия (уже-зелёное vs проверено
сейчас) по требованию владельца; галерея скринов reports/uat-mobile/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Независимо считает ожидаемый набор строк/итог (не «200 OK»): свежая temp-орг,
контролируемый сид (внук-со-стоком/ребёнок-ноль/родитель-минус/поиск/услуга в
3-уровневой группе), сверка для каждого контрола. Покрыто: stock includeZero,
negativeOnly, groupId/productGroupId (3 уровня вложенности), store-specific,
поиск по артикулу/штрихкоду, сортировка по количеству, пагинация (разбиение),
экспорт-с-фильтром; catalog groupId/productGroupId/isService. 21/21 ✓ на стенде.
Добавлено в nightly (step 3c) и quality-watchdog (step 12) на хосте.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RetailOversellingTests ждал 409 на продажу сверх остатка — устаревшая политика.
По решению владельца 08.10 продажа в минус разрешена и создаёт предупреждение об
отрицательном остатке. Тест переписан: проведение успешно, остаток уходит в минус,
создан negative-stock alert. Плюс обновлён docs/filters-sweep-progress.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
«Последние продажи»/Z-отчёт показывали времена вперёд, потому что дата чека
бралась от клиента (input.Date / POS OccurredAt) без нормализации: наивная
локальная дата или спешащие часы кассы давали будущий инстант. Добавил
BusinessTime.ToInstantUtc: Kind→UTC и clamp будущего (>5 мин) к now — чек не
может быть в будущем. Применено в рознице (create) и в приёме чеков с кассы
(движение берёт тот же инстант). Юнит-тесты на нормализацию/clamp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Интерфейс IServerApi получил QuickCreateProductAsync, но тестовый FakeServer его
не реализовывал → проект food-market.UnitTests не компилировался (все юнит-тесты
не запускались). Добавил стаб как у остальных методов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CauseUserId в уведомлениях о минусах был null: StockService писал автора только
если вызывающий явно передал CreatedByUserId в черновике, а большинство
документов (касса/списание/перемещение/приёмка/…) его не передавали. Теперь
автор берётся из черновика, иначе — из текущего пользователя (ITenantContext.UserId)
в единой точке ApplyMovementAsync, поэтому пишется и в StockMovement, и в алерт
для всех типов документов. Тест: минус от списания → causeUserId = текущий юзер.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Неверный тип/enum в теле запроса давал пользователю технический англ. текст с
именем класса и путём ($.reason: The JSON value could not be converted to
foodmarket.Api…LossInput…). Теперь InvalidModelStateResponseFactory прогоняет
ModelState через ModelErrorTranslator: технические сообщения → «Неверное значение
поля «Причина»» (имя поля по-русски, без классов/путей/англ.), осмысленные
FluentValidation-сообщения остаются как есть. Сырой текст пишется только в лог
бэкенда. Действует на web (toast) и mobile API. Тест: списание с неверным enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Остатки строились ОТ таблицы Stocks (JOIN Products), поэтому товары без строки
Stock (ни разу не приходованные) не попадали в список вообще, а includeZero лишь
снимал фильтр Quantity!=0 у существующих строк. То же в отчёте «Остатки на дату»,
в экспорте и в фильтре по группе (iOS получал total 0).
- GetStock/ExportStock: выборка ОТ Products (не-услуги, не-архивные) с остатком
коррелированным подзапросом (товар без Stock = 0). Склад выбран → строка склада;
без склада → агрегат «Все склады». Нулевые видны при includeZero; negativeOnly и
пагинация/сортировка/поиск/экспорт работают и для нулевых. Остаток/ед./штрихкод
дотягиваются batch-запросом по id страницы (EF8 не переводит подзапросы в SELECT+ORDER BY).
- Отчёт «Остатки на дату»: includeZero добавляет товары без движений (Qty 0);
фильтр по группе теперь с вложенностью.
- groupId/productGroupId — синонимы с вложенными группами на всех списках остатков
и в каталоге (List + Export). Раньше catalog?productGroupId молча игнорировался.
- Штрихкод рядом с артикулом в списке/экспорте — различать одноимённые товары
(4× «BIOBLAS шампунь» с разными артикулами из МойСклада — не удалять).
- Demo (30 894 товара): без флага 12, includeZero → 30 894, <500мс/страница.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
П.2 (POS): закрытие смены печатает Z-отчёт с разбивкой по товарам (продано/возврат)
через ESC/POS; кнопка «Печать Z-отчёта (по товарам)». ShiftReportDto += byProduct/overLimit24h.
П.1 (POS): первая продажа без открытой смены (онлайн) — диалог «Открыть смену. Размен?
(можно пропустить)» → автооткрытие и переход к оплате; офлайн — как было.
П.10: full-chain обновлён — продажа без смены теперь автооткрытие (не 409) + проверка
byProduct в Z-отчёте; прогон на стенде 16 ✓ / 0 ✗.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
stage-ui-parity.spec.ts (3 теста, стенд): блок «Прибыль за сегодня/месяц»; в карточке
нового товара штрихкод автозаполнен + кнопка «Сгенерировать» меняет его; товар только
с именем получает сгенерированный EAN-13.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POS: чек с точки без открытой смены — сервер автооткрывает смену (Cash-счёт точки,
размен 0) и принимает чек (как в вебе). Фоновый ShiftWatchJob (раз в час) уведомляет
кассира и админов о сменах, открытых дольше 24ч (персистентное уведомление + SignalR,
дедуп раз в сутки). Тест POS-автооткрытия.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /api/documents/{type}/{id}/related — основание + связанные (возвраты/оплаты/
опр-спис/возвраты по чеку) для supply/supplier-return/retail-sale/inventory/enter/loss.
Компонент RelatedDocsBlock применён на edit-страницах приёмки, возврата поставщику,
оприходования, списания, чека (инвентаризация имела свой блок). Integration-тест:
приёмка↔возврат (related/basedOn).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /api/money/reports/supplier-reconciliation?supplierId=&from=&to=: начальное
сальдо (до from), движения периода (приёмка +долг, возврат/оплата −долг) с бегущим
сальдо, конечное сальдо. Веб-страница /money/supplier-debts/:id/reconciliation —
печатная форма акта (window.print()/PDF) + кнопка «Акт сверки» в долгах поставщикам.
Integration-тест: приёмка как debit, конечное сальдо.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/purchases/supplier-returns/from-supply/{supplyId}: черновик возврата с
преднабором строк из проведённой приёмки (товар/кол-во/цена), поставщик/склад/валюта
и ссылка ReferenceSupplyId. Integration-тест: преднабор + связь.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
П.1: продажа на точке без открытой смены — смена автооткрывается (подбор Cash-счёта
точки/орг., размен 0, можно внести позже), как МойСклад; ручное открытие остаётся.
Длительность смены и отметка overLimit24h в /current и Z-отчёте (мягкое правило:
предупреждение + отметка, не жёсткий лимит).
П.2: Z-отчёт /shifts/{id}/report теперь с разбивкой byProduct (продано кол-во/сумма,
возвращено кол-во/сумма).
Тест переписан: продажа без смены → автооткрытие + проводится; /current open=true;
Z-отчёт содержит byProduct с верными кол-вом/суммой.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
П.3 паритета: create-return уже ставит способ исходного чека. В ручном возврате
(IsReturn + referenceSaleId) если способ отличается от оплаты чека — нужно право
администратора и причина (RefundReason, пишется в Notes). Сообщения понятные.
Тест: тот же способ ок; иной без причины → 400; иной с причиной (админ) → 201.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Правки владельца 08.10 (п.4 паритета):
- Сохранить можно при наличии Наименования. Единица не обязательна в запросе →
дефолт «шт»; группа → корень. Штрихкод ОБЯЗАТЕЛЕН у любого товара: нет в запросе/
импорте → автогенерация внутреннего EAN-13 (верная контр. цифра, префикс не
пересекается с весовыми/ценовыми префиксами орг. — InternalBarcode).
- Цена продажи не обязательна для сохранения; но ПРОДАТЬ без неё нельзя — касса и
розничная продажа дают «У товара «X» нет цены продажи» (можно ввести вручную).
- Быстрое создание из кассы/приёмки: POST /api/catalog/products/quick (имя + опц.
штрихкод/цена/весовой).
- Снят FluentValidation NotEqual(Guid.Empty) на единицу (контроллер подставляет дефолт).
Тесты: unit InternalBarcode 3/3 (валидный EAN-13, не парсится как весовой, обходит
префиксы); integration 3/3 (создание по одному имени, продажа без цены → 400, quick).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Касса работает из локальной базы сразу; вся синхронизация — в фоне, UI не ждёт.
- HttpClient с AutomaticDecompression: сервер уже жмёт JSON (gzip), но касса не
слала Accept-Encoding → каталог 30k ехал 22 МБ; теперь 3 МБ (×7.3).
- ApplySync: убран O(n²) скан цен (HashSet product-id) — запись 30k 12.2с → 1.8с.
- Остатки дельтой: PosSyncResponse.StocksFullSnapshot — первичная загрузка = полный
снимок, далее только изменённые (ушедшие в 0 удаляются). Рекуррентный тик:
остатков 30000→0, 806мс→189мс.
- Синк вне UI-потока (Task.Run) + маршалинг обновлений через Dispatcher: тяжёлая
запись каталога/дельты никогда не фризит окно. Флаг анти-перекрытия тиков.
- Индикатор в шапке «обновлено N мин назад / обновляется…»; ненавязчивая полоса
первичной загрузки (только когда каталог пуст), не модалка.
- Сервер: since нормализуется к UTC (был 500 от Npgsql на Kind=Unspecified).
- Тест POS-дельты остатков (upsert/zero-remove); oversell-тест кассы приведён к
решению владельца (продажа в минус разрешена → Accepted + предупреждение).
Замеры (стенд, 30 000 товаров): первичная 14.9с→5.1с (сеть 3.2с + запись 1.8с),
тёплый старт 141мс, дельта без изменений 189мс, дельта 500 товаров ~0.35с.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Второй spec: seed + списания в минус, интервал 1 мин, ждём cron-джоб (*/5),
открываем колокольчик → видно «N товаров ушли в минус» + «Пересчитать». Проходит
на стенде (~3.4 мин ожидания джоба).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Канал 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>
Сервер уже слал остатки в sync (StockSyncDto), но касса их отбрасывала. Теперь:
- PosStore хранит снимок остатков (таблица stock, перезапись каждый синк — сервер
шлёт полный снимок ненулевых); GetStock/NegativeCount/NegativeStocks.
- В строке чека при количестве больше остатка — «на остатке N шт — уйдёт в минус»
(жёлтое, НЕ блокирует продажу — решение владельца). Работает офлайн по снимку.
- В шапке — постоянный жёлтый индикатор «N в минусе — нужна инвентаризация» по
складу кассы, по тапу — список товаров (Flyout). Обновляется при старте, после
чека и после синка.
- Тесты PosStore: снимок/перезапись, GetStock, NegativeCount/список (4/4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playwright stage-ui-negative-stock: signup+seed+списания в минус (API), затем в
браузере проверяет постоянный баннер, навигацию «Пересчитать эти товары» →
черновик инвентаризации, фильтр «Только минусы». Проходит на стенде (12с).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Решение владельца 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>
После рефактора модели МойСклада (839fcaf) авто-загруженные строки получают
ActualQty=BookQty (а не 0), и проведение без расхождений — no-op (204, без
документов, остаток не меняется). Обновил два устаревших ассерта + все 4 теста
InventoryPostUnpostTests зелёные (вкл. cancel-results).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Было: unpost при проведённых связанных Оприходовании/Списании молча снимал их с
проведения и УДАЛЯЛ (204, номера исчезали). Хуже МойСклада, теряется история.
- unpost при ПРОВЕДЁННЫХ связанных → 409 с номерами О-/С- и текстом «Сначала
отмените результаты». Если связанные только черновики — удаляем черновики
(не влияли на остатки) и возвращаем инвентаризацию в черновик.
- Новое POST /{id}/cancel-results (InventoryEdit + EnterEdit/LossEdit): снимает
связанные Оприходование/Списание с проведения, НЕ удаляя (остаются черновиками,
видны в «Связанных документах», истории, аудите); остатки возвращаются; для
Оприходования — гард на уход остатка в минус. Аудит InventoryCancelResults.
- Веб: кнопка «Отменить результаты» (при проведённых связанных) + диалог со
списком документов и влиянием на остатки; «Вернуть в черновик» переформулирован.
- docs/mobile-api.md: unpost больше не удаляет проведённые (409), добавлен cancel-results.
- Тесты: integration (409 + cancel-results оставляет черновики + повторное
проведение) + full-chain шаг 10.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>