food-market/docs/sklad-0610-progress.md
nns e45600df4b
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
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
feat(invoices+catalog): порт улучшений 06.10.2026 из sklad
Основное:

* 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>
2026-10-07 19:50:03 +05:00

12 KiB
Raw Permalink Blame History

Порт изменений 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.

Чеклист

  • 1. Неявный [Required] у non-nullable string — выключить глобально, убедиться что FluentValidation прикрывает все обязательные поля, ответ в ProblemDetails + журнал. Integration-тест: SaveLines/apply со всеми необязательными полями = null → 200.
  • 2. Снимки фото-приёмки удаляются сразу после apply (проверка на стенде: 0 файлов этой накладной в uploads/invoices). Брошенные — 24 ч.
  • 3. Поставщик черновика — служебный «Поставщик не указан» по умолчанию. Никакого автоподбора по названию, никакого автозаведения. На экране проверки — необязательный выбор из существующих контрагентов.
  • 4. Готовая накладная (status=applied/done) — только просмотр: плашка со ссылкой на приёмку + readOnly поля; invalidateQueries после apply.
  • 5. Текст ошибки apply: error → title/detail → (код ответа).
  • 6. Список накладных: колонка «Загружена» с корректным UTC→локаль (добавить Z если нет) + мобильная карточка.
  • 7. Убрать «заполнить существующий черновик» (targetSupplyName, FillDraft, SupplyPicker) если есть.
  • 8. Проверка на стенде реальными запросами: Excel-накладная + 3 товара без выбранного поставщика → «Поставщик не указан»; с выбранным поставщиком; applied-накладная readonly; фото-накладная → apply → файлы удалены. Меню-прогон (quality-watchdog step 9) зелёный.
  • 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'ов не нужно.