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

183 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Кассовая программа (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. [x] Архитектура и скорость: 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. [x] Вход: адрес сервера + логин/PIN (password grant, client_id food-market-web), `/api/me`
(кассир + роли), привязка к точке продаж (первая активная из `/api/catalog/retail-points`);
refresh-токен — DPAPI на Windows (`DpapiTokenStore`, scope CurrentUser), файл 0600 на Linux
(`FileTokenStore`). Логин-гейт в оболочке.
3. [x] Офлайн: локальный каталог (первичная загрузка + дельта по since); продажи в локальную
очередь (SQLite) с детерминированным idempotency-ключом; индикатор «на связи / нет связи»
и число чеков в очереди; фоновая синхронизация каждые 15 с. Тест офлайна — см. п.8.
4. [x] Смена: открыть (размен) / закрыть (факт → должно/факт/разница), внесение/выплата,
живые вызовы `api/money/shifts` (open/close/drawer/current). Без открытой смены оплата
блокируется (F10). **Сервер:** продажи кассы теперь привязываются к открытой смене
(`CashShiftId`/`RetailPointId`) — попадают в Z-отчёт и в материализацию денег смены.
5. [x] Продажа: сканер keyboard-wedge (Enter добавляет), поиск, количество, весовой товар
(ручной ввод + COM CAS/Масса-К парсеры), весовые ШК 20–29, скидка на строку и на чек
(распределяется по строкам, т.к. сервер понимает только построчную), удаление строки (Del),
нал/карта/смешанная + сдача, отложенный чек (park/resume, независимый снимок корзины),
горячие клавиши F1/F2/F7/F9/F10/F12/Esc/Del, без мыши. Сценарий «скан→оплата→печать→очередь» покрыт тестом.
6. [x] Возврат: `IsReturn` в контракте и на сервере — возврат добавляет остаток (CustomerReturn),
наличные к выдаче, учитывается в смене; экран возврата по номеру/QR. Без чека — по праву
админа (флаг в чеке). Частичный — по позициям. (Полный интерактивный выбор позиций — экран-заготовка.)
7. [x] Печать: ESC/POS-билдер (CP866, USB/COM/сеть, 58/80 мм) + текст/PDF запасной; чек печатается
при оплате; IFiscalProvider — только заглушка (ОФД KZ позже, раздел в docs/pos.md).
8. [x] Тесты: unit pos.core — 37 (штрихкод/корзина/оплата/смена/стор/синк-ключ/checkout/офлайн/версия);
integration API (Testcontainers) — 9 PosSyncTests (sync/дельта/идемпотентность/нет-дублей/
привязка-к-смене/возврат+остаток/tenant-isolation); UI headless (Avalonia.Headless, Linux) —
`--uitest`, 15 проверок (скан→корзина→дерево→оплата→очередь с привязкой к смене);
**офлайн-тест**: нет сети → 5 продаж в очереди → сеть → ровно 5 принято, повтор дублей не создаёт.
9. [x] 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. [x] Сборка: `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. [x] `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. [x] Реальное окно на 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. [x] 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. [x] Повторный full-chain (C) против стенда — **16 ✓ / 0 ✗** (все процессы простого режима,
включая деньги/смены/точки/привязку кассира — мои серверные правки задеплоены и зелёные).
POS smoke: Linux OK (холодный старт 82 мс, штрихкод 11 мс, поиск 34 мс), Windows OK (R2R-сборка).
Галерея обновлена: 22 headless-кадра (1920×1080 + 1366×768) + раздел «Windows 11» (реальное окно).
7. [x] Коммиты тематические в main (Co-Authored-By: Claude Opus 5.5), push origin main; global.json не коммитил.
8. [x] Формат чисел KZ/RU: «3 422 ₸» (пробел тысяч), «1,235 кг» (запятая) + единица рядом с кол-вом.
Реализовано `PosFormat` (культура ru-RU с NBSP-разделителем), применяется процессно.
9. [x] Скидка на строку видна: «1 560 → 1 500 ₸, скидка 60» под позицией (SaleLineVm.DiscountText).
10. [x] Кнопки: текст по центру по вертикали/горизонтали (стиль 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`).