Commit graph

4 commits

Author SHA1 Message Date
nns 63da7a9334 perf(pos): мгновенный старт — gzip-каталог, дельта остатков, синк вне UI-потока
Касса работает из локальной базы сразу; вся синхронизация — в фоне, 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>
2026-10-09 04:59:01 +05:00
nns 01b47cc0c8 feat(pos): возврат по чеку — экран выбора позиций + защита от over-return
- Сервер: GET /api/pos/v1/sales/by-number/{number} — чек-основание с позициями
  и доступным к возврату (продано − уже возвращено, по QtyReturned). POS-путь
  возврата теперь валидирует over-return (нельзя вернуть больше остатка) и
  инкрементит QtyReturned на исходных строках (как RetailSalesController).
- Контракт: PosReturnLookupDto/PosReturnLineDto; IServerApi.ReturnLookupAsync.
- Касса: экран возврата — найти чек по номеру/скану QR, степпер количества по
  каждой позиции (0..доступно, кнопка «Всё»), сумма к выдаче, способ (нал/карта
  как в исходном чеке); CheckoutService.CommitReturnAsync пишет возврат в офлайн-
  очередь + печать + выгрузка.
- Тесты: integration — lookup by-number, возврат 2 из 3, over-return в Failed,
  довозврат 1, QtyReturned/остатки корректны; 404 на неизвестный номер. 11/11 PosSyncTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:16:48 +05:00
nns 606b81b4d9 feat(pos-api): привязка чеков кассы к смене/точке + возвраты + версия
- PosSaleDto: CashShiftId/RetailPointId/IsReturn/ReferenceSaleId (nullable,
  обратносовместимо с v1). PosController проставляет их в RetailSale, резолвит
  склад по точке продаж, проверяет что смена открыта. Без этого чеки кассы не
  попадали в Z-отчёт/материализацию денег смены (MoneyService агрегирует продажи
  по CashShiftId).
- Возврат покупателю (IsReturn): движение +остаток (CustomerReturn), без
  pre-flight на остаток; наличные к выдаче, учитывается в смене.
- GET /api/pos/v1/version (anon): версия + ссылки на сборки из конфигурации Pos
  (автообновление кассы + ссылка в админке).
- Интеграционные тесты: продажа в смене попадает в expected cash при закрытии;
  возврат возвращает остаток (9/9 PosSyncTests зелёные, Testcontainers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 04:58:05 +05:00
nns 640c8d9c22 feat(pos-api): GET /sync и POST /sales с двойной идемпотентностью (P1-12b)
Endpoints:
- GET /api/pos/v1/sync?since=ISO&storeId=Guid - выгрузка изменений
  (Products / Prices / Stocks / Counterparties) после reference time;
  Stocks - всегда полный снимок на момент ответа (POS нужен актуальный
  остаток на полке).
- POST /api/pos/v1/sales - батч продаж с idempotency.

Двойная идемпотентность:
1. Batch-level: PosBatchAck (новая таблица, unique idx по OrgId+Key) -
   повтор того же батча возвращает кешированный ответ. При параллельном
   race ловим 23505 на уникальном индексе и тоже возвращаем кеш.
2. Per-sale: ClientSaleId записывается в RetailSale.Notes как prefix
   "pos:GUID32". Перед созданием продажи проверяем что такой маркер ещё
   не встречался - если есть, возвращаем существующую продажу. Это
   спасает и при разных batch-ключах с пересекающимися ClientSaleId.

Pre-flight: проверка остатка ДО создания черновика - sale, которая не
влезает в полку, попадает в Failed, остальные в батче проводятся.

Domain: PosBatchAck (TenantEntity), миграция Phase7a_PosBatchAcks
(jsonb для ResponseJson, unique idx).

Контракты v1 из food-market.shared.

Тесты: 7 интеграционных - полная sync, дельта по since, POST батч
списывает stock, replay того же батча no-duplicates, ClientSaleId через
разные batch-keys тоже no-duplicates, недостача попадает в Failed,
tenant-изоляция.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:10:17 +05:00