Commit graph

370 commits

Author SHA1 Message Date
nns 64cc5b0d10 test(ui-deep): items 2-3 — navigation + Products CRUD
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Item 2 (4 specs): 27 sidebar-страниц последовательно открываются без
console-errors и без 5xx. Sidebar labels + active state проверены.

Item 3 (5 specs): Products full CRUD через UI — create+edit+delete с
ConfirmDialog, дубль артикула с понятным toast'ом, поиск, пагинация при
>50 товаров, загрузка картинки через setInputFiles.

watcher: фильтрует Chromium auto-сообщения «Failed to load resource: the
server responded with a status of N» — дубли network-обработчика.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 12:52:10 +05:00
nns 3cdb819331 fix(catalog): уберём cache-touch после Delete — просто navigate
Предыдущий фикс с qc.removeQueries({queryKey:['/api/catalog/products', id]})
+ invalidateQueries(exact:true) — оказался не до конца верным:

1) removeQueries на ещё-mounted ProductEditPage с активной подпиской на этот
key триггерит refetch (TanStack заполняет пустой cache на active subscriber).

2) invalidateQueries({queryKey:['/api/catalog/products'], exact:true}) на
самом деле не матчит ни list (ключ имеет 6 элементов с пагинацией), ни item
(ключ из 2 элементов) — exact=true ищет ровно [...] из 1 элемента.

Правильно: просто navigate('/catalog/products'). React Query refetchOnMount
сам обновит list при заходе на ProductsPage (staleTime=0 default).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 12:45:56 +05:00
nns 61ca7fee90 fix(catalog): после Delete не refetch'аем удалённый товар
Найдено через UI-deep: после Удалить ProductEditPage делал
qc.invalidateQueries({queryKey:['/api/catalog/products']}) до navigate'a.
React Query refetch'ил конкретно ['/api/catalog/products', id] (тот что
живёт на этой же странице) → 404 → axios interceptor показывал toast
«Не найдено» поверх редиректа на список.

Фикс: сначала navigate('/catalog/products'), потом
qc.removeQueries для item-кеша + invalidate список с exact=true чтобы не
матчить вложенный item-key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 12:39:39 +05:00
nns eb867697d0 test(ui-deep): setup + Item 1 — signup flow (5 specs)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Sprint UI-deep, пункт 1: реальный Chromium через Playwright Test.
Установлены @playwright/test 1.60.0 и otplib (для item 11).
Конфиг tests/e2e/playwright.config.ts — workers=1, traces+screenshots
on-failure, screenshot dir reports/playwright-artifacts/.

Хелперы tests/e2e/lib/ui.ts:
- apiSignup() — быстрый signup через API + login
- attachSession() — кладёт access_token в localStorage, грузит путь
- watchPage() — listener console-errors и network 4xx/5xx
- expectNoErrors() — assert после flow'a

Item 1 (5 specs, все ✓ на стейдже):
- 1.1 attach session → /dashboard, без console-ошибок
- 1.2 создание товара через UI (Empty CTA → форма → Сохранить)
- 1.3 первый контрагент через Modal
- 1.4 создать товар + контрагент через API, открыть форму приёмки,
       smoke на компоненты страницы
- 1.5 OnboardingPage (/) рендерится

Найден 1 реальный баг → починен:
- ProductEditPage: race на currencies.data — если быстро Сохранить,
  цена-MoneyInput добавляет строку с currencyId='' → server 400 с
  криптичным JSON validation. Фикс: MoneyInput disabled пока
  !currencies.data + canSave проверяет row.currencyId.
- Form error display показывал "Request failed with status code 400";
  теперь использует общий humanizeError() (exporting из @/lib/api).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 12:33:10 +05:00
nns cee92d86ce fix(catalog): ProductEditPage — race на currencies.data + читаемая ошибка
Найдено через UI-deep тестирование (Playwright):

Баг 1: race-condition. Если юзер быстро кликает Сохранить до того как
прогрузился справочник currencies, цена-MoneyInput добавляет строку с
currencyId='' (фолбэк `?? ''`). Сервер возвращает 400 с криптичным
JSON-validation: "$.prices[0].currencyId" не парсится.

Фикс: MoneyInput для цен disabled пока !currencies.data; вместо фолбэка
'' возвращаемся из onChange (no-op). canSave дополнительно проверяет
row.currencyId — двойная страховка.

Баг 2: при ошибке сохранения page показывал "Request failed with status
code 400" — generic axios message. Toast при этом показывал
человеко-читаемый текст через humanizeError (api interceptor).

Фикс: exporting humanizeError из @/lib/api, ProductEditPage onError
использует тот же helper. Теперь form-level error == toast-сообщение.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 12:27:07 +05:00
nns 1418c79b04 fix(a11y): Modal — role=dialog + aria-modal + aria-label на крестике
Найдено через UI-deep тестирование: Modal не имел ARIA-роли, screen
reader не определял его как диалог. Также добавил aria-label='Закрыть'
на X-кнопку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 12:18:16 +05:00
nns 64af42167b docs(sprint7): пункты 6-7 ✓ + итог по спринту
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Все 7 пунктов закрыты. Stage прошёл smoke-тест 5/5 после последнего деплоя.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:52:00 +05:00
nns c2ebbcc1bd fix(web): useShortcuts — бэр-клавиши не зависят от Shift
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
'?' на US-раскладке вводится через Shift+/, поэтому при нажатии e.shiftKey=true.
Старая логика требовала wantShift === e.shiftKey и блокировала '?' (wantShift=false).
Теперь для одиночных клавиш (без '+' в spec) сравниваем только e.key — это правильно
и для '/', и для '?', и для 'n', и не ломает 'mod+s'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:47:09 +05:00
nns 76cbe78257 feat(web): keyboard shortcuts на edit + list страницах + «?» overlay
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Item 7 Sprint 7 — финальный пункт.

Хук: src/lib/useShortcuts.ts — поддерживает 'mod+s' (Ctrl/Cmd), Escape,
одиночные клавиши ('/', 'n', '?'). Бэр-клавиши скипают input/textarea/
contenteditable чтобы не ломать ввод. preventDefault() автоматически,
второй параметр enabled=true для конфликтов с диалогами.

Edit-страницы (9: Product + 8 doc-edit):
- mod+s = save (через canSave/canSubmit и save.isPending)
- Escape = navigate(<list-path>)
- enabled = !dialogProps.open — чтобы Esc не пересекался с ConfirmDialog
  (иначе Esc бы и закрыл диалог, и навигировал на список).

List-страницы (10: Products + 9 doc-list):
- '/' = searchRef.current?.focus()
- 'n' = navigate('/<entity>/new')
  (на CounterpartiesPage — открыть create-modal, т.к. там нет роута)
- SearchBar переведён на forwardRef для ref-проброса в input.

«?»-Overlay: src/components/ShortcutsOverlay.tsx — глобальный модал со
шпаргалкой. Открывается '?', закрывается Esc или кликом снаружи.
Смонтирован в AppLayout один раз.

tsc clean. На стейдже задеплоено.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:41:54 +05:00
nns 821bc4ed8d feat(web): Breadcrumbs на edit-страницах (Каталог / Товары / Молоко 3.2%)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Item 6 Sprint 7 — реюзабельный <Breadcrumbs items={...}> над h1 на 9 edit-pages.

Компонент: src/components/Breadcrumbs.tsx — Lucide ChevronRight как
разделитель, последний item — текущий (semibold темнее, без линка), не-
последние — кликабельные Link'и react-router (если задан to). Truncate с
title-tooltip для длинных названий.

Применено:
- ProductEditPage: Каталог / Товары / <name|Новый товар>
- SupplyEditPage: Закупки / Приёмки / <number|Новая приёмка>
- EnterEditPage / LossEditPage / TransferEditPage / InventoryEditPage:
  Остатки / <тип> / <number>
- SupplierReturnEditPage: Закупки / Возвраты поставщикам / <number>
- DemandEditPage: Продажи / Оптовые отгрузки / <number>
- RetailSaleEditPage: Продажи / Чеки / <number>

Side-effect: на doc-edit pages убрана дублирующая subtitle «Черновик —
товар не списан, пока не проведёшь» (breadcrumbs дают контекст). Зелёная
плашка «Проведён <дата>» сохранилась — она несёт реальную инфу.

tsc clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:25:32 +05:00
nns 6fc74f8db6 docs(sprint7): пункт 5 ✓ + empty-state screenshot
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:21:30 +05:00
nns 8d532927e2 feat(web): Empty states с CTA на list-страницах
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Item 5 Sprint 7 — заменил сухое «Нет данных» в DataTable на дружелюбный
центрированный блок: иконка + заголовок + объяснение + CTA «Создать первый
…». Показывается только когда нет поиска/фильтров (truly fresh org), иначе
обычный fallback DataTable.empty.

Компонент: src/components/EmptyState.tsx — Lucide-иконка, optional
actionLabel + onAction, optional secondaryLabel + onSecondary.

Применено (14 страниц):
- Catalog: Products (Package → /catalog/products/new), Counterparties
  (Users → открыть create-modal через setForm)
- Inventory: Enters (PackagePlus), Losses (Trash2),
  Transfers (ArrowLeftRight), Inventories (ClipboardList)
- Purchases: Supplies (PackagePlus), SupplierReturns (Undo2)
- Sales: Demands (Truck), RetailSales (Receipt)
- Reports: Sales (BarChart3), Stock (Warehouse), Profit (TrendingUp),
  Abc (BarChart3) — без CTA, текст «отчёт построится когда…»

Тексты — конкретные («Списания фиксируют выбытие — просрочка, брак»),
не «нажмите чтобы создать».

tsc clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:16:11 +05:00
nns cd83269d3a docs(sprint7): пункт 4 ✓ + skeleton screenshot
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:07:54 +05:00
nns faa13521e8 feat(web): loading skeletons вместо «Загрузка…» в DataTable + edit-pages
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Item 4 Sprint 7 — shimmer-плейсхолдеры вместо текстовых лоадеров.

Компоненты (src/components/Skeleton.tsx):
- <Skeleton variant='line'|'block'|'circle' /> — базовый pulse-блок.
- <TableSkeleton rows cols /> — 8 строк × N колонок с псевдослучайной
  шириной плейсхолдеров, чтобы превью таблицы выглядело естественно.
- <FormSkeleton /> — заголовок + 2 секции по 6 полей.

DataTable: при isLoading=true теперь рендерит TableSkeleton (а не
«Загрузка…»). На list-страницах layout остаётся стабильным.

Edit-pages: добавил guard
  if (!isNew && existing.isLoading) return <FormSkeleton />
на 9 doc-edit pages (ProductEdit, DemandEdit, EnterEdit, InventoryEdit,
LossEdit, SupplierReturnEdit, TransferEdit, SupplyEdit, RetailSaleEdit) +
OrganizationSettingsPage. До этого они показывали пустые поля formы или
«Загрузка…».

DashboardPage: график выручки во время загрузки теперь Skeleton block
72rem высоты (вместо текста «Загрузка…»).

tsc clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:03:08 +05:00
nns 56dd9fb639 docs(sprint7): пункт 3 ✓ + toast screenshot script
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:59:06 +05:00
nns 27ce8dddfc feat(web): toast-система — error на 4xx/5xx + success на мутации (через meta)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Item 3 Sprint 7 — заменил молчаливый rej в src/lib/api.ts на toast.error
для всех 4xx/5xx (кроме 401, где идёт auto-refresh). Тосты успеха —
для мутаций через meta.successMessage, чтобы избежать спама на queries.

Компоненты:
- src/lib/toast.ts — мин singleton API (toast.success/error/info), без deps.
  Дедуп подряд идущих одинаковых сообщений. Autoclose через setTimeout.
- src/components/Toaster.tsx — фиксированный top-right контейнер. На мобиле
  растягивается до экрана с margin. Кнопка X для ручного закрытия.
- src/lib/api.ts — interceptor 4xx/5xx: humanizeError() читает
  ProblemDetails (errors.X[0] → detail → message → title); title по статусу
  («Нет доступа» / «Не найдено» / «Конфликт» / «Проверьте поля» / «Слишком
  много запросов» / «Ошибка сервера»). Опт-аут через config.__silent=true.

Глобальный mutation onSuccess (App.tsx) подтягивает meta.successMessage и
показывает toast. meta.successMessage=false → опт-аут.

Применено (через meta):
- useCatalogMutations: create=«Создано», update=«Сохранено», remove=«Удалено»
  (автоматически для всех list-pages: Counterparties, Stores, Countries,
  PriceTypes, ProductGroups, RetailPoints, EmployeeRoles, ...)
- Doc-edit pages (Demand/Enter/Inventory/Loss/SupplierReturn/Transfer/Supply/
  RetailSale): save=«Сохранено», post=«Проведено», unpost=«Снято с
  проведения», remove=«Удалено».
- ProductEditPage: save=«Сохранено», remove=«Удалено».
- OrganizationSettingsPage save: «Настройки сохранены».

tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:54:14 +05:00
nns c201625b2b docs(sprint7): пункт 2 ✓ + screenshot script
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:46:52 +05:00
nns 17a6da2f8b feat(web): ConfirmDialog компонент + useConfirm hook вместо window.confirm()
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Item 2 Sprint 7 — заменил все нативные confirm() в фронте на собственный
<ConfirmDialog> с понятной типографикой, Esc=cancel, focus-on-Cancel
(чтобы случайный Enter не подтверждал удаление), tone='danger' | 'warning'.

Компоненты:
- src/components/ConfirmDialog.tsx — UI поверх Modal-overlay, AlertTriangle
  иконка, primary/danger кнопки. Текст description конкретный («Удалить
  товар «Молоко 3.2%»? Действие необратимо»). aria-labelledby выставлен.
- src/lib/useConfirm.ts — хук-обёртка: const { confirm, dialogProps } =
  useConfirm(); if (await confirm({...})) action(). Возвращает Promise<bool>.

Button: переведён на forwardRef, чтобы dialog мог поставить фокус на Cancel.

Применено (17 страниц + 1 компонент):
- ProductEditPage (delete product)
- DemandEditPage / EnterEditPage / InventoryEditPage / LossEditPage /
  SupplierReturnEditPage / TransferEditPage / SupplyEditPage / RetailSaleEditPage:
  delete draft + post + unpost (всего 3 диалога на форму)
- EmployeesPage: уволить (warning) / удалить навсегда (danger), сохранена
  динамика по статусу
- CounterpartiesPage / StoresPage / ProductGroupsPage / RetailPointsPage /
  CountriesPage / PriceTypesPage / EmployeeRolesPage / SuperAdminUnitsOfMeasurePage:
  delete с именем сущности в description
- ProductImageGallery: delete image

tsc --noEmit: clean. Текстов: в описаниях есть имена сущностей (товар,
номер документа), чтобы было ясно что именно удаляется.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:38:31 +05:00
nns 26959d56d1 docs(sprint7): пункт 1 ✓ (demo-seeder)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:22:06 +05:00
nns ad09b56013 feat(stage): demo-data seeder для test.admin.food-market.kz
Some checks failed
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
Item 1 Sprint 7 — кнопка «Заполнить демо-данными» в OrganizationSettingsPage.

Что заполняет (за одну транзакцию, ~3с на стейдже):
- 5 групп товаров (Молочные / Хлеб / Напитки / Бакалея / Снеки)
- 50 товаров с барштрихкодами EAN-13 + retail-ценой (article DEMO-NN-MM)
- 10 контрагентов (5 поставщиков + 5 покупателей-юрлиц с BIN)
- Второй склад «Резерв» (если нет) для transfer'a
- 5 приёмок (Posted) за последние 30 дней с moving-average cost
- 30 розничных продаж (Posted) за последний месяц, Cash/Card случайно
- 1 опт-отгрузка (Demand, Posted) с 15% скидкой
- 1 списание (Loss, Posted, причина Expired)
- 1 перемещение (Transfer, Posted) между складами
- 1 инвентаризация (Posted) с небольшим diff +/- 1

Идемпотентность: маркер — наличие Product с Article startsWith "DEMO-".
Повторный POST → возвращает summary без вставок.

API:
- GET /api/admin/seed-demo/status — счётчики (Admin policy)
- POST /api/admin/seed-demo — запустить (Admin policy)

UI: OrganizationSettingsPage.tsx, секция «Демо-данные» с Sparkles-иконкой,
counts grid и кнопкой (disabled когда уже заполнено).

Тесты: tests/e2e/scenarios/stage-demo-seed (5/5 ✓ локально).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:17:49 +05:00
nns d89d6bf1dc docs(stage): итоговый отчёт — все 14 пунктов ✓ (94/94 шагов зелёные)
13 stage-сценариев, 94 шага — все green. 6 фиксов в проде:
- EF8 nav-collection (6 контроллеров)
- UNIQUE Article на products
- DateTime Kind=Unspecified→UTC (reports + audit-log)
- Enter.Post → Product.Cost (moving average)
- ABC Pareto по cumBefore
- Swagger operationIds+schemaIds

3 logic gap'a зафиксировано (не P0): stage-public→прод admin,
Enter/Loss без RowVersion, нет CSV-импорта Inventory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:59:04 +05:00
nns a0b985178b test(stage): пункт 14 — POS Sync API 7/7 ✓ (sync + sales с idempotency)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
GET /api/pos/v1/sync — full snapshot products/prices/stocks/counterparties
с serverTime; since-инкремент работает (products пусто после first sync).
POST /api/pos/v1/sales с idempotency:
- batch-level: повтор того же IdempotencyKey → replayedFromCache=true,
  stock не дублирует списание;
- per-sale: новый IdempotencyKey + тот же ClientSaleId → возвращает
  существующий ServerSaleId (маркер в Notes);
- qty > stock → failed-секция с error, accepted=0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:53:08 +05:00
nns 466595b4d5 fix(swagger): operationId + schemaId — генерация OpenAPI работает
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
В Development swagger.json валился двумя ошибками:

1. CustomOperationIds dereferencing api.ActionDescriptor.RouteValues['action']
   для минимальных API (/health, /metrics, /connect/*) кидало
   KeyNotFoundException. Делаем TryGetValue + fallback на RelativePath.

2. CustomSchemaIds с FullName! падал NRE на типах без FullName
   (generic-параметры). Fallback на t.Name через ??.

После фикса: /swagger/v1/swagger.json 200, 117 paths, все 19 новых
модулей (Enter/Loss/Transfer/Inventory/SupplierReturn/Demand/Reports/
AuditLog/2FA/POS/Signup) присутствуют, schemaId без дубликатов.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:51:23 +05:00
nns 6b6f27d238 test(stage): пункт 12 — 2FA TOTP 6/6 ✓ (enroll+verify+login flow+disable)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
TOTP-коды генерируются локально (RFC 6238 / Base32 + HMAC-SHA1)
из sharedKey. End-to-end: signup → enroll → verify (invalid+valid) →
login с otp_code (required+invalid+success) → disable → reset key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:41:22 +05:00
nns 6a5bb52b13 test(stage): пункт 11 — OrgAuditLog 7/7 ✓ + UTC fix
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
CRUD продукта генерирует записи create/update/delete с diff'ом
полей; фильтры по entityType/entityId/action работают; multi-tenant
строго (org B не видит логи org A).

Bonus fix: тот же DateTime Kind=Unspecified→UTC что в reports,
применён к from/to в /api/admin/audit-log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:39:54 +05:00
nns 97d5ae5eb0 fix(reports): 3 фикса по итогам stage-тестирования
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
1. **DateTime Kind=Unspecified → UTC** в ResolveRange / AsUtc.
   ASP.NET парсит 'from=2026-05-29' с Kind=Unspecified, Npgsql 8
   отказывается слать такие в timestamp with time zone (500).
   Принудительно конвертим Unspecified→UTC (трактуем как полночь
   UTC), Local→ToUniversalTime. Применено к Sales/Profit/ABC/Stock.

2. **Enter.Post теперь пересчитывает Product.Cost** по той же
   формуле скользящего среднего что Supply.Post. Без этого товары,
   попавшие в систему через Оприходование (а не через Supply),
   имели Cost=0 — Profit/ABC-отчёты показывали cost=0 и неверную
   маржу. Воспроизведение: Enter 100@30 + RetailSale 10@500 →
   Profit-отчёт показывал revenue=5000, cost=0 (должно cost=300).

3. **ABC report: Парето-граница по cumBefore (а не cumAfter).**
   Единственный товар с cumShare=100% валился в класс C, хотя
   полностью покрывает Парето — должен быть A. Чиним: товар
   принадлежит классу A если он нужен чтобы пересечь порог
   80% (cumBefore < 80%). Стандартный Парето-алгоритм.

stage-reports (8 шагов): Sales/Stock/Profit/ABC + CSV/XLSX
export + edge — все зелёные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:35:31 +05:00
nns 475c5ca674 test(stage): пункт 9 — Demand 8/8 ✓ (Cash + Credit + post + multi-tenant)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:23:03 +05:00
nns 74e14ebeb5 test(stage): пункт 8 — SupplierReturn 8/8 ✓ (CRUD+Post+Unpost+ref validation+multi-tenant)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:21:30 +05:00
nns 9df8e0123e test(stage): пункт 7 — CustomerReturn 6/6 ✓ (создание из чека+walk-in+overreturn+multi-tenant)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:10:08 +05:00
nns 7d69006a94 test(stage): пункт 6 — Inventory 8/8 ✓ + logic gap по CSV-импорту
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Auto-populate (lines=null), explicit lines с пересчётом diff, PUT с
изменением actualQty (фикс EF8 nav-collection теперь работает),
Post → корректирующие StockMovement type=InventoryAdjustment, Unpost,
multi-tenant. + Информационный gap: нет CSV-импорта фактических qty,
для оператора склада ввод через JSON-API неудобен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:05:28 +05:00
nns 24c3ff1635 test(stage): пункт 5 — Transfer 7/7 ✓ (CRUD+atomic post+unpost+multi-tenant)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:01:48 +05:00
nns d246354c20 test(stage): пункт 4 — Loss 8/8 ✓ (CRUD+Post+Unpost+multi-tenant)
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 16:59:56 +05:00
nns 96e0d84f86 docs(stage): пункт 3 done — Enter зелёный, EF8 фикс на 5 контроллеров 2026-05-29 16:57:59 +05:00
nns 4e15359378 fix(docs): EF8 nav-collection bug в Enters/Losses/Transfers/SupplierReturns/Inventories.Update
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Тот же баг что в TD-6 чинили на Supplies/Demands/RetailSales и в pt 2
на Products: добавление/замена line'ов через nav-collection даёт
DbUpdateConcurrencyException «0 rows affected» при следующем UPDATE
родителя. На документах без xmin это становится 500, на InventoryDoc
(с xmin от TD-6) — 409.

Переводим Enters/Losses/Transfers/SupplierReturns.Update на
ExecuteDelete + DbSet.Add (как Supplies). InventoriesController
дополнительно: добавление новых строк через _db.InventoryLines.Add
вместо doc.Lines.Add (RemoveRange/Clear там не было — merge-in-place
по ProductId).

Воспроизведение (на Enters):
1. POST /api/inventory/enters {lines:[A]}
2. PUT … {lines:[A,B]} (одна оставлена, одна новая) → было 500
   DbUpdateConcurrencyException ; стало 204.

stage-enter (10 шагов): CRUD + Post + Unpost + edge + multi-tenant +
concurrent PUT — все зелёные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 16:57:48 +05:00
nns 4c2841db5b docs(stage): пункт 2 done — каталог CRUD + multi-tenant зелёный, 2 фикса 2026-05-29 16:46:32 +05:00
nns d54e1cb968 fix(catalog): EF8 nav-collection bug в Products.Update + unique IX на Article
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
1. Products.Update: добавление нового barcode'а к существующему товару
   валилось с DbUpdateConcurrencyException 'Товар изменён в другом окне',
   хотя никакой конкурентной правки не было. Тот же EF8-баг, который в
   TD-6 чинили на Supplies/Demands/RetailSales: nav-collection.Add +
   client-side Id путает EF, UPDATE родителя получает 0 affected. Чиним
   тем же паттерном: ExecuteDelete старых ProductBarcodes/ProductPrices,
   DbSet.Add новых. Воспроизводится: создать товар с 1 barcode, PUT с
   2 barcodes → 409. После фикса → 204.

2. IX_products_OrganizationId_Article был обычным (не уникальным), хотя
   контроллер ловил нарушение по имени индекса и возвращал 'Артикул уже
   занят'. Catch-блок никогда не срабатывал. Делаем индекс уникальным
   миграцией Phase8d. Перед созданием — нумеруем дубликаты по существующим
   данным (если есть). NULL/пустые article остаются distinct (Postgres
   NULL semantics).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 16:46:10 +05:00
nns 0511cfacfd test(stage): smoke + signup на test.admin.food-market.kz
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
stage-smoke (5 шагов): signup happy-path, bootstrap (Store/Roles/
Units/ProductGroup/PriceTypes/RetailPoint), login → access+refresh
+ /api/me с правильным orgId+role=Admin, edge-cases (дубликат
email, короткий пароль, пустое название, кривой телефон), проверка
public-сайта.

Informational gap: stage-public (test.food-market.kz) использует тот
же build что прод-public, поэтому его форма signup POST'ит в прод
admin. Для stage-testing регистрируемся напрямую POST на test.admin.

Чек-лист stage-testing: пункт 1 ✓.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 16:29:04 +05:00
nns 4675f38a0f docs(sprint6): P2-4 done — все 5 пунктов выполнены, итог
Some checks failed
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Docker Public / Build + push Public (push) Has been cancelled
Docker Public / Deploy Public on stage (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:59:47 +05:00
nns 7b7a7091b9 feat(auth): TOTP 2FA для админов через AuthenticatorTokenProvider (P2-4)
ASP.NET Identity AuthenticatorTokenProvider (RFC 6238 — Google
Authenticator, Authy, 1Password OTP). TwoFactorEnabled и SecurityStamp
уже были в users-таблице из Identity-схемы.

Endpoints (Bearer-auth):
- GET /api/me/2fa/status — { enabled: bool }.
- POST /api/me/2fa/enroll — генерирует SecretKey (если ещё нет),
  возвращает otpauth-URI для QR + сам shared-key. Пока 2FA включён,
  enroll возвращает alreadyEnabled=true без секрета.
- POST /api/me/2fa/verify { code } — валидирует и включает 2FA.
- POST /api/me/2fa/disable { code } — выключает + ResetAuthenticatorKeyAsync.
  Требует текущий code как защиту от случайного отключения.

AuthorizationController.Exchange (password grant): после успеха проверки
пароля смотрит TwoFactorEnabledAsync; если true и нет otp_code в
запросе — возвращает invalid_grant с error_description="2fa_required";
если otp_code невалиден — "2fa_invalid"; иначе токен выдаётся.

Опционально для всех ролей — User самостоятельно решает включать или нет.
Для админов рекомендуется (отдельная политика — следующий шаг).

Тесты: 4 интеграционных (enroll+verify+status, неверный code → 400,
token-endpoint require otp_code, disable с code). Тесты сами генерируют
TOTP через ручную RFC 6238 имплементацию (HMAC-SHA1, 30-сек step).

Bonus: добавлены DI-заглушки UnusedSupplyWriter / UnusedRetailSalePoster
для CQRS-handler'ов из TD-1 — handler'ы пока не подключены к
контроллерам, заглушки нужны чтобы DI-validation на старте не падала.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:57:32 +05:00
nns 971c9b29a5 docs(sprint6): TD-1 done 2026-05-28 17:51:36 +05:00
nns ef8c4a3222 feat(cqrs): MediatR partial — 3 handler-образца (TD-1)
Подключён MediatR в food-market.api с авторегистрацией из сборки
food-market.application. Цель — показать паттерн, не полный
рефакторинг контроллеров (это отдельный спринт).

Образцы handler'ов в food-market.application:
- Purchases/Commands/CreateSupplyCommand + CreateSupplyHandler — создание
  Draft-приёмки с делегированием персистентности через ISupplyWriter
  абстракцию (testable без EF).
- Sales/Commands/PostRetailSaleCommand + PostRetailSaleHandler —
  проведение чека с валидацией платежа (переиспользует RetailPaymentValidator
  из Sprint 1) и делегированием stock-операции через IRetailSalePoster.
- Sales/Queries/GetSalesReportQuery + GetSalesReportHandler — агрегация
  плоских sale-строк по period:day/period:month/product. Pure-функция,
  безопасно тестируется в памяти.

Контроллеры пока используют прежние flow (контроллер → EF напрямую) —
поэтапная миграция, не big-bang. Эти handler'ы — образец, на который
оглядываемся при следующих feature'ах.

Тесты: 6 unit (2 GetSalesReportHandler, 1 CreateSupplyHandler,
3 PostRetailSaleHandler). 57 unit total зелёных.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:51:08 +05:00
nns 77c7bb52d1 docs(sprint6): TD-4 done 2026-05-28 17:46:48 +05:00
nns 443eebe862 feat(logging): структурные log-fields в Serilog (TD-4)
LogEnrichmentMiddleware: после Authentication+Authorization вытягивает из
ClaimsPrincipal OrgId (claim org_id) и UserId (sub/NameIdentifier), плюс
CorrelationId из заголовка X-Correlation-ID (или генерирует Guid). Все три
кладутся в Serilog LogContext через PushProperty — каждая ILogger.Log*
внутри пайплайна автоматически получает эти поля как структурные
properties (не текст), пригодные для фильтрации в Loki/ELK без regex.

Эхо CorrelationId в response-header — клиент видит id для support.

Business-логи (структурные плейсхолдеры, не string interpolation):
- Supply.Post → "Supply posted: {SupplyNumber} supplier={SupplierId}
  store={StoreId} lines={LinesCount} total={Total}".
- RetailSale.Post → "RetailSale posted: {SaleNumber} store={StoreId}
  payment={Payment} lines={LinesCount} total={Total}".

docs/logging.md — паттерн, anti-pattern'ы (string interpolation, PII в
логах, токены/пароли), correlation-id workflow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:46:17 +05:00
nns f936cd26c2 docs(sprint6): TD-2 done 2026-05-28 17:42:54 +05:00
nns eacf7e5cc8 feat(validation): FluentValidation + ValidationFilter для DTO (TD-2)
Подключён FluentValidation (уже был в Directory.Packages.props, теперь
активно используется):
- AddValidatorsFromAssemblyContaining<Program>() — авторегистрация всех
  IValidator<T> из сборки food-market.api.
- ValidationFilter (IAsyncActionFilter) глобально подключён через
  MvcOptions: на каждый action ищет IValidator<TArg> по рантайм-типу
  body-параметра, гоняет, fail → 400 ValidationProblemDetails (RFC 7807).

Не используем FluentValidation.AspNetCore — официально deprecated
(см. docs.fluentvalidation.net/aspnet); current recommendation —
DI-extensions + manual filter, как у нас.

Валидаторы (для 5 DTO):
- SupplyInputValidator — Supplier/Store/Currency ≠ Empty, Date ≤ tomorrow,
  Lines non-empty, line.Quantity > 0, line.UnitPrice ≥ 0.
- RetailSaleInputValidator — Store/Currency ≠ Empty, Date ≤ tomorrow,
  PaidCash/PaidCard ≥ 0, Lines non-empty с per-line проверками.
- ProductInputValidator — Name required, Vat∈[0,100], MinStock ≤ MaxStock.
- CounterpartyInputValidator — Name required, BIN/ИИН regex \d{12},
  Email формат (EmailAddress).
- EmployeeInputValidator — LastName/FirstName required, RoleId ≠ Empty,
  SendInvite → требует CreateAccount + Email, CreateAccount → требует Email.

Сообщения по-русски (фронт ждёт RU).

Тесты: 16 юнит-тестов на валидаторы (5 на SupplyInput, 2 на RetailSaleInput,
4 на ProductInput, 2 на CounterpartyInput, 3 на EmployeeInput). Полный
прогон unit-тестов зелёный.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:40:46 +05:00
nns 8f0773eab3 docs(sprint6): TD-6 done 2026-05-28 17:33:34 +05:00
nns ec0cff7fc4 feat(concurrency): RowVersion на документах через Postgres xmin (TD-6)
Optimistic concurrency через системную колонку Postgres xmin — никакой
дополнительной колонки и миграции не нужно, xmin есть у каждой таблицы
и автоматически обновляется при UPDATE.

Конфигурация:
- IVersionedEntity (маркер) + uint Xmin на Supply, Demand, RetailSale,
  Transfer, InventoryDoc.
- e.UseXminAsConcurrencyToken() в EF-конфиге для каждой — создаёт shadow
  property "xmin" с IsConcurrencyToken + ValueGeneratedOnAddOrUpdate.
- e.Ignore(x => x.Xmin): .NET-property живёт только для транспорта в DTO,
  не маппится в БД (xmin тащим shadow'ом).
- GetInternal в SuppliesController читает xmin через
  EF.Property<uint>(s, "xmin") в LINQ-проекции и складывает в DTO.

Wire-up:
- SuppliesController.Update принимает input.Xmin (uint?), сверяет с
  shadow xmin загруженного supply через EF.Entry().Property("xmin").
  Несовпадение → 409 с code=concurrency_conflict. null/0 от клиента →
  legacy compat, проверки нет.
- SaveOrFkErrorAsync ловит DbUpdateConcurrencyException → 409 (двойная
  защита: и явная сверка, и EF auto-check в SaveChanges).

Bonus: Supply.Update перешёл на тот же паттерн что Demand/RetailSale —
ExecuteDelete старых строк + AddRange новых напрямую в DbSet. Старый
RemoveRange-then-Add через nav-collection ломал EF concurrency check
(UPDATE supply_lines одной из старых строк падал 0 affected внутри той
же SaveChanges-транзакции).

Тесты: 2 интеграционных:
- two parallel updates with same xmin → один 204, другой 409; retry
  с новым xmin тоже 204.
- legacy clients без xmin → PUT работает без concurrency-проверки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:33:01 +05:00
nns 406fcb9d7d docs(sprint6): чек-лист — RowVersion, FluentValidation, Serilog, MediatR, 2FA
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:00:27 +05:00
nns c43f68c39b docs(sprint5): TD-5 done — все 4 пункта выполнены, итог
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 16:47:32 +05:00
nns b963adfa2e feat(import-jobs): persisted ImportJobRegistry в БД (TD-5)
Раньше прогресс фоновых импортов жил в ConcurrentDictionary внутри
Singleton-сервиса: рестарт процесса терял всю историю, активные
джобы навсегда оставались в статусе Running.

Теперь:
- Domain.Integrations.ImportJob (TenantEntity) — таблица import_jobs,
  миграция Phase8c_ImportJobs (jsonb для ErrorsJson, индексы по
  OrgId+StartedAt / OrgId+Status / FinishedAt).
- ImportJobRegistry рефакторен: Create() пишет строку немедленно,
  SaveAsync() обновляет, Get/RecentlyFinished читают из БД. API
  совместимое со старой in-memory версией — MoySkladImportService
  и контроллеры не меняются.
- MoySkladImportController.RunInBackgroundAsync теперь:
  * Periodic flush через Timer каждые 2 секунды — UI видит
    реальный progress (Stage/Created/Total), а не Create-snapshot;
  * Финальный flush в finally — обязательный для terminal state.
- AdminCleanupController.WipeAllAsync — то же финальное сохранение.
- SkipAudit=true для import-job записей — служебные, в OrgAuditLog
  не пишем.

Tenant-isolation: query-filter работает прозрачно, B не видит джоб A.

Тесты: 3 интеграционных (survives across scope, RecentlyFinished
читает из БД, tenant-isolation).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 16:45:08 +05:00