На реальном объёме перенесённого магазина список движений отдавал >2.5с из-за COUNT по
3-table join. Count теперь по stock_movements с фильтрами (join не меняет число строк).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DevDataSeeder по умолчанию (dev/stage) сидирует демо-орг; на проде SEED_DEMO_DATA=false
отключает демо-орг/demo-admin/привязку, сохраняя SuperAdmin+роли+глобальные ед.изм.+SystemSettings.
Нужно для чистого (пустого) старта реального магазина.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ImportRetailReturnsAsync (retailsalesreturn): связь с чеком по externalId demand, +остаток
- ImportInventoriesAsync (inventory): Completed без движений, BookQty/ActualQty из calculated/quantity
- Все 7 импортёров принимают updatedFromMsk → filter=updated>= (МСК) для дельты
- Контроллер: ?updatedFrom=<МСК> прокинут во все типы
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
startOffset у всех документных импортов + эндпоинт ?offset=N: при продолжении
пропускаем уже перенесённые через offset API (а не перечитывая сотни страниц).
Идемпотентный skip по msId держит границу.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
По проверенному шаблону приёмок (идемпотентно по msId, резюмируемо, МСК→UTC, тиыны,
дизамбигуация номеров, SaveChanges по документу, движения остатков):
- enter (+остаток), loss (−остаток, WriteOff), purchasereturn (−остаток, SupplierReturn),
retaildemand (−остаток, RetailSale; оплата нал/безнал из cashSum/noCashSum, кассир=owner).
- Общие помощники LoadMaps/Aggregate. Эндпоинт POST /import-supplies?type=enter|loss|
purchasereturn|retaildemand (фон, job с нужным kind).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
МойСклад отдаёт sum/cashSum/price как число с точкой (900000.0) → long не парсился
(«JSON value could not be converted to System.Int64»). Поля DTO → decimal. Плюс
GET /api/admin/moysklad/jobs/{id} для наблюдения за прогрессом/ошибками переноса.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Тяжёлый expand=positions.assortment раскрывал весь товар в каждой позиции → страница
гигантская и зависала. assortment.meta.href в позиции есть и без раскрытия. Таймаут
HttpClient МойСклада поднят до 130с (страница иногда > 100с).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 2 (документы) — проверенный шаблон на приёмках:
- Phase41: MoySkladExternalId у counterparties/stores/units/retail_points/employees и
документов (supplies/enters/losses/inventories/supplier_returns/retail_sales/
cash_shifts/money_transactions) + индексы. Свойства на доменных сущностях.
- MoySkladClient.StreamDocumentsAsync: документы в хронологии (order=moment,asc), expand
позиций/связей (limit=100), обработка 429/сбоев.
- MoySkladDocumentImportService.ImportSuppliesAsync: идемпотентно по msId, резюмируемо
(skip перенесённых), moment МСК→UTC, суммы из тиынов, поставщик по externalId контрагента,
товары по externalId, проведение → движения остатков (StockService). Журнал ошибок.
- Импорт контрагентов теперь пишет MoySkladExternalId (нужно для ссылок в документах).
- Эндпоинт POST /api/admin/moysklad/import-supplies (фон, job). Остальные типы — по шаблону.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Найдено Mac-Claude на стенде:
1. Генератор внутреннего EAN-13 давал «02…» (ведущий 0 → зона UPC-A, может совпасть
с реальным товаром). InternalBarcode.PickPrefix теперь берёт 2x из restricted-
circulation 20–29 (без ведущего нуля), не пересекаясь с весовыми префиксами орг;
«02» — только крайний резерв. Юнит-тест обновлён.
2. «Проверка данных» на Демо показывала все 0 и lastCheckedAt=null — ночной cron ещё
не отрабатывал после пересева. Добавлен разовый BackgroundJob.Enqueue(DataQualityJob)
при старте: после деплоя/пересева счётчики считаются сразу по всем организациям.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Реальный сценарий владельца: весов с печатью этикеток нет, штрихкоды весовых —
обычные коды «27xxxxx» в карточке. Исправлены два бага:
- поиск по ШК матчил весовой PLU по АРТИКУЛУ (вода арт.12345 → 1,5 кг) — убрано,
поиск по штрихкоду больше нигде не матчит по артикулу;
- разбор весовых этикеток был дефолт-ON (WeightedBarcodeSettings.Enabled,
Organization.WeightBarcodeEnabled) и ловил обычные коды — теперь дефолт OFF.
- ByBarcode: сначала точное совпадение штрихкода (вкл. короткие «27xxxxx»),
разбор этикетки — только если включён в орг И точного ШК нет.
- Phase40: WeightBarcodeEnabled DEFAULT false + выключение у существующих орг;
WeightedCodeFormat('short'|'ean13')/WeightedCodePrefix(27)/WeightedCodeDigits(5).
- Генератор ШК нового весового товара: короткий «<prefix>+N цифр» (27xxxxx, next-free)
или EAN-13 по настройке; уникальность.
- Настройки орг отдают/принимают формат кода весовых.
- Sync отдаёт unitShort («шт»/«кг»/«л») рядом с unitCode (ОКЕИ) — shared UnitShort.
- Тесты: MobileApi (exact-only по умолчанию; не матчит артикул; PLU как ШК при вкл) 9/9;
unit WeightedBarcode/UnitShort 16/16.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ValidationFilter (FluentValidation-путь): русский заголовок «Проверьте
правильность заполнения» через ModelErrorTranslator вместо дефолтного англ.
«One or more validation errors occurred» (сообщения полей уже были русскими).
- stage-ui-uat-mobile.spec.ts: P1-экраны на 390px без h-scroll/console/5xx +
проверка «Остатки» (артикул/штрихкод, тумблер «показать нулевые»).
- docs/uat-report-2026-10-09.md: матрица покрытия (уже-зелёное vs проверено
сейчас) по требованию владельца; галерея скринов reports/uat-mobile/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
«Последние продажи»/Z-отчёт показывали времена вперёд, потому что дата чека
бралась от клиента (input.Date / POS OccurredAt) без нормализации: наивная
локальная дата или спешащие часы кассы давали будущий инстант. Добавил
BusinessTime.ToInstantUtc: Kind→UTC и clamp будущего (>5 мин) к now — чек не
может быть в будущем. Применено в рознице (create) и в приёме чеков с кассы
(движение берёт тот же инстант). Юнит-тесты на нормализацию/clamp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Неверный тип/enum в теле запроса давал пользователю технический англ. текст с
именем класса и путём ($.reason: The JSON value could not be converted to
foodmarket.Api…LossInput…). Теперь InvalidModelStateResponseFactory прогоняет
ModelState через ModelErrorTranslator: технические сообщения → «Неверное значение
поля «Причина»» (имя поля по-русски, без классов/путей/англ.), осмысленные
FluentValidation-сообщения остаются как есть. Сырой текст пишется только в лог
бэкенда. Действует на web (toast) и mobile API. Тест: списание с неверным enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Остатки строились ОТ таблицы Stocks (JOIN Products), поэтому товары без строки
Stock (ни разу не приходованные) не попадали в список вообще, а includeZero лишь
снимал фильтр Quantity!=0 у существующих строк. То же в отчёте «Остатки на дату»,
в экспорте и в фильтре по группе (iOS получал total 0).
- GetStock/ExportStock: выборка ОТ Products (не-услуги, не-архивные) с остатком
коррелированным подзапросом (товар без Stock = 0). Склад выбран → строка склада;
без склада → агрегат «Все склады». Нулевые видны при includeZero; negativeOnly и
пагинация/сортировка/поиск/экспорт работают и для нулевых. Остаток/ед./штрихкод
дотягиваются batch-запросом по id страницы (EF8 не переводит подзапросы в SELECT+ORDER BY).
- Отчёт «Остатки на дату»: includeZero добавляет товары без движений (Qty 0);
фильтр по группе теперь с вложенностью.
- groupId/productGroupId — синонимы с вложенными группами на всех списках остатков
и в каталоге (List + Export). Раньше catalog?productGroupId молча игнорировался.
- Штрихкод рядом с артикулом в списке/экспорте — различать одноимённые товары
(4× «BIOBLAS шампунь» с разными артикулами из МойСклада — не удалять).
- Demo (30 894 товара): без флага 12, includeZero → 30 894, <500мс/страница.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Раньше флагировались все товары в группах с ParentId==null (включая реальные
верхнеуровневые категории вроде «Молочные»). Теперь — только товары в служебной
группе «Все товары»/«Без группы». На seed-demo: было 50 (ложно), стало 3 (верно).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hangfire DataQualityJob (ночью 02:20 UTC) считает по орг. счётчики проблем: без группы
(корень/«Без группы»), без цены продажи, без штрихкода (кроме услуг), дубль штрихкода,
возможный дубль имя+единица; товары в минусе — ссылкой на центр (не дублируем). Результаты
в data_quality_results (Phase43), живые детальные списки — по запросу.
API /api/data-quality: сводка, {check}/products, recompute, dismiss («это разные товары»),
bulk set-group/set-price/generate-barcodes, PUT settings (отключение проверок). unknown-cost
отложен до FIFO; PLU убран (поправка владельца). Integration-тест: счётчики/список/bulk/dismiss.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POS: чек с точки без открытой смены — сервер автооткрывает смену (Cash-счёт точки,
размен 0) и принимает чек (как в вебе). Фоновый ShiftWatchJob (раз в час) уведомляет
кассира и админов о сменах, открытых дольше 24ч (персистентное уведомление + SignalR,
дедуп раз в сутки). Тест POS-автооткрытия.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /api/documents/{type}/{id}/related — основание + связанные (возвраты/оплаты/
опр-спис/возвраты по чеку) для supply/supplier-return/retail-sale/inventory/enter/loss.
Компонент RelatedDocsBlock применён на edit-страницах приёмки, возврата поставщику,
оприходования, списания, чека (инвентаризация имела свой блок). Integration-тест:
приёмка↔возврат (related/basedOn).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /api/money/reports/supplier-reconciliation?supplierId=&from=&to=: начальное
сальдо (до from), движения периода (приёмка +долг, возврат/оплата −долг) с бегущим
сальдо, конечное сальдо. Веб-страница /money/supplier-debts/:id/reconciliation —
печатная форма акта (window.print()/PDF) + кнопка «Акт сверки» в долгах поставщикам.
Integration-тест: приёмка как debit, конечное сальдо.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/purchases/supplier-returns/from-supply/{supplyId}: черновик возврата с
преднабором строк из проведённой приёмки (товар/кол-во/цена), поставщик/склад/валюта
и ссылка ReferenceSupplyId. Integration-тест: преднабор + связь.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
П.1: продажа на точке без открытой смены — смена автооткрывается (подбор Cash-счёта
точки/орг., размен 0, можно внести позже), как МойСклад; ручное открытие остаётся.
Длительность смены и отметка overLimit24h в /current и Z-отчёте (мягкое правило:
предупреждение + отметка, не жёсткий лимит).
П.2: Z-отчёт /shifts/{id}/report теперь с разбивкой byProduct (продано кол-во/сумма,
возвращено кол-во/сумма).
Тест переписан: продажа без смены → автооткрытие + проводится; /current open=true;
Z-отчёт содержит byProduct с верными кол-вом/суммой.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
П.3 паритета: create-return уже ставит способ исходного чека. В ручном возврате
(IsReturn + referenceSaleId) если способ отличается от оплаты чека — нужно право
администратора и причина (RefundReason, пишется в Notes). Сообщения понятные.
Тест: тот же способ ок; иной без причины → 400; иной с причиной (админ) → 201.
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>
- POST /api/admin/seed-demo?bulk=N — массовое наполнение каталога (до 60k товаров)
батчами для нагрузочных замеров (идемпотентно по маркеру BULK-).
- POS --bench <url> <user> <pass>: мерит первичную загрузку (сеть/запись отдельно),
тёплый старт (открытие базы + поиск), дельта-синк. С gzip-декомпрессией.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Касса работает из локальной базы сразу; вся синхронизация — в фоне, 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>
Канал Web Push для центра уведомлений (item 5). Ключи VAPID генерируются один раз
и хранятся в platform_settings (без ручной настройки секретов); публичный отдаётся
фронту. Таблица web_push_subscriptions (Phase42) + /api/web-push/{public-key,
subscribe,unsubscribe}. Отправка через web-push-csharp из фонового джоба минусов
(best-effort, протухшие подписки чистятся). SW: обработчики push/notificationclick
(am-v3→am-v4), клик открывает нужный путь. Кнопка «Вкл. push» в колокольчике.
Работает в т.ч. в iOS Safari для PWA на домашнем экране.
Тесты: integration — публичный ключ стабилен, подписка идемпотентна, 400/unsubscribe
(4/4 группы); unit 299/299.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Два предсуществующих бага, незаметных пока UnitTests не компилировался (с pass 1
конструктор StockService требовал новый параметр):
- MoneyConfigurations: CHECK ck_money_link_single_target использовал Postgres-only
`::int` — SQLite (EnsureCreated в юнит-тестах) падал 'unrecognized token ":"'.
Переписан на портируемый CASE WHEN (семантика та же, что в миграции Phase33a).
- ApiReferenceDocsJob: regex прав требовал `[RequiresPermission(...)]` в отдельных
скобках и пропускал комбинированную форму `[HttpGet, RequiresPermission("X")]`
(её используют в т.ч. контроллеры negative-stock). Убраны обязательные скобки.
Юнит-сьют снова зелёный: 297/297.
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>
«InventoryCancelResults» (22) не влезал в OrgAuditLog.Action varchar(20) →
SqlState 22001 → cancel-results возвращал 500. Укоротил до «InvCancelResults».
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>
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>
Пересняты скриншоты публичного сайта в интерфейсе 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>
Замечание владельца: инвентаризация стояла «Проведено» без оприходования/списания.
Переделано по МойСкладу (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>
БЕЗОПАСНОСТЬ:
- Страница входа больше НЕ префилит e-mail/пароль dev-админа и не показывает
подсказку «Dev admin: …» — только в import.meta.env.DEV на localhost
(на стенде/проде поля пустые).
- DevDataSeeder: пароль SuperAdmin/демо-админа ТОЛЬКО из секрета
(Seed:SuperAdminPassword / env SEED_SUPERADMIN_PASSWORD). Дефолт «Admin12345!»
допустим лишь в Development. На stage/prod (ASPNETCORE_ENVIRONMENT=Production)
без секрета аккаунт с известным паролем НЕ создаётся (warning в лог).
- stage-compose: проброшен Seed__SuperAdminPassword (без дефолта).
- (Пароль существующего SuperAdmin на стенде сменён вручную на сложный и сохранён
в ~/.fm-watchdog/stage-superadmin-password; quick-login по sentinel/master цел.)
ТОСТ 404:
- StageBanner и QuickLoginBlock пробуют необязательный /api/dev/quick-login/accounts
через raw fetch (минуя axios-интерсептор) — на проде/без API 404 больше не
показывается глобальным тостом. На странице входа тостов ошибок нет.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- EmailTemplates (invite/low-stock/weekly-summary): шапка с текстовым
вордмарком «all market» (бренд-цвета, всегда рендерится — без внешних
картинок и привязки к домену) + подпись «ALL MARKET — система учёта торговли».
- Имя отправителя по умолчанию, темы/тела системных писем, экспорт-README,
дневная сводка, тест SMTP → ALL MARKET.
- Цвета писем и домены не менялись.
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>
- Сервер: Employee.PinHash (PBKDF2, миграция Phase36); POST
/api/organization/employees/{id}/pin — админ задаёт/снимает PIN (4–6 цифр,
уникален среди активных сотрудников на пересекающихся точках). Кастомный
OpenIddict-грант grant_type=pin (retail_point_id + pin): находит кассира,
привязанного к точке, по PIN и выдаёт токены. Брутфорс-защита — in-memory по
точке (5 неверных → блок 5 мин). Клиент-сидер досоздаёт gt:pin существующим
клиентам (без пересоздания). Пароль остаётся запасным.
- Касса: PIN-пад на зарегистрированном устройстве (маскированный ввод, 0–9/⌫/
Войти), переключение на вход по логину; AuthService.PinLoginAsync.
- Тесты: unit PinHasher (9), integration (3): вход по PIN выдаёт рабочий токен;
5 неверных → блок; дубль PIN на точке → 409. + скриншот PIN-пада в галерее.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Сервер: /api/me отдаёт retailPointIds (точки, к которым привязан сотрудник).
- Касса: DeviceRegistration (чистая логика, 5 unit-тестов) — первый запуск:
админ выбирает точку из списка активных (сохраняется на устройстве, kv);
кассир на незарегистрированном устройстве → «обратитесь к администратору»;
кассир, не привязанный к точке устройства → «вы не привязаны к этой кассе»;
админ работает на любой точке. Смена точки — кнопка-сброс (новую выберет
только админ). Экран входа: шаг выбора точки (ComboBox активных точек).
- Заменяет прежний «берётся первая активная» (ошибка при двух точках).
- Тесты: unit (5 решений гейта) + integration (/api/me retailPointIds кассира).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Сервер: 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>
Инцидент menu-walk 2026-10-08: demo-cashier получал 403 на
GET /api/money/accounts — страница смен (/money/shifts) дёргала его для выбора
кассы при открытии смены, но эндпоинт под MoneyView, а у кассира MoneyView=false
(«кассир не видит денег»), только CashShiftOperate.
- Новый узкий GET /api/money/shifts/accounts (RequiresPermission
"MoneyView,CashShiftOperate"): только активные наличные кассы + эквайринг,
без остатков и банковских счетов. Кассиру достаточно для выбора кассы/эквайринга
при открытии смены, но денег он по-прежнему не видит.
- Веб: CashShiftsPage использует useShiftAccounts() вместо useMoneyAccounts(true).
- Регресс-тесты: кассир 200 на /shifts/accounts (только нал+эквайринг) и 403 на
/money/accounts; /api/me permissions для кассира (advanced-only скрыт).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- RolePermissions.Effective(role, isSuperAdmin, isAdmin, advancedMode) — флаги с
учётом роли и простого/расширенного режима; AdvancedOnly — единый список
advanced-only прав (зеркалит [RequiresAdvancedMode]).
- /api/me теперь отдаёт permissions (имя→bool). Мобильное и веб скрывают одно и то же.
- Веб: хук useCan() читает me.permissions (раньше гейтил только по именам ролей).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/inventory/stock?groupId= — остатки по группе включая все вложенные
подгруппы (через денормализованный ProductGroup.Path). Комбинируется с
storeId/search/сортировкой. Нужно мобильному «FM Склад» для навигации по категориям.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- POST /api/catalog/products/{id}/barcodes {code, kind}: добавить штрихкод к
товару, уникальность в организации, 409 «штрихкод уже у товара X» (+название),
идемпотентно при повторе тому же товару, автоопределение типа по длине.
- Весовые этикетки (внутримагазинные EAN-13, как в МойСкладе): общий парсер
food-market.shared/Barcodes/WeightedBarcode (настраиваемые префиксы веса/цены,
длина PLU); касса pos.core делегирует туда. Настройки организации
(WeightBarcode*: enabled/префиксы/длина кода) + миграция Phase35.
- GET /api/catalog/products/by-barcode/{value} разбирает весовую этикетку →
товар + количество (вес) / встроенная цена; обычный ШК — как прежде.
- GET/PUT /api/organization/settings отдаёт/принимает настройки (блок весовых
применяется только если передан — round-trip-safe для текущей веб-формы).
- Офлайн-каталог ProductSyncDto: явный isWeighed (packaging==2) для мобильного/кассы.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
Раньше продажи/возвраты/внесения/выплаты/расхождение смены не влияли на остаток
кассы (касса уходила в −, эквайринг оставался 0). Теперь при закрытии смены
MoneyService.MaterializeShiftMovementsAsync создаёт Posted-движения money_transactions
по типам (Category): продажи нал/карта, возвраты нал/карта, внесение, выплата,
излишек/недостача — привязанные к смене, идемпотентно. Остаток кассы/эквайринга и
отчёт «Деньги» теперь верны автоматически (всё читает money_transactions).
Отрицательный остаток: расход (CashOut/PaymentOut/TransferOut) больше остатка счёта
и выплата из смены больше наличных смены → 409 insufficient-funds; настройка
Organization.AllowNegativeBalance (по умолч. выкл., меняет только Admin).
Миграция Phase34 (idempotent): money_transactions.Category/.CashShiftId,
organizations.AllowNegativeBalance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>