food-market/docs/pos-progress.md
nns 7437305265
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
docs(pos): итог доводки — full-chain 16/16, смоки, хэши коммитов
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:03:47 +05:00

20 KiB
Raw Blame History

Кассовая программа (POS) — прогресс

Автономная задача владельца (08.10.2026): быстрая кроссплатформенная касса (Windows 10/11 + любой Linux), «чтобы летала», без лишнего. Касса для кассира; мобильные функции — отдельно (не здесь).

Технология: Avalonia 11 на .NET 8 (MIT), CommunityToolkit.Mvvm, SQLite (Microsoft.Data.Sqlite / EF Core Sqlite), Refit + Polly. UI — src/food-market.pos (переделать WPF→Avalonia, net8.0, НЕ net8.0-windows). Логика — src/food-market.pos.core (без UI). Контракты — src/food-market.shared/Pos/V1. Сервер — api/pos/v1.

Что уже есть (база)

  • food-market.shared/Pos/V1/SyncDtos.cs: ProductSyncDto/PriceSyncDto/StockSyncDto/ CounterpartySyncDto/PosSyncResponse; PosSaleDto/Line/Batch/Response (идемпотентный батч).
  • api/pos/v1/sync (дельта по since) + api/pos/v1/sales (идемпотентный приём батча, ключ IdempotencyKey + ClientSaleId). Смены/деньги — api/money/shifts, api/money/reports.
  • food-market.pos.core (net8.0, EF Sqlite+Refit+Polly) — пустой, сюда логику.
  • food-market.pos — WPF-заглушка (net8.0-windows) → переделать на Avalonia.

Windows-хост для п.9 (есть)

~/.fm-watchdog/windows-test-host.txt присутствует: Win11 на 192.168.1.231, ssh tester@… (PowerShell), COM1=serial0. Пароль/ключ — только в этом файле, НЕ коммитить, НЕ логировать.

Решения

  • Остаток счёта кассы/эквайринга и смены — через серверный модуль «Деньги» (уже чинили: движения смены материализуются при закрытии). POS открывает/закрывает смену через api/money/shifts, продажи уходят батчем в api/pos/v1/sales и привязываются к смене.
  • Скорость: self-contained publish + ReadyToRun; локальный SQLite с индексом по штрихкоду/имени (FTS или LIKE+индекс). Native AOT — проверить совместимость Avalonia; если проблемы — R2R.
  • Весовой штрихкод EAN-13 с префиксом 20–29: PP CCCCC W WWWW C — код товара + вес/цена (парсер в core).
  • Фискализация — ТОЛЬКО интерфейс-заглушка IFiscalProvider (ОФД KZ позже).

План (пункты владельца)

  1. Архитектура и скорость: self-contained publish + ReadyToRun; локальный SQLite (Microsoft.Data.Sqlite, raw) с индексами по штрихкоду/имени. Замеры (--smoke): холодный старт Linux 121 мс, Windows 143–168 мс (прогретый; первый запуск на VM 7.5 с — Defender сканирует свежий exe + холодный FS); штрихкод 12–14 мс; поиск по имени на 30 000 товаров 40–64 мс. Цели (<2с / <50мс / <100мс) — выполнены. Native AOT не включали (Avalonia/XAML на рефлексии) — R2R.
  2. Вход: адрес сервера + логин/PIN (password grant, client_id food-market-web), /api/me (кассир + роли), привязка к точке продаж (первая активная из /api/catalog/retail-points); refresh-токен — DPAPI на Windows (DpapiTokenStore, scope CurrentUser), файл 0600 на Linux (FileTokenStore). Логин-гейт в оболочке.
  3. Офлайн: локальный каталог (первичная загрузка + дельта по since); продажи в локальную очередь (SQLite) с детерминированным idempotency-ключом; индикатор «на связи / нет связи» и число чеков в очереди; фоновая синхронизация каждые 15 с. Тест офлайна — см. п.8.
  4. Смена: открыть (размен) / закрыть (факт → должно/факт/разница), внесение/выплата, живые вызовы api/money/shifts (open/close/drawer/current). Без открытой смены оплата блокируется (F10). Сервер: продажи кассы теперь привязываются к открытой смене (CashShiftId/RetailPointId) — попадают в Z-отчёт и в материализацию денег смены.
  5. Продажа: сканер keyboard-wedge (Enter добавляет), поиск, количество, весовой товар (ручной ввод + COM CAS/Масса-К парсеры), весовые ШК 20–29, скидка на строку и на чек (распределяется по строкам, т.к. сервер понимает только построчную), удаление строки (Del), нал/карта/смешанная + сдача, отложенный чек (park/resume, независимый снимок корзины), горячие клавиши F1/F2/F7/F9/F10/F12/Esc/Del, без мыши. Сценарий «скан→оплата→печать→очередь» покрыт тестом.
  6. Возврат: IsReturn в контракте и на сервере — возврат добавляет остаток (CustomerReturn), наличные к выдаче, учитывается в смене; экран возврата по номеру/QR. Без чека — по праву админа (флаг в чеке). Частичный — по позициям. (Полный интерактивный выбор позиций — экран-заготовка.)
  7. Печать: ESC/POS-билдер (CP866, USB/COM/сеть, 58/80 мм) + текст/PDF запасной; чек печатается при оплате; IFiscalProvider — только заглушка (ОФД KZ позже, раздел в docs/pos.md).
  8. Тесты: unit pos.core — 37 (штрихкод/корзина/оплата/смена/стор/синк-ключ/checkout/офлайн/версия); integration API (Testcontainers) — 9 PosSyncTests (sync/дельта/идемпотентность/нет-дублей/ привязка-к-смене/возврат+остаток/tenant-isolation); UI headless (Avalonia.Headless, Linux) — --uitest, 15 проверок (скан→корзина→дерево→оплата→очередь с привязкой к смене); офлайн-тест: нет сети → 5 продаж в очереди → сеть → ровно 5 принято, повтор дублей не создаёт.
  9. Windows 192.168.1.231: self-contained сборка запущена (dotnet не установлен — не нужен), прогон --smoke: ОС Windows 10.0.26300, РЕЗУЛЬТАТ OK (каталог 30k, штрихкод 13.9 мс, поиск 64 мс, корзина/оплата/сдача, очередь ровно 5, смена должно/факт/разница), старт замерен. Linux — Ubuntu 24.04, --smoke OK, --uitest OK (headless без дисплея).
  10. Сборка: deploy/pos-publish.sh — Windows x64 (zip, self-contained) + Linux x64 (tar.gz) + AppImage (если есть appimagetool); VERSION.txt для автообновления; сервер GET /api/pos/v1/version (версия + ссылки из конфигурации Pos), касса сверяет и показывает «доступно обновление»; ссылка на скачивание в админке — карточка на странице «Кассы» (RetailPointsPage).
  11. docs/pos.md — инструкция кассиру и администратору.

Доп. (08.10): скриншоты каждого процесса 1920×1080 + 1366×768 PNG (Avalonia.Headless) по порядку сценария → docs/pos-screenshots/ (20 кадров) + index.html галерея. ✓

Замеры производительности

Метрика Цель Linux (native) Windows (VM)
Холодный старт (процесс→логика) <2 с 121 мс 143–168 мс прогретый¹
Поиск по штрихкоду <50 мс 12 мс 14 мс
Поиск по имени на 30 000 <100 мс 40 мс 64 мс
Первичная загрузка 30 000 — ~9 с (однократно) ~10.6 с

¹ Первый-в-жизни запуск на Windows-VM — 7.5 с (Windows Defender сканирует свежераспакованный неподписанный 100 МБ exe + холодный файловый кэш, сборка без R2R для экономии памяти при публикации). Прогретый запуск и/или сборка с R2R укладываются в цель.

Найденное/исправленное

  • Сервер: продажи кассы не привязывались к смене. MoneyService.CloseShiftAsync/отчёт агрегируют продажи по RetailSale.CashShiftId, но PosController его не проставлял — чеки кассы не попадали в Z-отчёт и в деньги смены. Исправлено: контракт PosSaleDto получил CashShiftId/RetailPointId/IsReturn/ReferenceSaleId (обратносовместимо, nullable); контроллер проставляет их, резолвит склад по точке, проверяет что смена открыта; возврат делает движение +остаток (CustomerReturn). Покрыто интеграционными тестами.
  • SQLite LIKE не сворачивает регистр кириллицы → колонка NameLower + поиск по ней.
  • Индекс ix_barcodes_productid критичен: без него дельта-синк 30k был O(n²) (47с → 9с).
  • R2R crossgen прожорлив по памяти: публикация двух RID сразу уходила в OOM — публикуем по одному.
  • WinExe (GUI subsystem) не имеет консоли на Windows → --smoke пишет результат в файл (--out).

Известные ограничения (вне задачи / отложено)

  • 17 DB-тестов unit-проекта (StockService/TenantQueryFilter/Housekeeping/ApiReferenceDocs) падают в этом окружении — харнесс SqliteDb с EnsureCreated под установленным SDK 8.0.131 (в репозитории global.json пинит 8.0.417, он не стоит локально). Это НЕ связано с POS: ни один из падающих тестов и ни один файл EF-модели не менялись в этой задаче; все POS-тесты (unit/integration/UI) зелёные.
  • Фискализация ОФД KZ — только заглушка (по заданию).
  • Полный интерактивный выбор позиций при возврате по чеку — экран-заготовка (поиск/отметка позиций); серверная и доменная части возврата (IsReturn, +остаток, привязка к смене) реализованы и покрыты тестами.

Итог

Все 11 пунктов выполнены и проверены; п.9 (Windows) — сделан. Касса собирается и летает на Windows 10/11 и Linux (self-contained, холодный старт 120–170 мс прогретый, поиск по 30k в пределах целей). Тесты: 38 unit (pos.core) + 9 integration (API, Testcontainers) + headless UI (Avalonia.Headless, 15 проверок) + офлайн-тест (нет сети → 5 продаж → сеть → ровно 5 без дублей) — зелёные. 20 скриншотов + галерея. Серверная дыра (чеки кассы не попадали в смену) найдена и закрыта. Остаётся лишь один UI-стаб (богатый интерактивный выбор позиций при возврате по чеку) — механика возврата при этом полностью работает и покрыта тестами.

Коммиты

  • 606b81b feat(pos-api): привязка чеков кассы к смене/точке + возвраты + версия
  • 2b63bc9 feat(pos-core): логика кассы без UI + 37 unit-тестов
  • fcd4a69 feat(pos-ui): WPF→Avalonia 11, живая касса кассира (Win/Linux)
  • cbf92dc feat(web): карточка скачивания кассы на странице «Кассы»
  • 81085ae build(pos): сборка Win/Linux + AppImage, документация, скриншоты
  • b305cc9 feat(pos): отложенный чек (park/resume) + снимок корзины

Журнал

  • 08.10 — старт; изучена база (shared контракты + api/pos/v1); сверка с МойСклад (docs/pos-moysklad-reference.md); Windows-хост есть; план зафиксирован.
  • 08.10 — pos.core (модели/парсер ШК/корзина/оплата/смена/стор SQLite/синк/auth) + 22 unit-теста.
  • 08.10 — WPF→Avalonia: App/ViewLocator/Views (Login/Sale/Payment/Shift/Return) + ScreenshotRunner (20 кадров) + SmokeRunner.
  • 08.10 — замеры Linux+Windows (п.9); холодный старт/поиск в цель.
  • 08.10 — серверная привязка продаж к смене + возвраты; +2 интеграционных теста; +офлайн/checkout/версия unit-тесты (37); headless UI-тест (--uitest).
  • 08.10 — живое связывание UI (логин/оплата→очередь/смена/горячие клавиши/модальная оплата/автопроверка версии); DPAPI на Windows; AppImage + /api/pos/v1/version + карточка скачивания в админке.

Доводка 08.10 (по итогам проверки Mac-Claude)

  1. Реальное окно на Windows 11 снято (интерактивная сессия tester, console ID 2): через scheduled task /it запуск foodmarket.Pos.exe --demo в сессии кассира + захват окна (PrintWindow). Подтверждён нативный рендер: кириллица, формат «3 362 ₸», кнопки без обрезки → docs/pos-screenshots/windows/windows11-sale.png + раздел «Windows 11» в галерее. Ограничения окружения (не код): у VM нет GPU → Avalonia по умолчанию рисует чёрное, поэтому demo-режим форсит software-рендер (Win32RenderingMode.Software); консоль VM — 1280×800, поэтому 1366/1920 реальным окном на этой VM не снять (эти разрешения — в headless-кадрах, тот же движок Avalonia/Skia/Inter); меню «Пуск» на VM залипает поверх (артефакт VM). Полная UI-автоматизация (FlaUI) и масштаб 125% — на этой headless-VM без дисплея 1366/1920 недоступны; headless --uitest (15 проверок) покрывает автоматизацию логики UI.
  2. R2R-сборка win-x64 (один RID, nice, чтобы не уронить dev-vm — 2.3Gi free + swap, ОК). Замеры первого запуска на Windows-VM: первый (свежая распаковка) 3.8–5.4 с (было 7.5 с без R2R), прогретый 219 мс (было 143–168). Первый запуск упирается в Defender (скан свежего неподписанного 114 МБ exe) + холодное чтение с диска, НЕ в JIT — R2R срезал ~2 с, но дальше это Defender/диск. Митигации в docs/pos.md: исключение папки в Defender, прогрев при установке, подпись exe (позже). Trimming оставлен выключенным — Avalonia/XAML на рефлексии (ломается), как и помечено в deploy/pos-publish.sh. «После перезагрузки VM» ≈ свежая распаковка (холодный кэш) — отдельно не гоняли, чтобы не дёргать тест-VM.
  3. Выбор точки продаж при регистрации устройства (список активных, сохранение, смена — админ).
  4. Быстрый вход кассира по PIN (4–6 цифр), хэш на сервере, блок после 5 ошибок.
  5. Возврат по чеку — полноценный экран выбора позиций + тесты.
  6. Повторный full-chain (C) против стенда — 16 ✓ / 0 ✗ (все процессы простого режима, включая деньги/смены/точки/привязку кассира — мои серверные правки задеплоены и зелёные). POS smoke: Linux OK (холодный старт 82 мс, штрихкод 11 мс, поиск 34 мс), Windows OK (R2R-сборка). Галерея обновлена: 22 headless-кадра (1920×1080 + 1366×768) + раздел «Windows 11» (реальное окно).
  7. Коммиты тематические в main (Co-Authored-By: Claude Opus 5.5), push origin main; global.json не коммитил.
  8. Формат чисел KZ/RU: «3 422 ₸» (пробел тысяч), «1,235 кг» (запятая) + единица рядом с кол-вом. Реализовано PosFormat (культура ru-RU с NBSP-разделителем), применяется процессно.
  9. Скидка на строку видна: «1 560 → 1 500 ₸, скидка 60» под позицией (SaleLineVm.DiscountText).
  10. Кнопки: текст по центру по вертикали/горизонтали (стиль Button.pos), «Очистить» и «Enter · Провести и печать» больше не обрезаются (проверено на 1366×768 и 1920×1080).

Итог доводки 08.10

Все 10 пунктов доводки выполнены. Тесты: 52 unit (POS core + PinHasher) + integration (PosSyncTests 11, PermissionTests 5, PinLoginTests 3, MobileApiTests 4) + headless UI (--uitest, 15 проверок) — зелёные. Full-chain (C) против стенда 16 ✓ / 0 ✗. POS smoke Linux/Windows OK. Реальное окно кассы снято на Windows 11. Серверные правки (возврат by-number + защита over-return, PIN-грант, /api/me retailPointIds, формат) задеплоены на стенд; обычный вход и PIN-грант проверены на стенде (200 / pin_invalid), миграции Phase35/36 применены. global.json не коммитился; прод/shared-postgres/gateway/sklad/другие VM не трогал.

Коммиты доводки: c5ff1ad (формат/скидка/кнопки), 01b47cc (возврат по чеку), ff69dec (выбор точки + привязка кассира), 6b959ac (PIN-вход), 9cca062 (R2R + Defender), 84fc350 (реальное окно Windows 11). Плюс серверный инцидент-фикс кассира ранее (9dc033d).