# Порт изменений sklad 06.10 → food-market Источник: `~/ref/sklad` (b7eb637..894d057 + 1e4906b docs). Handoff: `~/ref/sklad/docs/handoff-2026-10-05-nakladnye-inventarizaciya.md` (разделы «Если ты уже перенёс версию от 05.10 — что поменять», 10, 11, 12). Ограничения владельца: не трогать sklad (ref only), прод, shared-postgres, global.json, gateway nginx. ## Чеклист - [x] 1. Неявный [Required] у non-nullable string — выключить глобально, убедиться что FluentValidation прикрывает все обязательные поля, ответ в ProblemDetails + журнал. Integration-тест: SaveLines/apply со всеми необязательными полями = null → 200. - [x] 2. Снимки фото-приёмки удаляются сразу после apply (проверка на стенде: 0 файлов этой накладной в uploads/invoices). Брошенные — 24 ч. - [x] 3. Поставщик черновика — служебный «Поставщик не указан» по умолчанию. Никакого автоподбора по названию, никакого автозаведения. На экране проверки — необязательный выбор из существующих контрагентов. - [x] 4. Готовая накладная (status=applied/done) — только просмотр: плашка со ссылкой на приёмку + readOnly поля; invalidateQueries после apply. - [x] 5. Текст ошибки apply: `error → title/detail → (код ответа)`. - [x] 6. Список накладных: колонка «Загружена» с корректным UTC→локаль (добавить Z если нет) + мобильная карточка. - [x] 7. Убрать «заполнить существующий черновик» (targetSupplyName, FillDraft, SupplyPicker) если есть. - [x] 8. Проверка на стенде реальными запросами: Excel-накладная + 3 товара без выбранного поставщика → «Поставщик не указан»; с выбранным поставщиком; applied-накладная readonly; фото-накладная → apply → файлы удалены. Меню-прогон (quality-watchdog step 9) зелёный. - [x] 9. ~/deploy-stage.sh, обновить docs/supply-inventory.md. ## Журнал решений ### Пункт 1 — SuppressImplicitRequired + ValidationProblemDetails + журнал - `food-market.api/Program.cs`: `SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true` + `ConfigureApiBehaviorOptions.InvalidModelStateResponseFactory` → `ValidationProblemDetails` (humanizeError на фронте уже умеет), Serilog Warning с путём и полями. - `InvoiceModels.cs`: `Name`, `Source`, `PhotoHint.FilePath` → `string?`. - Прошёлся по DTO-input'ам контроллеров (MeAccountController, AuthForgot/Signup, TwoFactor, UserPresets, Loyalty, Feedback, MoySkladImport, SuperAdminEmployees/ Organizations): у всех обязательных текстовых полей есть ручной `IsNullOrWhiteSpace`-guard — после снятия implicit [Required] они не начнут принимать null. - Добавлены валидаторы `InvoiceSaveLinesValidator` и `InvoiceApplyValidator` (NotNull Lines, Guid.Empty на Store/Currency = ошибка; SupplierId опционален). - Integration-тест `SaveLines_with_all_optional_fields_null_returns_204` ✓ (все поля InvoiceLine = null → 204 NoContent). ### Пункт 2 — файлы фото-приёмки удаляются сразу - У нас это уже было (`InvoiceProcessor.DeleteStored(up)` вызывается в `Apply` без условия на Source — см. комментарий «owner-policy 2026-10-05» в контроллере). - Регрессия закрыта тестом `Applied_invoice_detail_has_processed_at_and_files_cleaned_up`: после apply `hasFiles=false` и физический StoredPath не существует. ### Пункт 3 — служебный «Поставщик не указан» - Phase32b миграция: добавила колонку `IsPlaceholder bool` на `counterparties` + partial unique-ish index по `IsPlaceholder=true` (быстрый lookup, одна запись на орг — поддерживается конкуррентно). - `CounterpartiesController`: - в `List` новый `OrderBy(c => c.IsPlaceholder)` — служебный всегда последним во всех сортировках; - `Update` и `Delete` при `IsPlaceholder=true` → 409 Conflict. - `CounterpartyDto.IsPlaceholder` + Mapster-config обновлены — фронт умеет пометить его бейджем «служебный». - `InvoicesController.ApplyDto.SupplierId` → `Guid?`; если null или Guid.Empty — `ResolvePlaceholderCounterpartyAsync` ищет/создаёт. - Поставщика из накладной в комментарий документа **не** пишем (sklad §11). - Автоподбора/автозаведения контрагентов нигде не было — специально проверил grep'ом по `search=`/`AgentHref`/похожему: чисто. - Integration-тесты: `Apply_without_supplier_uses_placeholder_counterparty_and_is_idempotent` (две накладные → один служебный), `Delete_and_update_of_placeholder_counterparty_returns_409`. ### Пункт 4 — готовая накладная readOnly - `InvoiceReviewPage.tsx`: - новый флаг `applied = d.status === 'applied'`; - плашка «Черновик приёмки № создан » со ссылкой на `/purchases/supplies/{supplyId}`; - шапка (поставщик/№/дата/итого) + поля Кол-во/Цена в карточках — `readOnly disabled`; - скан-блок и кнопка «Создать приёмку» скрыты; - `applied` вклейка в карточку: удалить/найти товар — тоже спрятаны. - `supplyNumber` + `processedAt` добавлены в `InvoiceDetailDto`. - После успешного `apply` — `qc.invalidateQueries(['invoice', id])` + `qc.invalidateQueries(['invoices-list'])`, экран сразу переходит в readOnly. ### Пункт 5 — текст ошибки apply - `ApplyDialog`: `error → detail → title → (код)`. У нас humanizeError уже делает похожее на уровне axios, но здесь оставлена форма с явным кодом HTTP — на мобильном он помогает понять, что это сервер, а не связь. Диалог послан с `__silent: true` чтобы наш глобальный тост не дублировал локальное сообщение. ### Пункт 6 — колонка «Загружена» (UTC → локаль) - `parseServerDate(s)` помощник в `InvoicesPage.tsx`: добавляет `Z` суффикс, если его нет (на всякий случай — EF Core обычно шлёт Z, но некоторые промежуточные middleware'ы его теряли в sklad; у нас не словили, но подстелили соломку). - `uploadedAt` подпись в hashmap карточки + отдельная строка «загружена …» в бейдже статуса (видна в десктопе и в мобильной карточке — у нас одна и та же карточка). - `List` на бэке дополнен `ProcessedAt` в DTO для будущих виджетов. ### Пункт 7 — targetSupplyName / FillDraft / SupplyPicker - grep по всей репе — не нашлось: ни в API, ни в web, ни в shared. Этот sklad-ный сценарий никогда к нам не портировался. ### Пункт 8 — stage retest - `~/deploy-stage.sh` → build+push+redeploy → `/health/ready` зелёный. - Прогон real-request smoke на `https://test.admin.food-market.kz` через SuperAdmin + X-Org-Override на Демо-магазин (сначала `/api/admin/seed-demo` — демо был пуст после миграции на новый stage postgres): - A→F: upload Excel-накладной + SaveLines с null во всех необязательных полях + apply БЕЗ поставщика → **200 OK**, создан черновик `П-2026-000001` на «Поставщик не указан», detail показывает `status=applied, supplyNumber=П-2026-…, processedAt=…, hasFiles=false`. - G: supply выдан c `supplierName="Поставщик не указан"`. - H: в списке контрагентов 11 записей, служебный **последний**, `isPlaceholder=true`, других placeholder'ов нет. - I: вторая накладная apply без поставщика → тот же служебный, дубля не завелось. - J: третья накладная с явным `supplierId=…ИП Бегимбаев К.Т.` → `supplierName="ИП Бегимбаев К.Т."` в приёмке, 1 строка. - K: `DELETE /api/catalog/counterparties/{placeholder}` → **409** `{"error":"Служебный контрагент «Поставщик не указан» не удаляется."}`. - Menu-walk (quality-watchdog step 9), `E2E_ADMIN_URL=https://test.admin.food-market.kz`: **3 passed (3.7m)**. demo-admin 28 ссылок, demo-keeper 17, demo-cashier 7, 0 неожиданных 4xx/5xx, 0 toast.error, 0 console.error. - Фото-накладная через Claude CLI не гонял: пайплайн `apply→DeleteStored` закрыт `hasFiles=false` в F и integration-тестом; реальная съёмка с телефона — follow-up когда владелец пришлёт реальные снимки. - 1 пред-существующий фейл в интеграционных: `OrgAuditLogTests.Counterparty_update_diff_contains_before_after` → 403 от `/api/admin/audit-log` из-за `[RequiresAdvancedMode]` (commit 921bca0, s29). К порту sklad-а отношения не имеет; не чиню. - 1 пред-существующий фейл в юнит-тестах: `ApiReferenceDocsJobTests.Scans_endpoints_with_doubly_nested_generic_return` — не нашёл TestEdit permission в сгенерированной markdown-таблице. Чисто регекс-тюнинг, не моего портфеля. ### Пункт 9 — supply-inventory.md + deploy-stage.sh - `docs/supply-inventory.md`: шаг 7 расписан под новое поведение (поставщик необязательный, служебный «Поставщик не указан», без создания контрагента), шаг 8 (applied → readOnly) добавлен, в табличку «Файлы» дописана новая миграция Phase32b. - `~/deploy-stage.sh` ничего не менял — он уже прогнался успешно с нашим билдом; никаких правок guards'ов не нужно.