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>
153 lines
12 KiB
Markdown
153 lines
12 KiB
Markdown
# Порт изменений 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'ов не нужно.
|