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

153 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Порт изменений 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'`;
- плашка «Черновик приёмки № <supplyNumber> создан <processedAt>» со
ссылкой на `/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'ов не нужно.