Часть 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>
Снято на win11-kassa-test VM (foodmarket.Pos.exe --demo, software-рендер,
захват из интерактивной сессии). Заголовок окна «ALL MARKET · Касса», крупное
лого all market, «Касса» рядом, экран продажи целиком, таскбар 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>
now-pos-sale.png / now-pos-login.png были сняты 17:41, до фикса 3a0f9e2 (18:07).
Переснято с текущего UI: лого ALL MARKET ~140px, «Касса» через разделитель,
бейдж «Отложенные 2» целиком внутри кнопки.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Раньше флагировались все товары в группах с ParentId==null (включая реальные
верхнеуровневые категории вроде «Молочные»). Теперь — только товары в служебной
группе «Все товары»/«Без группы». На seed-demo: было 50 (ложно), стало 3 (верно).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страница /catalog/data-quality: счётчики по проверкам, детальные списки, массовое
исправление (задать группу/цену, сгенерировать штрихкоды), «это разные товары» для
дублей, пересчёт, тумблеры отключения проверок. Ненавязчивый бейдж «N товаров требуют
внимания» на дашборде (исчезает при 0). Пункт меню «Проверка данных» в каталоге.
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>
Скан неизвестного штрихкода (онлайн) → диалог «Новый товар» (наименование + опц.
цена) → POST /products/quick → товар сразу в чеке; попадёт в локальный каталог на
ближайшем синке. Офлайн — как было («не найден»). Smoke/uitest зелёные.
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>
Веб Z-отчёт показывает byProduct (товар/продано/выручка/возвраты) и баннер «смена
открыта дольше 24ч». Печать уже была (window.print()).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проведённая приёмка → «Создать возврат» вызывает from-supply (преднабор строк) и
открывает черновик возврата поставщику. Вместе с секцией оплат приёмки (supply→
payment) и ссылками-основаниями в DTO — развивает «создать на основании».
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>
П.9: DocStatusBadge (Черновик/Проведён с подсказкой) раскатан на Оприходования,
Списания, Перемещения, Возвраты поставщикам, Отгрузки (было: приёмки/продажи/инвент.).
П.4: в карточке товара рядом с полем штрихкода — кнопка «Сгенерировать» (внутренний
EAN-13). Группа по умолчанию — «Все товары», единица — «шт» (уже было).
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>
П.7: типы денег названы привычно — «Приход наличных (ПКО)», «Расход наличных (РКО)»,
«Входящий/Исходящий платёж», «Перевод между счетами».
П.8: блок «Прибыль за сегодня/месяц» на дашборде (во всех режимах, /api/dashboard/margin),
с пометкой «приблизительно; точно после FIFO».
П.9: общий DocStatusBadge (Черновик/Проведён/Завершена) с tooltip «что это значит»,
применён на списках Приёмок/Продаж/Инвентаризаций.
П.6: проверено — Оприходование/Списание в простом режиме не прячутся (отдельные пункты).
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>
Некритичная проверка версии при старте — fire-and-forget, но на мёртвой сети висела
до 30с (дефолт HttpClient). Короткий CTS, чтобы фоновая задача не держалась зря.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docs/moysklad-pos-startup.md: прямой замер «Кассы МойСклад» (Celeron N4120, 99–113 с
до готовности; каталог перезаписывается целиком, полный JSON+FTS) — подтверждённые
причины, не догадки.
- docs/pos-progress.md: раздел «Мгновенный старт» — что сделано (касса из локальной базы +
фоновый синк, дельта остатков, gzip-каталог ×7.3, синк вне UI-потока) + таблица замеров
(Linux/стенд + Win11 VM + 1 ядро как прокси Celeron). Цель <2 с тёплый старт на слабом
CPU достигнута (<1 с). Серверный кэш снимка осознанно не делали (риск памяти, путь
«раз на установку»).
- deploy/pos-install.ps1: установщик Windows (исключение Defender + прогрев + ярлык);
холодный старт 10 с → 4,5 с. deploy/pos-win-measure.ps1 — скрипт замеров на VM.
- docs/pos.md: обновлён раздел первого запуска + варианты/стоимость подписи exe (не покупали).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- POST /api/admin/seed-demo?bulk=N — массовое наполнение каталога (до 60k товаров)
батчами для нагрузочных замеров (идемпотентно по маркеру BULK-).
- POS --bench <url> <user> <pass>: мерит первичную загрузку (сеть/запись отдельно),
тёплый старт (открытие базы + поиск), дельта-синк. С gzip-декомпрессией.
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>
Пасс 3: касса (индикатор+предупреждение+офлайн), Web Push (VAPID), e2e колокольчика.
Итог: unit 299/299, integration 4/4, POS 4/4, e2e-браузер 2/2.
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>