- Сервер: 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>
По итогам проверки Mac-Claude (скриншоты):
- Формат чисел как в вебе/мобильном: «3 422 ₸» (пробел-разделитель тысяч),
вес «1,235 кг» (запятая), единица измерения рядом с количеством («3 шт»).
PosFormat — культура ru-RU с NBSP-разделителем, применяется процессно.
- Скидка на строку видна: «1 560 → 1 500 ₸, скидка 60» под позицией, чтобы
цена×кол-во ≠ сумма не выглядело ошибкой (SaleLineVm.DiscountText/HasDiscount).
- Кнопки: текст по центру по вертикали и горизонтали (стиль Button.pos +
ContentPresenter); «Очистить» и «Enter · Провести и печать» больше не
обрезаются на 1366×768 и 1920×1080 (проверено скриншотами).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Инцидент #1791428701 (quality-watchdog): кассир получал 403 на /api/money/accounts
на странице смен. Фикс — узкий /api/money/shifts/accounts (см. 9dc033d). Retest на
стенде: menu-walk 3/3 роли зелёные.
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>
- Cart.Snapshot()/Restore() — независимая копия строк и скидки на чек.
- SaleViewModel: ParkCommand («Отложить» — сохранить чек, начать новый) и
ResumeCommand («Вернуть» — восстановить отложенный); кнопки на экране продажи.
- Unit-тест независимости снимка (38 POS unit-тестов зелёные). Скриншоты обновлены.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Запрашивает GET /api/pos/v1/version и показывает версию + кнопки «Скачать для
Windows / для Linux» (ссылки из конфигурации сервера, секция Pos). Если ссылки
не настроены — подсказка администратору.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Модели (товар/корзина/строка/смена), парсер весовых ШК (EAN-13 20–29),
корзина (штучный инкремент, весовой, скидки строка/чек с клампом), математика
оплаты и смены (должно/факт/разница).
- PosStore на raw Microsoft.Data.Sqlite (быстрый старт, AOT-дружелюбно): каталог
+ штрихкоды + очередь продаж + kv; индексы по штрихкоду/NameLower (кириллица
не сворачивается через LIKE) и barcodes.ProductId (дельта-синк O(n), не O(n²)).
- SyncService: дельта-pull каталога + push очереди с детерминированным
idempotency-ключом (MD5 отсортированных ClientSaleId → один набор = один ключ).
- Auth: password/refresh grant, ITokenStore (FileTokenStore 0600 на Unix,
InMemory для тестов). Checkout.BuildSale: сборка серверного чека, скидка на чек
распределяется по строкам (сервер понимает только построчную). UpdateCheck.
- Тесты: штрихкод/корзина/оплата/смена/стор/синк-ключ/checkout/офлайн(5 без
дублей)/версия — 37 зелёных.
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>
full-chain: indepBalance() считает остаток независимо (касса += факт−размен,
эквайринг += картой−возвраты) и сверяет с API /balances — а не с суммой из того же
API; шаг 8 — оплата нал ≤ остатка кассы + проверка отказа при превышении (409).
Unit: shift-net == факт−размен (MoneyMathTests). Integration: закрытие смены пишет
нал в остаток кассы, расход больше остатка → 409 (MoneyModuleTests); существующим
тестам оплаты дан достаточный остаток кассы.
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>
stage-reset-demo.sql — очистка орги от транзакционных данных (KEEP каталог/
сотрудники/точки). fix-internal-ean13.sql — пересчёт неверной контрольной цифры у
уже выданных внутренних EAN-13 (на стенде исправлен 41 код, список — docs/ean13-fixed-20261008.txt).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16 шагов против настоящего стенда: регистрация→справочники→сотрудники/точки→
деньги→3 приёмки→оплаты→смена/продажи/возврат/Z→склад→отчёты→инварианты→откат.
Сверяет конкретные числа и инварианты, 0×5xx/0×тостов/0×console.error. Режимы
C (temp-орг) и B (Демо на реальном каталоге). Hi-res скриншоты каждого процесса +
галерея. Включён в nightly-verify и quality-watchdog (скрипты в $HOME).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Хелпер BusinessTime (UTC+5). Дашборд (RevenueToday/Week/Month + график) и
группировка period:day в отчётах продаж/прибыли теперь по местной дате.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1) digitsChecksum считал веса слева — для 12-значного тела (EAN-13) чётность
переворачивалась и контрольная цифра была неверной; теперь справа налево
(EAN-8/UPC-A не изменились). 2) SupplyScanPage читал /api/catalog/currencies как
массив, а это PagedResult {items} → список валют пуст (нет KZT). 3) InvoiceReviewPage
слал ?q=, а API ждёт ?search= (PagedRequest.Search).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Авто-подтянутые строки создаются как ActualQty=BookQty (Diff=0) вместо
ActualQty=0 (Diff=-stock). Проведение не трогает непересчитанные позиции (в т.ч.
вне выбранной группы); истинный ноль вводится явным ActualQty=0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenShiftAsync проверяет RetailPoint.IsActive (retail-point-inactive) и привязку
кассира к точке (retail-point-not-assigned; у кого привязок нет — на любой точке).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DbUpdateConcurrencyException «0 rows affected» при PUT сотрудника, у которого
есть EmployeeRetailPointAssignment. Замена nav RemoveRange+Clear+Add на
ExecuteDeleteAsync по EmployeeId + DbSet.Add (как в Supplies/Enters/Losses.Update).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Роль «Кассир» больше не имеет MoneyView (видит только свою смену/Z через
CashShiftOperate); роль «Кладовщик» получает InventoryEdit/EnterEdit/LossEdit
(веб уже показывал эти пункты — API теперь совпадает). PermissionRequirement
поддерживает список прав через запятую (ИЛИ); CashShifts List/Get/Report →
MoneyView,CashShiftOperate. DevDataSeeder: дефолты + backfill существующих орг.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/money.md — как открыть смену, оплатить приёмку, смотреть долги/остатки,
права ролей. Прогресс-док: пп.8–9 отмечены, журнал с тремя фиксами по ходу
e2e (back-fill прав, date-only→UTC, список счетов 500). Итог 10/10.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MoneyAccountsController.List проецировал xmin через EF.Property ПОСЛЕ
ToListAsync на AsNoTracking-сущности → InvalidOperationException (shadow-
свойство вне трекера). С пустым списком не всплывало (нет строк), с данными —
500, и ломались ВСЕ money-страницы, дёргающие useMoneyAccounts (счета/смены/
платежи) — поймал menu-walk на stage. Переносим xmin в server-side Select.
Регресс закрыт: date-only тест теперь ещё и GET-ит список с данными.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI (DateField) шлёт openingDate/date как "YYYY-MM-DD" → DateTime Kind=Unspecified
→ Npgsql не пишет в timestamp with time zone → 500 при создании счёта/платежа/
корректировки. MoneyMath.AsUtc нормализует (Unspecified→UTC as-is дата,
Local→ToUniversalTime, Utc→без изменений); применено в MoneyAccounts/
MoneyTransactions/CounterpartyAdjustments на всех сайтах записи даты.
Интеграционные тесты раньше слали DateTime.UtcNow (Kind=Utc) и баг не ловили —
добавлен Account_create_accepts_date_only_opening_date_from_ui (строго
"2026-10-07"). Money integration 8/8 ✓, unit 25/25 ✓.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SeedEmployeeRolesAsync пропускает уже засиженные орги (guard anyRole), поэтому
демо-орга на stage/prod осталась бы без MoneyView/CashShiftOperate у кассира →
403 на /api/money/shifts и красный menu-walk. EnsureMoneyDefaultsAsync
(идемпотентно, во всех env на старте) проставляет эти флаги системной роли
«Кассир» по всем оргам и гарантирует 5 системных статей расходов. Прочие права
кассира сохраняются (in-place мутация owned JSON). DemoSeederIdempotency 2/2 ✓.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Основное:
* Program.cs: SuppressImplicitRequiredAttributeForNonNullableReferenceTypes=true
+ InvalidModelStateResponseFactory → ValidationProblemDetails + Serilog Warning.
Пустые необязательные строковые поля в InvoiceLine (Note, BarcodeSource и т.п.)
больше не роняют запрос в 400. humanizeError на фронте уже умеет этот формат.
* InvoiceModels.cs: Name/Source/FilePath → string?.
* Validators.cs: InvoiceSaveLinesValidator (Lines NotNull), InvoiceApplyValidator
(Guid.Empty на Store/Currency = ошибка, SupplierId опционален).
Служебный «Поставщик не указан» (адаптация sklad §11 под multi-tenant):
* Phase32b миграция добавляет Counterparty.IsPlaceholder bool + partial index
по IsPlaceholder=true.
* InvoicesController.Apply: supplierId опционален; если не указан —
ResolvePlaceholderCounterpartyAsync ищет/создаёт одного служебного per org.
Имя из накладной в комментарий документа НЕ пишется.
* CounterpartiesController: служебный всегда последним в List сортировках,
Update и Delete при IsPlaceholder → 409 Conflict.
* CounterpartyDto.IsPlaceholder + Mapster-config обновлены.
* Автоподбора/автозаведения контрагентов нигде не было — специально проверили.
Applied-накладная readOnly (§10):
* InvoiceDetailDto: добавлены supplyNumber + processedAt.
* InvoiceReviewPage: applied-режим — плашка «Черновик приёмки № … создан …»
со ссылкой на приёмку, шапка и строки readOnly, скан-блок и apply-кнопка
скрыты. После успешного apply — invalidateQueries, экран сразу readOnly.
Текст ошибки apply (§5) и список накладных (§6):
* ApplyDialog: error → detail → title → (код), __silent на axios.
* Invoices list: parseServerDate подшивает «Z» если его нет, «Загружена DD.MM.YY HH:MM»
в бейдже статуса (видимо в десктоп-таблице и в мобильной карточке — у нас общая).
Тесты + проверки:
* tests/food-market.IntegrationTests/InvoiceApplySklad0610Tests.cs — 5 кейсов:
SaveLines со всеми null, apply без поставщика с idempotency по placeholder'у,
Delete/Update placeholder → 409, applied detail содержит processedAt +
hasFiles=false, apply с Guid.Empty storeId → ValidationProblemDetails.
Все 5 ✓.
* stage retest на https://test.admin.food-market.kz: upload+SaveLines(null-fields)+
apply(без поставщика)+apply(с ИП Бегимбаев)+DELETE placeholder → весь flow
зелёный, служебный один на орг, hasFiles=false после apply.
* quality-watchdog step 9 (menu-walk): 3/3 passed (3.7m).
* Фото-накладная через Claude CLI не гонял — пайплайн apply→DeleteStored
закрыт через hasFiles=false в тестах и реальный stage smoke.
docs/supply-inventory.md — шаг 7 расписан под необязательного поставщика,
новый шаг 8 (applied → readOnly), миграция Phase32b в таблице «Файлы».
docs/sklad-0610-progress.md — финал 9/9 с подробным журналом решений.
Пред-существующие (не трогал):
* OrgAuditLogTests.Counterparty_update_diff_contains_before_after → 403 от
[RequiresAdvancedMode] на /api/admin/audit-log (commit 921bca0, s29);
* ApiReferenceDocsJobTests.Scans_endpoints_with_doubly_nested_generic_return —
регекс-тюнинг доков.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Старый idempotency-маркер — Product с Article LIKE 'DEMO-%'. После того как
«Демо-магазин» на стенде наполнили реальным каталогом из МойСклада
(артикулы не DEMO-*), маркер срабатывал false и QuickLoginDemoSeeder при
каждом рестарте стенда пытался залить фиктивный каталог заново — падая
на unique-индексе IX_product_barcodes_OrganizationId_Code (23505). В БД
ничего лишнего не попадало (транзакция откатывалась), но лог stage-api
получал 9 ERR в сутки.
Фикс: SeedAsync и PeekAsync считают «уже сидировано» при любом Product в
орге. Поведение из UI (POST /api/admin/seed-demo) не меняется для свежих
org'ов. Прочие стартовые сидеры (OpenIddictClientSeeder,
SystemReferenceSeeder, DevDataSeeder) и HangfireJobsConfigurator — уже
идемпотентны, аудит в docs/seeder-noise-progress.md.
Integration-тест:
- SeedDemo_second_call_on_populated_org_returns_already_seeded
- SeedDemo_on_org_with_imported_catalog_skips_without_duplicate_barcode
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
05.10.2026 общий postgres-инстанс с чужими боевыми базами (sklad, purchase,
calcman, calcman_staging, makesales + мусорная старая food_market) вынесен
из compose прода food-market в отдельный compose-проект shared-postgres
(контейнер shared-postgres, порт 5434, том food-market-postgres-data external).
Прод food-market поднимается в собственный контейнер food-market-prod-postgres
(порт 5437, том food-market-prod-postgres-data). Простой при разделении — 12с,
соседи живы. См. docs/shared-postgres-followup-progress.md.
Этот коммит синхронизирует репозиторий, чтобы следующий деплой из main не
воскресил старую раскладку:
- deploy/docker-compose.yml: container_name/port/volume приведены к прод-файлу
(food-market-prod-postgres, 127.0.0.1:5437, food-market-prod-postgres-data).
- deploy/shared-postgres/ (новое): документационная копия соседнего compose'а,
с явной пометкой «food-market им не управляет».
- deploy/food-market-backup.sh / anonymize-prod.sh / db-schema-diff.sh:
defaults FM_PG_CONTAINER / FM_PROD_CONT = food-market-prod-postgres.
- docs/RUNBOOK.md: новый раздел «Postgres на prod-vm» с таблицей трёх
инстансов и явным запретом трогать shared-postgres из наших скриптов.
- docs/backup-restore.md: контейнер в таблице и примерах — новый.
Guards (не в репе — живут в ~/deploy-stage.sh и ~/quality-watchdog.sh)
расширены симметрично: стенду запрещены shared-postgres, food-market-prod-
postgres[-data], 5434/5437; проду — food-market-postgres-data (чужие данные).
Проверка: docker compose config/ps на prod-vm shared-postgres не упоминают,
grep по репе — ни один скрипт не делает docker exec в shared-postgres.
Третья утечка stage↔prod: stage-compose разделял с прод-compose
/opt/food-market-data/uploads И названные тома (food-market-postgres-data,
food-market-api-data, food-market-api-logs). Теперь всё разведено.
- Новый deploy/docker-compose.stage.yml — шаблон стендовского стека с
food-market-stage-* волюмами, bind /opt/food-market-stage-data/uploads,
container_name food-market-stage-*, OpenIddict Issuer по дефолту смотрит
на test.admin.food-market.kz (а не на admin.food-market.kz).
- На prod-vm сделаны: pg_dump стенда → down → переименованы stage-*-pre-
isolation-20261005 → up с новыми волюмами → pg_restore. Прод-контейнеры,
прод-том food-market-postgres-data и прод-папка /opt/food-market-data/
не тронуты (read-only inspect).
- ~/deploy-stage.sh получил guard до билда: проверяет шаблон репо и
удалённый stage-compose на запрещённые паттерны прод-путей/томов/имён/
issuer-URL.
- ~/quality-watchdog.sh +step 10 (06:XX ежесуточно) — тот же набор
паттернов + docker inspect реального стенда.
- docs/stage-prod-isolation-progress.md: таблица до/после, что оставили,
как разводили, follow-ups.
Smoke на test.admin.food-market.kz: /health/ready Healthy, 31233 товара,
invoice upload → parsing (OCR Hangfire job), GDPR org-export zip упал в
/opt/food-market-stage-data/uploads/exports/. Прод-папка /opt/food-market-
data/uploads/ до/после миграции: 13 файлов, 0 изменений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Второй проход после ручного ретеста владельца: добавил RemoteHost ssh на
dev-vm, объяснил почему e2e п.4 не поймал (не запустил), и описал
owner-policy про удаление файлов + cleanup legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner caught (повторно, после фикса 500): контейнер stage-api пытался
запустить "claude" в своём cwd (/app), а binary там нет —
"No such file or directory". Включаем RemoteHost SSH:
1. ClaudeCliInvoiceExtractor: передаёт в ssh/scp опции -i <SshKey>,
-o StrictHostKeyChecking=accept-new, -o UserKnownHostsFile=<SshKnownHosts>,
-o BatchMode=yes, -o ConnectTimeout=10. Конфиги Invoices:ClaudeCli:SshKey
и Invoices:ClaudeCli:SshKnownHosts.
2. deploy/Dockerfile.api: + openssh-client в runtime.
3. (compose + ssh-ключ на prod-vm не в репо: deploy/secrets/invoice-ocr-key,
authorized_keys на dev-vm с from="192.168.1.190",no-pty,no-forwarding;
env RemoteHost=nns@192.168.1.192, Executable=/home/nns/.local/bin/claude.)
Owner-policy: фото/сканы накладных НЕ хранить на серверах. InvoiceProcessor
удаляет StoredPath сразу после распознавания — успех и неудача; remote
tmp копия на dev-vm подчищается в finally. InvoiceWorker больше не содержит
sweeper (периодическая очистка не нужна), конфиг Invoices:KeepFilesDays
игнорируется (оставлен в комментарии для backwards compat).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Пропустил в первом коммите: OrganizationSettingsPage в простом режиме
дёргал /api/organization/telegram/status, который [RequiresAdvancedMode].
Токсичный тост силен silent-GET-403 политикой (api.ts), но menu-walk
e2e всё равно ловил 4 сетевых 403 для demo-admin → fail. Чиним у
источника: не фетчим если not advanced.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner caught on stage (2026-06-07 18:57):
1) POST /api/purchases/invoices/upload → 500: migration Phase31a создала
invoice_uploads.UpdatedAt как NOT NULL, но конвенция проекта — DateTime?
nullable + стамп только для Modified в AppDbContext.StampTimestamps.
EF на INSERT отправлял UpdatedAt=NULL → 23502. Фикс: Phase31b DROP NOT NULL
+ Phase31b для invoice_aliases.
2) Фоновые 403 → тост «Нет доступа» при F5 на любой странице:
- SavedPresets фетчил /api/user/presets (RequiresAdvancedMode) из любой
страницы с фильтрами в простом режиме → 403;
- useOnboardingProgress фетчил /api/organization/employees (Admin only)
для кладовщика/кассира на '/' → 403.
Фикс:
- api.ts: GET-403 silent, тост только на действия юзера (POST/PUT/DELETE);
- SavedPresets gated на advancedMode (useMe + enabled);
- useOnboardingProgress canSeeEmployees-gate.
3) tests/food-market.IntegrationTests/InvoiceUploadHttpTests.cs — HTTP-прогон
POST /upload через Testcontainers-Postgres (дыра: s31 pipeline tests
проверяли matcher, но ни разу не инсертили в invoice_uploads).
4) tests/e2e/scenarios/stage-ui-menu-walk.spec.ts — regress: каждая демо-роль,
каждый пункт меню, F5, assert 0 unexpected 4xx/5xx + 0 error-тостов +
0 console.error. В quality-watchdog step 9 с 4-часовой каденцией.
5) tests/e2e/scenarios/stage-ui-invoice-upload.spec.ts — demo-keeper
загружает реальное фото накладной (tests/fixtures/invoice-sample.jpg,
сгенерирована Playwright-ом из HTML, 1800×2400 JPEG) → ждёт recognition
→ экран проверки. При не-настроенном Claude CLI тест fail'ится с ясным
сообщением (это тоже баг).
Прогон локально 4/4 (InvoicePipelineTests 2/2 + InvoiceUploadHttpTests 2/2).
docs/hotfix-invoice500-403-progress.md — ход работы 1-4/5.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Playwright 1/1 passed против test.admin.food-market.kz после полной
заливки МойСклада в Демо-магазин:
- /catalog/products под demo-admin — 30 894 записей в шапке списка.
- Поиск по частичному названию «шоколад» → хотя бы одна строка.
- Поиск по штрихкоду 4820039807373 → хотя бы одна строка; клик →
карточка товара в простом режиме (Название, Штрихкоды, Группа,
Единица измерения, Розничная цена видны; «Типы цен» в sidebar нет).
Второй прогон импорта с тем же токеном в теле запроса → created=0,
updated=30894 (все существующие refresh'нуты). Идемпотентность по
MoySkladExternalId подтверждена.
Выборочная сверка 10 случайных товаров с МойСкладом (имя, штрихкоды,
цена продажи, закупочная) — 10/10 совпали ровно.
Token checks:
- Organizations.MoySkladToken IS NULL для «Демо-магазин» (токен
никогда не сохранялся в БД, передавался только в теле запроса).
- /tmp/ms-token после импорта shred -u — локально ничего не осталось.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Для полной заливки каталога МойСклада в «Демо-магазин» на стейдже
переделал импорт по требованию «идемпотентный + без дублей».
Схема:
- Phase32a_MoySkladExternalId: VARCHAR(64) на products и product_groups
+ partial-unique index (OrgId, MoySkladExternalId) WHERE IS NOT NULL.
NULL разрешён — товары, созданные вручную, без MS id.
Клиент (MoySkladClient):
- Auto-detect Basic vs Bearer: МойСклад принимает оба, но сторонние
проекты (sklad на prod-vm) используют Base64(login:password). Строку
распознаём как base64 с ':' в декоде → ставим Basic, иначе Bearer.
- StreamProductsAsync принимает includeArchived (default true для
back-compat; импортный путь передаёт false).
- GetAllUomsAsync — грузит справочник единиц измерения один раз.
Сервис (MoySkladImportService):
- MoySkladImportOptions: OverwriteExisting / SkipArchived / MergeByBarcode.
- Resolution order: MS id → barcode → article. Первый найденный
апдейтится, не плодит дубль.
- Группы — upsert по MS id; pre-существующие группы без MS id с тем же
именем в том же родителе — подхватываются (MS id проставляется).
- Товары из архива MoySklad (archived=true) полностью пропускаются
(счётчик SkippedArchivedInSource в результате).
- Barcode-дубли внутри MoySklad: первый встреченный выигрывает, остальные
пропускаются с записью в Errors (счётчик SkippedDuplicateBarcodeInSource).
- UOM: MS uom.id → MS uom.code → наш локальный Code. Весовые товары
(weighed=true) всегда ставятся в «кг».
- Расширенный MoySkladImportResult: Created / MergedByBarcode /
SkippedArchivedInSource / SkippedDuplicateBarcodeInSource / GroupsUpdated.
Контроллер (MoySkladImportController):
- ImportRequest получает SkipArchived (default true), MergeByBarcode
(default true). Токен по-прежнему принимается в body и НЕ сохраняется
в Organizations.MoySkladToken, если передан в теле запроса.
- Финальный Message прогресса собирает все счётчики.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- stage-ui-menu-links.spec.ts: убрали describe.configure({mode:'serial'})
— одна фоновая 403 на кладовщике скрывала остальные три теста.
Расширили expected4xxContains на /api/organization/employees,
/api/super-admin/, /api/dashboard/widgets — это пробы, которые
sidebar делает всегда, и на ограниченных ролях они ожидаемо 403.
- docs/menu-links-progress.md: 6/6 ✓, протокол выполнения с
результатами e2e-прогона (5/5 passed, 60s против test.admin).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>