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
Основное:
* 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>
12 KiB
12 KiB
Порт изменений 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 = trueConfigureApiBehaviorOptions.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: после applyhasFiles=falseи физический StoredPath не существует.
Пункт 3 — служебный «Поставщик не указан»
- Phase32b миграция: добавила колонку
IsPlaceholder boolнаcounterparties- partial unique-ish index по
IsPlaceholder=true(быстрый lookup, одна запись на орг — поддерживается конкуррентно).
- partial unique-ish index по
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":"Служебный контрагент «Поставщик не указан» не удаляется."}.
- A→F: upload Excel-накладной + SaveLines с null во всех
необязательных полях + apply БЕЗ поставщика → 200 OK,
создан черновик
- 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](commit921bca0, 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'ов не нужно.