П.7: типы денег названы привычно — «Приход наличных (ПКО)», «Расход наличных (РКО)»,
«Входящий/Исходящий платёж», «Перевод между счетами».
П.8: блок «Прибыль за сегодня/месяц» на дашборде (во всех режимах, /api/dashboard/margin),
с пометкой «приблизительно; точно после FIFO».
П.9: общий DocStatusBadge (Черновик/Проведён/Завершена) с tooltip «что это значит»,
применён на списках Приёмок/Продаж/Инвентаризаций.
П.6: проверено — Оприходование/Списание в простом режиме не прячутся (отдельные пункты).
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>
- 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>
Пасс 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>
Таблица токенов устройств (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>
Решение владельца 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>
Было: 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>
Галерея docs/site-screenshots/ — задеплоенные страницы test.food-market.kz
с новыми честными текстами (главная, касса, импорт, возможности, перенос и
интеграции, тарифы, для продуктовых, о нас). Удалён кадр удалённой вертикали.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Убраны обещания того, чего нет: фискализация ОФД/Kaspi Pay (skeleton) →
«в разработке»; ЕГАИС/акцизы/модификаторы/серии-сроки/размерные сетки →
удалены 5 выдуманных страниц-вертикалей (остался продуктовый); CRM/публичный
API убраны; «О нас» без выдуманной истории и команды. Касса — Windows и Linux
(не .msi), «Скачать» → «скоро». Импорт — CSV/1С-CSV/МойСклад. Лояльность — в
админке, не в кассе. Интеграции — честный блок «есть / в разработке».
Язык владельца магазина, без жаргона (дашборд, нативно, вертикали, фишки,
триал, SLA, CRM, Unlimited, кейсы) и пафоса. Заголовок кассы: «Быстрая касса,
которая работает и без интернета». Цены тарифов не менялись.
По правкам Mac-Claude после ревью docs/site-copy-draft.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Проверил каждое утверждение сайта по коду/стенду: фискализация ОФД/Kaspi Pay —
skeleton (нет), ЕГАИС/акцизы/модификаторы/серии-сроки/размерные сетки/гарантии —
нет, CRM — нет, лояльность — в админке не в кассе, импорт — CSV/1С-CSV/МойСклад,
касса — Windows+Linux (zip/AppImage, не .msi), «О нас» — выдуманная история.
Замеры скорости кассы: холодный старт 82 мс, инициализация 625 мс (< 1 c), поиск
по ШК 36 мс. Черновик переписанных текстов — docs/site-copy-draft.md (было→стало).
НЕ выкладывается до «ок» Mac-Claude.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. Английские строки в админке → русский: ru.json dashboard.title «Dashboard»→
«Сводка», common.live/offline «live/offline»→«онлайн/офлайн»; дашборд-виджеты
defaultValue «Low-stock alerts»→«Низкий остаток», «Live-обновление»→«Обновляется
в реальном времени».
2. Каталог: системная группа «Все товары» больше не дублирует корень дерева
(ProductGroupDto.IsSystem + ProductGroupTree прячет системные группы).
3. Валюта: розничная цена в каталоге теперь «₸», а не «KZT» (символ вместо кода).
4. Касса на сайте: вместо HTML-макета — НАСТОЯЩИЙ экран Avalonia-кассы
(--screenshots) с витринными данными «Магазин у дома», кассир Виктория Ким, с
логотипом; убран префикс «Магазин » в шапке (не было двойного «Магазин Магазин»).
HTML-макет удалён из публичного пайплайна.
5. Витрина: фасовка/единица товара выводится из названия (кг→весовой, иначе
штучный) — нет «Авокадо 1шт весовой» / «Бешбармак 1кг штучный».
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Скриншоты задеплоенных страниц test.food-market.kz (главная/касса/импорт/
тарифы/вертикали/о компании) для проверки на глаз. Все новые скриншоты ALL MARKET
на витринных данных отрисовываются корректно.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Пересняты скриншоты публичного сайта в интерфейсе ALL MARKET на витринной
организации «Магазин у дома» (200 товаров, ~30 дней продаж, чистые данные
без e2e/test-мусора). Светлая тема, 1440×900 @2x, WebP + srcset, alt по-русски.
- screenshots.mjs: стенд + витринный владелец, 14 экранов (дашборд/каталог/
карточка/приёмки/остатки/инвентаризация/деньги/отчёты/контрагенты), скрытие
баннеров стенда/«что нового»/MoM-дельты, резолв AsyncSelect-подписей
- optimize-screenshots.mjs: PNG→WebP (1x 1440w / 2x 2880w) через canvas, все ≤300КБ
- pos-mockup/pos-mockup.html: светлая касса ALL MARKET «Магазин у дома» (продажа+оплата)
- index.astro: hero-дашборд + каталог + новый блок «Касса»; pos.astro: продажа+оплата
- ShowcaseSeeder: стартовый завоз всех товаров (нет отрицательных остатков)
- deploy/showcase-seed.sh: RESET=1 пересоздаёт витрину с нуля
- исходные PNG не коммитятся (.gitignore), доставка — только WebP
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ShowcaseSeeder — чистые данные для публичных скриншотов: 200 товаров без
Article-маркеров, реальные EAN-13, боевой формат номеров документов
(П-2026-…, ПР-2026-…), 3 поставщика, счета (касса/Kaspi/эквайринг),
сотрудники с нормальными именами, ~30 дней продаж, приёмки, 2 списания,
1 завершённая инвентаризация. Без e2e/test/Y1/DEMO в видимых данных.
- POST /api/admin/seed-demo?showcase=1 (DemoSeedController)
- повторяемый deploy/showcase-seed.sh (signup + seed, идемпотентно)
- YearDemoSeeder: data-массивы → internal для переиспользования
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- InventoryEditPage: «Завершить подсчёт» вместо «Провести»; на завершённой — кнопки
«Создать оприходование излишков» / «Создать списание недостачи» (блокируются, если нет
разницы или документ уже создан) + блок «Связанные документы» + «Вернуть в черновик».
- InventoriesPage: статус «Завершена»; колонка «Результат» (опр./спис. № или «не применено»).
- Enter/LossEditPage: «На основании инвентаризации №…» со ссылкой.
- types.ts: InventoryStatus Draft/Completed; related в InventoryDto; результат в списке;
inventoryDocId/Number в Enter/Loss DTO.
- docs/inventory-moysklad-reference.md (сверка), inventory-model-progress.md,
supply-inventory.md («Как провести инвентаризацию», 5 шагов), mobile-api.md (compat post/unpost).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Замечание владельца: инвентаризация стояла «Проведено» без оприходования/списания.
Переделано по МойСкладу (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>
Дополнен docs/moysklad-logic-audit.md (+ .html) разделом «Проход 2»:
- Все «проверить» закрыты фактом (код файл:строка и/или запрос на стенде):
возврат→ReferenceSaleId; Z-отчёт без «по товарам»; долг поставщику считается
(MoneyReportsController:120-183); обязательные поля товара Name+Unit+Group;
приёмка с телефона/терминалы/фискализация — нет; отмена проведения ЕСТЬ у всех
документов (исправление прохода 1); смена — только ручное открытие.
- Себестоимость: подтверждён FIFO у МойСклада; у нас средняя; реальный пример Демо
(BIOBLAS, Product.Cost=548.6441 из 3 приёмок) — показано искажение прибыли.
- Казахстан (новое): фискализация ОФД РК (заглушка у нас; МойСклад — WebKassa/reKassa),
НДС 16% с 2026 (дефолт KZ уже 16, SystemReferenceSeeder:38), маркировка ИС МПТ
(только флаг IsMarked), СНТ/ЭСФ, акциз. Блок «Казахстан — обязательное по закону».
- Сводка обновлена: топ-15 скорректирован (фискализация/маркировка РК вместо неверных
пунктов), добавлен KZ-блок. Касса МойСклад vs наша — по функциям кассира.
Только отчёт, без правок кода.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Сверка всех процессов простого режима + справочников/кассы/мобильного/отчётов с
МойСкладом (API dev.moysklad.ru + support.moysklad.ru + школа торговли). Для каждого:
как в МойСкладе (ссылки) | как у нас (файл:строка) | расхождение | серьёзность |
слабые места МойСклада | предложение «как лучше» [НА РЕШЕНИЕ ВЛАДЕЛЬЦА].
Главные находки (🔴): себестоимость у нас скользящее среднее vs FIFO у МойСклада;
прибыль берёт COGS приближённо (Product.Cost), а не на момент продажи; инвентаризация
сама двигает остатки и статус «Проведено» без оприходования/списания. Также — какие
наши тесты закрепляют неправильное поведение и должны быть переписаны.
docs/moysklad-logic-audit.md — полный отчёт; .html — краткая версия для владельца.
Только отчёт, без правок кода.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- now-web-login-stage.png: вход на test.admin.food-market.kz после деплоя —
баннер стенда, быстрый вход, логотип ALL MARKET, без тостов ошибок.
- rollback-logo-foodmarket.png: подтверждение отката (лого Food Market).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Отчёт по ревью безопасности (сидер/префилл/ротация пароля стенда), результат
проверки отката в worktree, уточнённые границы (артефакты кассы, маркетинг-скриншоты).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Верхняя полоса: логотип ALL MARKET высотой 48 (ширина ≈140px ≥ минимума
брендбука), «Касса» отделена тонким разделителем (не оторвана).
- Бейдж количества у «Отложенные» теперь целиком внутри кнопки (меньше паддинг
кнопки, плотнее контент) — не обрезается правым краем.
- Перегенерированы скриншоты кассы; обновлён кадр веб-входа в галерее (без тоста,
без префилла пароля).
- Зелёный UI не менялся (--color-brand #00B207, идентичен дорибрендинговому).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/rebrand-screenshots/ + index.html: пары Было|Стало по логотипу, favicon,
письму-приглашению, OG-картинке, кассе (вход/продажа); «Стало» для веб-входа и
публичной главной. «Было» извлечено из тега pre-rebrand-all-market.
Отмечено, что встроенные продуктовые скриншоты маркетинг-сайта обновятся при
прод-выкатке (screenshots.mjs против живой админки).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/imports.md, docs/simple-mode.md: «Food Market» → «ALL MARKET».
- Перегенерированы скриншоты кассы (вход/продажа/оплата/смена/возврат) с
логотипом ALL MARKET в шапке и на экране входа.
- Внутренние TZ/verify-доки оставлены как историческая фиксация.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Верхняя полоса и экран входа: логотип-изображение ALL MARKET из ресурсов
(avares Assets/), color на светлой теме и white на тёмной (Brand.ApplyLogo).
- Заголовок окна «ALL MARKET · Касса»; иконка окна и ApplicationIcon = allmarket.ico
(собрана из фирменных PNG 32/180/192).
- Чек: в шапке — название точки/магазина (резолвится из RetailPoint, PosSession.StoreName),
внизу мелко «ALL MARKET — система учёта торговли» (был хардкод «Food Market» в шапке).
- AppImage .desktop: Name «ALL MARKET Касса» + фирменная иконка.
- Эталон-макет pos-mockup.html — логотип «all market».
- AssemblyName/exe (foodmarket.Pos) и имена артефактов не менялись (инфраструктура).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Тег pre-rebrand-all-market на дорибрендинговом main (c8c0be5), запушен.
- docs/rebrand-all-market-progress.md — план и границы (что НЕ трогаем).
- docs/rebrand-rollback.md — точные команды отката кода (revert-диапазон) и стенда.
- deploy/rebrand-rollback.sh — dry-run по умолчанию, --apply для реального отката.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Реальное окно Windows 11 заново: продажа (1280×800, компактный уровень —
«Очистить чек» виден, ничего не перекрывается) и оплата.
- docs/pos-screenshots/density/: экран продажи в 1280×720/800, 1366×768, 1920×1080
(подтверждает адаптив панели по высоте и шапки по ширине) + раздел в галерее.
- Перегенерированы «Стало»-кадры и compare-*.png — вёрстка 1:1 с макетом сохранена.
- docs/pos-ui-progress.md: раздел «Полировка» 7/7.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Текст во ВСЕХ кнопках по центру: HorizontalAlignment=Stretch + Horizontal/
VerticalContentAlignment=Center в стилях Button.pos/key/seg (раньше key/seg не
наследовали центровку → цифры/«−/+»/«Сброс»/«⌫» липли влево-вверх).
- Клавиатура оплаты — ровная сетка 3×4 одинаковых крупных клавиш (72px), цифры по центру.
- Быстрые суммы и сегменты «Нал/Карта/Смешанная» — UniformGrid одинаковой ширины;
сегменты слитным переключателем; «Без сдачи» не обрезается.
- Модалка оплаты по высоте содержимого, по центру, на затемнённой подложке (#0F1F17 ~40%).
- Правая панель: ровная 2-кол. сетка вторичных кнопок; добавлены «Возврат · F9» и
«Внесение/Выплата · F10»; «Очистить · Esc» — красный контур во всю ширину; без пустот
(блок компактный, Оплатить сразу под ИТОГО).
- Строка корзины: степпер 44×44 с количеством по центру, отступ до иконки удаления.
- Экран продажи снимается С ОБОЛОЧКОЙ (верхняя полоса + капсулы горячих клавиш);
«Избранное» — шеврон ▾/▸. --uitest OK, 44 unit зелёные.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Избранное: 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>
- --demo: окно с примером чека без логина (software-рендер на Windows, т.к. у
тест-VM нет GPU — иначе окно чёрное).
- Снят реальный экран кассы на Windows 11 (интерактивная сессия tester через
scheduled task /it + PrintWindow): подтверждён нативный рендер — кириллица,
формат «3 362 ₸», кнопки без обрезки → docs/pos-screenshots/windows/ + раздел
«Windows 11» в галерее.
- Ограничения VM (не код, честно в docs): нет GPU → software-рендер; консоль
1280×800 → 1366/1920 реальным окном недоступны (они в headless-кадрах, тот же
движок); меню «Пуск» VM залипает. Автоматизация логики UI — headless --uitest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Публикация win-x64 с ReadyToRun (по одному RID, nice — dev-vm не уронили).
- Замеры на Windows-VM: первый запуск (свежая распаковка) 3.8–5.4 с (было 7.5 с),
прогретый 219 мс. Первый запуск упирается в Defender (скан свежего неподписанного
114 МБ exe) + холодное чтение диска, не в JIT.
- docs/pos.md: раздел «первый запуск медленный — что делать» (исключение Defender,
прогрев при установке, подпись exe). Trimming оставлен выкл. (Avalonia на рефлексии).
- Мелочь: убран warning CS8826 (имена параметров partial-методов смены).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
По итогам проверки Mac-Claude (скриншоты):
- Формат чисел как в вебе/мобильном: «3 422 ₸» (пробел-разделитель тысяч),
вес «1,235 кг» (запятая), единица измерения рядом с количеством («3 шт»).
PosFormat — культура ru-RU с NBSP-разделителем, применяется процессно.
- Скидка на строку видна: «1 560 → 1 500 ₸, скидка 60» под позицией, чтобы
цена×кол-во ≠ сумма не выглядело ошибкой (SaleLineVm.DiscountText/HasDiscount).
- Кнопки: текст по центру по вертикали и горизонтали (стиль Button.pos +
ContentPresenter); «Очистить» и «Enter · Провести и печать» больше не
обрезаются на 1366×768 и 1920×1080 (проверено скриншотами).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Инцидент #1791428701 (quality-watchdog): кассир получал 403 на /api/money/accounts
на странице смен. Фикс — узкий /api/money/shifts/accounts (см. 9dc033d). Retest на
стенде: menu-walk 3/3 роли зелёные.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cart.Snapshot()/Restore() — независимая копия строк и скидки на чек.
- SaleViewModel: ParkCommand («Отложить» — сохранить чек, начать новый) и
ResumeCommand («Вернуть» — восстановить отложенный); кнопки на экране продажи.
- Unit-тест независимости снимка (38 POS unit-тестов зелёные). Скриншоты обновлены.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stage-reset-demo.sql — очистка орги от транзакционных данных (KEEP каталог/
сотрудники/точки). fix-internal-ean13.sql — пересчёт неверной контрольной цифры у
уже выданных внутренних EAN-13 (на стенде исправлен 41 код, список — docs/ean13-fixed-20261008.txt).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16 шагов против настоящего стенда: регистрация→справочники→сотрудники/точки→
деньги→3 приёмки→оплаты→смена/продажи/возврат/Z→склад→отчёты→инварианты→откат.
Сверяет конкретные числа и инварианты, 0×5xx/0×тостов/0×console.error. Режимы
C (temp-орг) и B (Демо на реальном каталоге). Hi-res скриншоты каждого процесса +
галерея. Включён в nightly-verify и quality-watchdog (скрипты в $HOME).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/money.md — как открыть смену, оплатить приёмку, смотреть долги/остатки,
права ролей. Прогресс-док: пп.8–9 отмечены, журнал с тремя фиксами по ходу
e2e (back-fill прав, date-only→UTC, список счетов 500). Итог 10/10.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Основное:
* 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>
Старый idempotency-маркер — Product с Article LIKE 'DEMO-%'. После того как
«Демо-магазин» на стенде наполнили реальным каталогом из МойСклада
(артикулы не DEMO-*), маркер срабатывал false и QuickLoginDemoSeeder при
каждом рестарте стенда пытался залить фиктивный каталог заново — падая
на unique-индексе IX_product_barcodes_OrganizationId_Code (23505). В БД
ничего лишнего не попадало (транзакция откатывалась), но лог stage-api
получал 9 ERR в сутки.
Фикс: SeedAsync и PeekAsync считают «уже сидировано» при любом Product в
орге. Поведение из UI (POST /api/admin/seed-demo) не меняется для свежих
org'ов. Прочие стартовые сидеры (OpenIddictClientSeeder,
SystemReferenceSeeder, DevDataSeeder) и HangfireJobsConfigurator — уже
идемпотентны, аудит в docs/seeder-noise-progress.md.
Integration-тест:
- SeedDemo_second_call_on_populated_org_returns_already_seeded
- SeedDemo_on_org_with_imported_catalog_skips_without_duplicate_barcode
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
05.10.2026 общий postgres-инстанс с чужими боевыми базами (sklad, purchase,
calcman, calcman_staging, makesales + мусорная старая food_market) вынесен
из compose прода food-market в отдельный compose-проект shared-postgres
(контейнер shared-postgres, порт 5434, том food-market-postgres-data external).
Прод food-market поднимается в собственный контейнер food-market-prod-postgres
(порт 5437, том food-market-prod-postgres-data). Простой при разделении — 12с,
соседи живы. См. docs/shared-postgres-followup-progress.md.
Этот коммит синхронизирует репозиторий, чтобы следующий деплой из main не
воскресил старую раскладку:
- deploy/docker-compose.yml: container_name/port/volume приведены к прод-файлу
(food-market-prod-postgres, 127.0.0.1:5437, food-market-prod-postgres-data).
- deploy/shared-postgres/ (новое): документационная копия соседнего compose'а,
с явной пометкой «food-market им не управляет».
- deploy/food-market-backup.sh / anonymize-prod.sh / db-schema-diff.sh:
defaults FM_PG_CONTAINER / FM_PROD_CONT = food-market-prod-postgres.
- docs/RUNBOOK.md: новый раздел «Postgres на prod-vm» с таблицей трёх
инстансов и явным запретом трогать shared-postgres из наших скриптов.
- docs/backup-restore.md: контейнер в таблице и примерах — новый.
Guards (не в репе — живут в ~/deploy-stage.sh и ~/quality-watchdog.sh)
расширены симметрично: стенду запрещены shared-postgres, food-market-prod-
postgres[-data], 5434/5437; проду — food-market-postgres-data (чужие данные).
Проверка: docker compose config/ps на prod-vm shared-postgres не упоминают,
grep по репе — ни один скрипт не делает docker exec в shared-postgres.
Третья утечка stage↔prod: stage-compose разделял с прод-compose
/opt/food-market-data/uploads И названные тома (food-market-postgres-data,
food-market-api-data, food-market-api-logs). Теперь всё разведено.
- Новый deploy/docker-compose.stage.yml — шаблон стендовского стека с
food-market-stage-* волюмами, bind /opt/food-market-stage-data/uploads,
container_name food-market-stage-*, OpenIddict Issuer по дефолту смотрит
на test.admin.food-market.kz (а не на admin.food-market.kz).
- На prod-vm сделаны: pg_dump стенда → down → переименованы stage-*-pre-
isolation-20261005 → up с новыми волюмами → pg_restore. Прод-контейнеры,
прод-том food-market-postgres-data и прод-папка /opt/food-market-data/
не тронуты (read-only inspect).
- ~/deploy-stage.sh получил guard до билда: проверяет шаблон репо и
удалённый stage-compose на запрещённые паттерны прод-путей/томов/имён/
issuer-URL.
- ~/quality-watchdog.sh +step 10 (06:XX ежесуточно) — тот же набор
паттернов + docker inspect реального стенда.
- docs/stage-prod-isolation-progress.md: таблица до/после, что оставили,
как разводили, follow-ups.
Smoke на test.admin.food-market.kz: /health/ready Healthy, 31233 товара,
invoice upload → parsing (OCR Hangfire job), GDPR org-export zip упал в
/opt/food-market-stage-data/uploads/exports/. Прод-папка /opt/food-market-
data/uploads/ до/после миграции: 13 файлов, 0 изменений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Второй проход после ручного ретеста владельца: добавил RemoteHost ssh на
dev-vm, объяснил почему e2e п.4 не поймал (не запустил), и описал
owner-policy про удаление файлов + cleanup legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Playwright 1/1 passed против test.admin.food-market.kz после полной
заливки МойСклада в Демо-магазин:
- /catalog/products под demo-admin — 30 894 записей в шапке списка.
- Поиск по частичному названию «шоколад» → хотя бы одна строка.
- Поиск по штрихкоду 4820039807373 → хотя бы одна строка; клик →
карточка товара в простом режиме (Название, Штрихкоды, Группа,
Единица измерения, Розничная цена видны; «Типы цен» в sidebar нет).
Второй прогон импорта с тем же токеном в теле запроса → created=0,
updated=30894 (все существующие refresh'нуты). Идемпотентность по
MoySkladExternalId подтверждена.
Выборочная сверка 10 случайных товаров с МойСкладом (имя, штрихкоды,
цена продажи, закупочная) — 10/10 совпали ровно.
Token checks:
- Organizations.MoySkladToken IS NULL для «Демо-магазин» (токен
никогда не сохранялся в БД, передавался только в теле запроса).
- /tmp/ms-token после импорта shred -u — локально ничего не осталось.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Для полной заливки каталога МойСклада в «Демо-магазин» на стейдже
переделал импорт по требованию «идемпотентный + без дублей».
Схема:
- 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>
- stage-ui-menu-links.spec.ts: убрали describe.configure({mode:'serial'})
— одна фоновая 403 на кладовщике скрывала остальные три теста.
Расширили expected4xxContains на /api/organization/employees,
/api/super-admin/, /api/dashboard/widgets — это пробы, которые
sidebar делает всегда, и на ограниченных ролях они ожидаемо 403.
- docs/menu-links-progress.md: 6/6 ✓, протокол выполнения с
результатами e2e-прогона (5/5 passed, 60s против test.admin).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Три новых роута из Sprint 31 были достижимы только по прямому URL —
в sidebar/командной палитре их не было. Добавлено:
- AppLayout: «Накладные (по фото)» (/purchases/invoices) в разделе
«Закупки», «Инвентаризация с телефона» (/inventory/mobile) в
«Остатках» — для Admin и Storekeeper, видно в простом режиме;
i18n ru + en.
- SplitButtonMenu — переиспользуемая dropdown-кнопка «Новая…».
SuppliesPage: вручную / по фото / сканированием. InventoriesPage:
вручную / с телефона.
- CommandPalette (Cmd+K): новые static-pages invoices, supply-scan,
inventories (был пропущен), inventory-mobile.
- QuickActionsPalette (Cmd+J): invoice-new, supply-scan,
inventory-mobile.
- Параллельно всплыл сирота /admin/diagnostic — добавили ссылку
в sidebar под Admin'ом.
Playwright stage-ui-menu-links.spec.ts: demo-keeper + demo-admin,
desktop 1280×800 + mobile 375×667 — кликами с главной до всех
трёх новых экранов, скриншоты в reports/. Негатив для demo-cashier
проверяет, что ему эти пункты не видны.
Orphan-audit: tests/scripts/menu-orphans-audit.mjs парсит App.tsx и
диффит против фактических точек входа (sidebar/Cmd+K/Cmd+J/split-
кнопок). После всех правок — 0 сирот (отчёт docs/menu-orphans-report.md).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Smoke подтверждён на test.admin.food-market.kz после rebuild:
- GET /api/purchases/invoices (auth) → 200 (пустой пока).
- GET /api/inventory/inventories/mobile-catalog → 200, 321 штрихкод в сжатом слепке.
- GET /api/purchases/invoices/counterparty-search → 200.
Прод не тронут (API 2 месяца Exited, prod-БД foodmarket-postgres без входящих).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Unit:
- InvoiceMatcherTests (12): barcode wins, short barcode falls through,
alias by code/by name_norm, alias is supplier-scoped, по названию
НЕ матчим автоматически (sklad-урок про LD Super Slims),
NormalizeName quotes/whitespace/unit-glue/ё→е, OnlyDigits, AliasKeys
skip пустые.
- ClaudeCliInvoiceExtractorParseTests (8): plain JSON, envelope с
result+usage, markdown fence, barcode spaces strip, null-tolerant,
'empty' raises, пустое name пропускается. Parse/FromJson теперь
public (internal мешал тестам, InternalsVisibleTo избыточен).
Integration:
- Matcher_hits_product_by_barcode_from_real_catalog — реальный
Postgres + seeded org + создаём товар+штрихкод, matcher находит.
- Multi_tenant_isolation_matcher_does_not_see_other_orgs_barcodes —
две org, штрихкод orgA НЕ виден в matcher'е orgB (tenant isolation).
Все новые тесты зелёные. Pre-existing failures в full-suite не связаны.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backend дополнения (InventoriesController):
- GET /mobile-catalog?groupId= — компактные тройки [barcode, productId,
name] для оффлайн-работы. Фильтр по группе = частичная инвентаризация.
- POST /{id}/add-line — scan-mode: +1 к существующей строке или новая,
bookQty подтягивается из текущего Stock (пункт 6: без ручного
«Пересчитать»).
- GET /{id}/book-drift — список позиций, где текущий Stock != сохранённого
BookQty. UI показывает перед Post, чтобы было видно что учётный поплыл
(продажи/приёмки с момента создания). Пункт 6 целиком.
Существующий Create уже заполняет bookQty из Stock (строки not passed →
подтягивает весь склад; строки passed → подтягивает book по каждой).
Update тоже auto-fill'ит для новых строк.
Web (lib/offlineCatalog.ts): IndexedDB wrapper (DB=fm-inventory,
STORE=catalog, META=meta). saveCatalog/findByBarcode/catalogMeta/
countRows. Порт из sklad'овского db.ts.
Web (InventoryMobilePage.tsx) — три stage'а в одном файле:
- setup: склад + опц. группа + 'Загрузить каталог на телефон'.
- scan: Камера <CameraScanner /> или аппаратный сканер; скан → +1 к
существующей строке; неизвестные → в отдельный список unknown с
предупреждением; офлайн-плашка; localStorage сохраняет rows+docId.
- review: карточки с book/факт/дельта/дельта*себестоимость + фильтр
'только расхождения'; drift-плашка если учётный поплыл;
'Провести' → POST /post.
Routes: /inventory/mobile. tsc зелёный.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CameraScanner (components/CameraScanner.tsx) — порт из sklad'a @zxing/
browser + @zxing/library. Непрерывное сканирование: окно не закрывается
между сканами, EAN13/EAN8/UPC/Code128/ITF. Подавление повтора 1.5с (в
кадре товар распознаётся десятки раз в секунду). Вибрация 40мс + flash
зелёный/красный + анимация scanPop/scanRing.
SupplyScanPage (/purchases/supplies/scan):
- Выбор поставщика (search по /counterparty-search), склада, валюты.
- Кнопка 'Сканировать камерой' → <CameraScanner /> на полный экран.
- Alt: аппаратный сканер — <input> в details-блоке, Enter = +1.
- Каждый скан: GET /product-by-barcode → если нашёлся, +1 к
существующей строке или новая сверху. Цена подставляется last-
purchase-price (из последней Posted supply-line по этому товару).
- Неизвестный штрихкод: feedback 'Нет в каталоге', вибрация ошибки.
Товар создавать — отдельный переход на /products/new (future wiring).
- Прогресс сохраняется в localStorage ('fm.supply-scan.rows') —
перезагрузка не теряет; очищается после успешного сохранения.
- Save → POST /api/purchases/supplies (существующий endpoint), статус
Draft; редирект в /purchases/supplies/:id для проверки/проведения.
InvoicesController.ProductByBarcode теперь возвращает ещё и
lastPurchasePrice (из последней Posted supply-line).
@zxing/browser@0.1.5 + @zxing/library@0.23.0 добавлены в deps.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
InvoicesPage: карточки загрузок со статусом (бейдж + иконка),
поллинг каждые 3с пока есть pending/parsing (останавливается на
needs_review/applied/failed). Две кнопки загрузки:
- 'Снять камерой' (<input capture="environment" accept="image/*">)
- 'Загрузить файл' (image/* + pdf, multi-select)
Mobile-first: кнопки большие, grid 2-col; на desktop растёт.
InvoiceReviewPage: карточками-строками (телефон читается, desktop
масштабируется). Редактируемые поля qty/price, авто-сумма.
- Шапка: supplier, №, дата, сумма (editable). Предупреждение если
сумма строк отличается от «Итого» больше чем на 1.
- Общий сканер (input Enter) → в первую несопоставленную строку.
- Для несопоставленной строки — 'Найти товар' (debounced search
по /api/catalog/products?q=) с ссылкой 'Создать товар' если
ничего не нашлось.
- Save → PUT /lines (пишет alias'ы автоматически).
- Apply → ApplyDialog: выбор поставщика (search с БИН),
склад (default main), валюта (default KZT). POST /apply →
редирект в /purchases/supplies/:id.
Routes добавлены в App.tsx: /purchases/invoices и /purchases/
invoices/:id.
tsc зелёный.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Координатор отротировал мастер-пароль стенда: теперь значение живёт в
~/.fm-watchdog/stage-shared-password на dev-vm (не коммитится) и в
QUICK_LOGIN_MASTER_PASSWORD в stage-env на prod-vm. Старый дефолт '123456'
на обоих сторонах убран — он мог вводить в заблуждение и был записан в
бандл.
Клиент-серверный контракт:
- Server (AuthorizationController.IsMasterPassword) принимает ДВА пароля
как мастер-ключ: (a) QuickLoginOptions.MasterPassword (константное
сравнение, FixedTimeEquals); (b) публичный sentinel
QuickLoginOptions.SentinelPassword = '__QUICK_LOGIN_PWLESS__'. Оба
гейтованы EffectivelyEnabled — вне стенда отбиваются как любой чужой
пароль.
- SPA (LoginPage.handleQuickLogin) при клике карточки шлёт sentinel в
поле password. Реальный пароль в JS-бандл больше не попадает; ротация
секрета больше не требует rebuild'а веба.
- Форму мы НЕ наполняем sentinel'ом — поле password остаётся пустым,
чтобы ни одно значение не светилось в DOM.
Default QuickLoginOptions.MasterPassword = "" (пустая строка не
матчится в IsMasterPassword). deploy/docker-compose.yml дефолт
QUICK_LOGIN_MASTER_PASSWORD тоже пустой.
Тесты:
- integration (3/3 ok) — добавлены sentinel-кейсы: пропускает активного
при Enabled=true, отвергается при Enabled=false. Admin12345!-санити-
логин в Disabled-тесте перенесён вперёд, чтобы снять lockout-счётчик
от соседей по коллекции (Identity lockoutOnFailure=true).
- e2e stage-quick-login (4/4 ok) — ql02 шлёт sentinel вместо '123456'
(симулирует реальный SPA flow).
docs/quick-login.md:
- без упоминания реального пароля; хранилище — ~/.fm-watchdog/
stage-shared-password; ротация-рецепт через python читает файл
в-процессе (не экспонирует значение в ps/логи).
- раздел «Доступ к стенду» — host-nginx basic-auth для внешнего
интернета, 192.168.1.x бьёт без неё.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Workflow (.forgejo/workflows/docker-public.yml):
- Собирает ДВА тега: :test с PUBLIC_APP_URL=https://test.admin.food-market.kz
(для test.food-market.kz) и :latest с PUBLIC_APP_URL=https://admin.food-
market.kz (для будущего food-market.kz). deploy_prod отключён
намеренно: прод-public закрыт владельцем.
- deploy_test: SSH на prod-vm, идемпотентно устанавливает PUBLIC_TAG=test
в ~/food-market/deploy/.env, pull + up -d public. Smoke проверяет что
/signup/ ссылается на test.admin и НЕ содержит admin.food-market.kz.
nginx.conf (src/food-market.public/deploy/):
- absolute_redirect off; port_in_redirect off;
Было: curl -sI https://test.food-market.kz/signup → location: http://...
(schema терялась на 301 /signup → /signup/).
Стало: location: /signup/ (относительный, браузер держит https).
Env-пример (.env.example) — комментарий, что для test-билда ставить
test.admin и прод в коде оставить дефолтом в source-коде (SignupForm/
Header), но в билде всегда задавать явно через PUBLIC_APP_URL build-arg.
RUNBOOK — новый раздел «Публичный сайт: какой образ ↔ какой домен» с
таблицей и smoke-командой.
Smoke проверил:
- https://test.food-market.kz/signup → 301 на /signup/ (без схемы, https
сохраняется).
- https://test.food-market.kz/signup/ → ссылается на test.admin.food-
market.kz; ни одного вхождения admin.food-market.kz (без test.).
- POST /api/auth/signup с Origin test.food-market.kz → user создан в
food-market-stage-postgres-1 (STAGE), не в food-market-postgres (прод-
adjacent).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
E2E stage-quick-login финально 4/4 green. ql03 проверяет инвариант
безопасности «прод НЕ отдаёт список»: любой ответ ≠ 200 ok, 404 — норма,
502 — пре-существующий outage прод-API (отмечается как gap в отчёте).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Критический фикс: раньше safety-override срабатывал на host.EndsWith(
".admin.food-market.kz") — это ВКЛЮЧАЛО сам стейдж (test.admin.food-market.kz
заканчивается на ".admin.food-market.kz"). Деплой на стенд это и показал:
Warning в логе "test.admin.food-market.kz — это ПРОД" и /api/dev/quick-
login/accounts возвращал 404.
Теперь проверка: host == "admin.food-market.kz" (точное совпадение). Любой
поддомен (test.admin.*, dev.admin.*) — стенд, быстрый вход включается.
Если в будущем появится второй прод-хост — дописать условие явно.
Также добавлено:
- deploy/docker-compose.yml: пробрасывает QuickLogin__Enabled и
QuickLogin__MasterPassword из env (default off).
- docs/quick-login.md: как работает, предохранители, как выключить, как
проверить прод.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Integration (QuickLoginTests.cs):
- Options_effective_matrix: Enabled=false → off; пустой/мусорный Issuer → off;
Issuer=admin.food-market.kz → off (прод); любой другой host (включая
test.admin.*) → on.
- Disabled_returns_404_and_rejects_master_password: default-factory
(Enabled=false) /accounts → 404, master-пароль → 400 invalid_grant,
обычный Admin12345! → 200 (sanity check).
- Enabled_at_runtime_...: мутируем IOptions<QuickLoginOptions>.Value +
IConfiguration в живом сервис-провайдере (так как WithWebHostBuilder на
уже-построенной parent-factory даёт ObjectDisposedException). Проверяем:
/accounts → 200 + SuperAdmin в списке; master-пароль пускает активного
SuperAdmin'а; НЕ пускает деактивированного; смена Issuer на прод
переключает всё в safety-off.
Также фикс в QuickLoginOptions: прод-хост теперь СТРОГО 'admin.food-market.kz'
без suffix-match (раньше test.admin.food-market.kz тоже считался прод-ом).
E2E (tests/e2e/scenarios/stage-quick-login.{yml,steps.ts}): 4 шага —
GET /accounts на стенде → 200 + демо-trio; master-пароль пускает
demo-cashier → /api/me role=Cashier; GET на прод → 404; Playwright: клик
по карточке demo-cashier → редирект с /login + нет ссылки /products
в sidebar (меню кассира).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
HousekeepingJobs.PruneAutoTestEntitiesAsync — Hangfire-recurring (02:45 UTC
daily, Cron:PruneAutoTestEntities). Удаляет:
- orgs по regex autotest-паттерна в имени (quality-, soak-, UI-a11y-,
SmokeOrgA/B-, PerfBaseline-, Reg-, RT-ratetest-, Probe Org, cat3*,
visual-, factory-smoke-, mt6*, doc4*, auth2*, rep5*, rt8*, wiz9*,
i18n*, twofa*, iso*, mob1*, axdet-, load-, s19-test- + любое имя
с [0-9]{10,}) старше 3 дней + все их таблицы с OrganizationId;
- orphan autotest-юзеров (без удалённой org) по regex в email и доменам
@e.kz / @test-fm.local.
Защищены: SuperAdmin (OrganizationId IS NULL), admin@food-market.local,
admin@demo-market.local, demo-*@food-market.local, market@email.com,
«Demo Market» и «Демо-магазин» — явные исключения.
deploy/stage-prune-autotest.sql — ровно та же логика как SQL-скрипт для
ручного запуска. Прогнал на stage: было 8653 user'ов / 8634 orgs →
стало 300 users / 305 orgs. SuperAdmin/demo/market остались.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
StageBanner — жёлтая полоса сверху всех страниц, когда /api/dev/
quick-login/accounts отвечает 200 (стенд). В проде эндпоинт 404 →
компонент возвращает null, никакая полоса не рисуется.
Решение «stage vs prod» кэшируется в sessionStorage, чтобы не стрелять
запрос на каждой навигации в SPA (один запрос за открытие вкладки).
На стенде динамически добавляется <meta name=robots content=noindex,
nofollow> — не хотим чтобы Google индексировал test.admin.food-market.kz.
Один web-билд ходит в оба окружения; никаких отдельных index.html.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Над обычной формой логина теперь висит жёлтый блок «Быстрый вход
(тестовый стенд)»: клик по карточке → моментальный логин через
мастер-пароль 123456 (сохранён как константа в LoginPage.tsx — должен
совпадать с server-side QuickLoginOptions.MasterPassword).
Блок сам опрашивает /api/dev/quick-login/accounts: 200 → рендерится,
404 (= прод) → возвращает null, НИЧЕГО не рисует. Так одним и тем же
web-билдом покрываются и стенд, и прод — секрета в UI нет.
Поиск: ?q= на сервере (debounced 200ms), фильтрует по email/ФИО/роли/
магазину. Карточки с badge SuperAdmin / Демо. Lazy-рендер, max-h-80
с overflow-y чтобы не ломать компоновку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hosted-сервис, который на старте, когда EffectivelyEnabled=true, создаёт:
- организацию «Демо-магазин» (отдельная от «Demo Market» DevDataSeeder'а,
чтобы не ломать e2e, завязанные на DemoMarket);
- tenant references через существующий SeedTenantReferencesAsync (единицы,
магазин, PriceType, системные роли);
- каталог демо-товаров через DemoTenantSeeder (50 товаров + 30 продаж +
приёмки/контрагенты) — идемпотентно, маркер DEMO-*;
- три учётки с ролями Admin/Cashier/Storekeeper:
demo-admin@food-market.localdemo-cashier@food-market.localdemo-keeper@food-market.local
(пароль Admin12345!, но мастер-пароль 123456 тоже подойдёт — через
QuickLoginOptions в AuthorizationController).
На проде (EffectivelyEnabled=false) сервис тихо выходит в StartAsync —
ничего не трогает, ничего не логирует.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Анонимный эндпоинт, который ВИДЕН только когда QuickLogin эффективно
включён (EffectivelyEnabled=true). В проде отдаёт 404, чтобы по наличию
эндпоинта нельзя было вычислить среду.
Фильтрация:
- IsActive=false → не показываем;
- автотестовые (prefix cat31-/visual-/factory-/mt6*/doc4*/auth2*/rep5*/
rt8*/wiz9*/quality-/i18n*/twofa*/a11y-/mob14-/mob15-/iso*/axdet-/
smoke-/ratetest-/perf-/probe-/load-/s19-test-/admin-/cashier-/
keeper-) → не показываем;
- email@e.kz, @test-fm.local — служебные домены diag-probes, тоже скрыты;
- длинный числовой суффикс [0-9]{10,} в email → автотест.
Порядок: SuperAdmin → demo → остальные (ThenBy email для стабильности).
Лимит 50 + поиск ?q по email/full-name/org-name/role (case-insensitive).
Карточка: email, FullName, роль (SuperAdmin / EmployeeRole.Name),
название org, флаги IsSuperAdmin/IsDemo для сортировки/отображения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AuthorizationController теперь после провала обычного CheckPasswordSignIn
даёт второй шанс через QuickLoginOptions.IsMasterPassword — FixedTimeEquals,
чтобы не светить длину. Включается только когда EffectivelyEnabled()=true
(т.е. Enabled=true И Issuer не prod).
Что сохранено:
- неактивные пользователи отшиваются до пароля (user.IsActive сверху);
- 2FA / org-check — после master-password пути ВСЁ ЕЩЁ работают;
- rate-limit (UseAuthFormUsernameKey + UseRateLimiter) отрабатывает ДО
нас, никак не ослабляется;
- собственный пароль продолжает работать — не ломает автотесты и seeded
admin'ов, которые используют Admin12345!.
Warning в лог при каждом срабатывании — чтобы в прод-логах (если кто-то
по ошибке активировал) было видно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dev-only флаг «быстрого входа». Enabled=false по умолчанию, подхватывает
QuickLogin:Enabled и QuickLogin:MasterPassword из конфига (планируется
проброс через env в stage-compose).
EffectivelyEnabled() — жёсткий предохранитель: даже с Enabled=true, если
OpenIddict:Issuer указывает на admin.food-market.kz (прод) — считаем
выключенным. Пустой Issuer тоже отбивает — не рискуем в неизвестном окружении.
Program.cs логирует Warning на старте: включили → «QuickLogin включён для
стенда», Enabled=true но прод → «не активирован», Enabled=false — молчим.
Чтобы ошибка конфигурации всегда была видна в логах прод-инстанса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- deploy-stage.sh → stage поднят с новым кодом; /health/ready зелёный.
- Smoke: /api/me на стейдже содержит advancedMode.
- E2E stage-simple-mode — 7/7 passed, включая Playwright-шаг
«Недоступно в простом режиме».
- docs/simple-mode.md: пользовательская документация с таблицей
простого режима, перечнем расширенных модулей, инструкцией для
SuperAdmin (API + UI), ProblemDetails-примером и ссылкой на тесты.
- tests/e2e/scenarios/stage-simple-mode.yml: заквочен title с «:» для
js-yaml. steps.ts: правильный ключ localStorage SPA ('fm.access_token').
docs/simple-mode-progress.md — все 8 пунктов [x], итог.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- SuperAdminOrganizationsPage:
- OrgRow.isAdvancedMode добавлен.
- Новая колонка «Режим» — бейдж «Простой» (slate) / «Расширенный»
(violet).
- Кнопка-экшн (иконка Sparkles) открывает модалку подтверждения
с разной копией для вкл/выкл; успешный PUT → list.refetch().
- Выкл-копия напоминает что данные НЕ удаляются и переключение
обратимо; вкл-копия перечисляет открывающиеся модули.
Переключение из карточки организации — отложено, список-страница
с бейджем и кнопкой закрывает UX.
pnpm run build — ✓ 2.02s, 0 TS errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Везде формула: simpleMode = me && !me.advancedMode && !SuperAdmin.
Значения скрытых полей в state сохраняются и при save на сервер
отправляются как есть; данные НЕ стираются.
- ProductEditPage: скрыты Артикул (auto-gen), Описание, Страна,
Эталонная цена, Ставка НДС, «В т.ч. НДС», Валюта закупки,
«Услуга», «Маркируемый», Максимальный остаток, не-retail типы
цен в блоке «Цены продажи».
- CounterpartiesPage: скрыты Юр.название, Страна, Email, Банк,
Р.счёт, БИК, Контактное лицо. Остаются: Название, Тип лица,
БИН/ИИН, Адрес, Телефон, Заметки.
- SupplyEditPage: колонка «Валюта» всегда скрыта в простом режиме
(одна валюта по умолчанию).
«Поставщик/Покупатель»-галочки отложены — требуют новых колонок
IsSupplier/IsBuyer на Counterparty (новая функциональность).
pnpm run build — ✓ 2.36s, 0 TS errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AdvancedModeGuard — render-guard: простой режим + не-SuperAdmin → страница
«Недоступно в простом режиме» с текстом + кнопкой «На главную».
SuperAdmin пропускается, чтобы можно было проверить работу после
переключения.
- useMe (+ локальные MeResponse) расширены полем advancedMode: boolean.
- AppLayout.buildNav(roles, advancedMode) — в простом режиме скрыты:
priceTypes, transfers, demands, promotions, loyaltyPrograms/Cards,
reportAbc, auditLog, LanguageSwitcher.
- App.tsx — скрытые маршруты обёрнуты в <AdvancedModeGuard>:
/catalog/price-types, /inventory/transfers*, /sales/demands*,
/loyalty/programs, /loyalty/cards, /promotions, /audit-log,
/reports/abc.
- DashboardPage — в простом режиме скрыты виджеты «Топ товаров» и
«Маржа». Остаются KPI, график продаж, «Последние продажи», «Низкий
остаток».
- Данные скрытых модулей НЕ удаляются при выключении режима; при
повторном включении всё видно.
pnpm run build — ✓ 2.6s, 0 TS errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- RequiresAdvancedModeAttribute (TypeFilterAttribute → IAsyncAuthorizationFilter):
читает Organization.IsAdvancedMode по org_id-claim через AppDbContext,
кэш 30 с, инвалидация после переключения. SuperAdmin bypass. На отказ
возвращает 403 ProblemDetails с code=advanced-mode-required.
- Атрибут навешен на контроллеры:
Sales/DemandsController, Inventory/TransfersController,
Loyalty/LoyaltyCardsController + LoyaltyProgramsController,
Promotions/PromotionsController, Reports/AbcReportController,
Admin/OrgAuditLogController, Telegram/TelegramBindingController,
Common/UserPresetsController.
- И на отдельные действия:
Catalog/PriceTypesController POST/PUT/DELETE (GET доступен — товары
используют retail-прайс),
Catalog/ProductsController POST /bulk-update.
- SuperAdmin toggle (шаг 2) инвалидирует кэш фильтра после SaveChangesAsync.
- SSO (Auth/ExternalAuthController) гейтится UI, а не атрибутом: endpoint
инициируется анонимом без org_id в токене.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PUT /api/super-admin/organizations/{id}/advanced-mode { enabled: bool }
— только SuperAdmin, админ орги → 403 от [Authorize(Roles=...)].
- Idempotent: совпадающее состояние → 200 без записи в аудит; разное →
запись SuperAdminAuditLog (EnableAdvancedMode / DisableAdvancedMode)
с before/after + IP. Архивную org переключать нельзя (409).
- /api/me теперь отдаёт advancedMode (bool) — читается из Organization
с IgnoreQueryFilters (SuperAdmin без org → false — работает через
/api/super-admin/...).
- OrgRow / OrgDetail в SuperAdmin list / detail дополнены IsAdvancedMode
для бейджа «Простой/Расширенный» (шаг 6).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Второе срабатывание того же external-network root cause'a (TLS-proxy
88.204.171.93 → 'fetch failed: other side closed' в Playwright). Patch v1
(incident-1780974301) suppress'ил инциденты inline в mark_red — order-
dependent: ui_flow (step #4) бежит ДО signalr (step #6) который
устанавливает NETWORK_DEGRADED, поэтому ui_flow incident создавался
ложно.
Patch v2 — process_incidents() в конце run'a + INCIDENT_QUEUE array.
Видит финальное NETWORK_DEGRADED после всех шагов, подавляет ВЕСЬ
queue если сеть деградировала. Также добавлены Node.js fetch паттерны
('fetch failed', 'other side closed') в network-detection grep.
Verified 3× run при flapping-сети: 4/8 red but 0 incident-файлов.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Watchdog за 08:05 на cron'е и 08:17 manual run флагнул multi_tenant +
products + signalr + ui_flow → создал 4 incident-файла. Воспроизведение
изнутри stage-VM (curl http://localhost:8085) даёт 5/5 → 200; снаружи
(https://test.admin.food-market.kz) — 50% HTTP 000 (Connection reset
by peer). Внешний TLS-терминатор 88.204.171.93 нестабилен.
Тот же 24.8% http_req_failed в 4h-soak (sprint27-progress) — same root.
Не баг food-market. Incident-файлы удалены, state reset.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Реальный 4-часовой soak (Sprint 28 overnight): 03:15 → 07:15.
iterations: 718482 @ 49.89/s (target 50)
api_mem: 250-300 MiB, без линейного роста ✓
pg_conn: 18-19 steady, no exhaust ✓
p95 latency: me=269ms / products=327ms / stats=328ms (steady)
http_req_failed: 24.8% — НЕ из-за API.
Внешний TLS-терминатор 88.204.171.93 (между dev-vm и stage) перодически
ронял соединения с 'unexpected EOF' / 'connection reset by peer'. На
внутренней сети stage'а (`docker exec curl localhost:8085`) — Healthy
всё время. ISP/Cloudflare-level ограничение на длительные RPS, не баг
food-market.
Артефакты:
tests/load/soak-runs/2026-06-09/summary.json
tests/load/soak-runs/2026-06-09/metrics.csv
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>