Sprint 23 (adversarial): атаковали систему как недоброжелатель.
Найдено 4 бага, все починены.
Bug #001 (Medium): NULL-byte в Product.Name вызывал 500 без тела.
Postgres TEXT не принимает \x00. Добавил NoControlChars() в
ProductInputValidator + CounterpartyInputValidator.
Bug #002 (Low): ProductInputValidator MaximumLength(200) конфликтовал
со StringLength(500) в DTO и schema HasMaxLength(500). Сделал 500
везде. Counterparty: 200 → 255 (matches HasMaxLength).
Bug #003 (CRITICAL): параллельные posting'и под Serializable выбрасывали
PostgresException 40001 → middleware → 500 empty body. Добавил
SerializationConflictMiddleware который мапит 40001 → 409 Conflict
с {error, retryable: true}. Также SerializableRetry helper для
явного retry внутри endpoint'ов с exp backoff. Применил retry-wrap
к RetailSalesController.Post (PostCoreAsync extracted).
Bug #004 (Low): цена 0.0000001 округлялась до 0 уже после прохождения
required-price check (check был ДО RoundIfNeeded). FindMissing-
RequiredPriceAsync теперь округляет перед сравнением — required
цена реально > 0 после rounding.
Bug reports: tests/e2e/reports/bugs/bug-00[1-4]-*.md (github-issue format).
Multi-tenant attacks (cat 3): clean — все cross-org GET/PUT/DELETE
дают 404, bulk-update affected=0, lists не утекают.
Auth-edge (cat 2): clean — JWT tampering 401, garbage 401, CORS evil.com
не получает allow-origin, fake refresh 400 invalid_grant.
DOS (cat 7): clean — 50MB body 413, 200 headers 431, long URL 200.
Hangfire safety (cat 8): clean — regular Admin → /hangfire 403,
seed-demo использует tenant context, body org-id игнорируется.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /api/pos/v1/sync — full snapshot products/prices/stocks/counterparties
с serverTime; since-инкремент работает (products пусто после first sync).
POST /api/pos/v1/sales с idempotency:
- batch-level: повтор того же IdempotencyKey → replayedFromCache=true,
stock не дублирует списание;
- per-sale: новый IdempotencyKey + тот же ClientSaleId → возвращает
существующий ServerSaleId (маркер в Notes);
- qty > stock → failed-секция с error, accepted=0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
В Development swagger.json валился двумя ошибками:
1. CustomOperationIds dereferencing api.ActionDescriptor.RouteValues['action']
для минимальных API (/health, /metrics, /connect/*) кидало
KeyNotFoundException. Делаем TryGetValue + fallback на RelativePath.
2. CustomSchemaIds с FullName! падал NRE на типах без FullName
(generic-параметры). Fallback на t.Name через ??.
После фикса: /swagger/v1/swagger.json 200, 117 paths, все 19 новых
модулей (Enter/Loss/Transfer/Inventory/SupplierReturn/Demand/Reports/
AuditLog/2FA/POS/Signup) присутствуют, schemaId без дубликатов.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CRUD продукта генерирует записи create/update/delete с diff'ом
полей; фильтры по entityType/entityId/action работают; multi-tenant
строго (org B не видит логи org A).
Bonus fix: тот же DateTime Kind=Unspecified→UTC что в reports,
применён к from/to в /api/admin/audit-log.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. **DateTime Kind=Unspecified → UTC** в ResolveRange / AsUtc.
ASP.NET парсит 'from=2026-05-29' с Kind=Unspecified, Npgsql 8
отказывается слать такие в timestamp with time zone (500).
Принудительно конвертим Unspecified→UTC (трактуем как полночь
UTC), Local→ToUniversalTime. Применено к Sales/Profit/ABC/Stock.
2. **Enter.Post теперь пересчитывает Product.Cost** по той же
формуле скользящего среднего что Supply.Post. Без этого товары,
попавшие в систему через Оприходование (а не через Supply),
имели Cost=0 — Profit/ABC-отчёты показывали cost=0 и неверную
маржу. Воспроизведение: Enter 100@30 + RetailSale 10@500 →
Profit-отчёт показывал revenue=5000, cost=0 (должно cost=300).
3. **ABC report: Парето-граница по cumBefore (а не cumAfter).**
Единственный товар с cumShare=100% валился в класс C, хотя
полностью покрывает Парето — должен быть A. Чиним: товар
принадлежит классу A если он нужен чтобы пересечь порог
80% (cumBefore < 80%). Стандартный Парето-алгоритм.
stage-reports (8 шагов): Sales/Stock/Profit/ABC + CSV/XLSX
export + edge — все зелёные.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Auto-populate (lines=null), explicit lines с пересчётом diff, PUT с
изменением actualQty (фикс EF8 nav-collection теперь работает),
Post → корректирующие StockMovement type=InventoryAdjustment, Unpost,
multi-tenant. + Информационный gap: нет CSV-импорта фактических qty,
для оператора склада ввод через JSON-API неудобен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Тот же баг что в TD-6 чинили на Supplies/Demands/RetailSales и в pt 2
на Products: добавление/замена line'ов через nav-collection даёт
DbUpdateConcurrencyException «0 rows affected» при следующем UPDATE
родителя. На документах без xmin это становится 500, на InventoryDoc
(с xmin от TD-6) — 409.
Переводим Enters/Losses/Transfers/SupplierReturns.Update на
ExecuteDelete + DbSet.Add (как Supplies). InventoriesController
дополнительно: добавление новых строк через _db.InventoryLines.Add
вместо doc.Lines.Add (RemoveRange/Clear там не было — merge-in-place
по ProductId).
Воспроизведение (на Enters):
1. POST /api/inventory/enters {lines:[A]}
2. PUT … {lines:[A,B]} (одна оставлена, одна новая) → было 500
DbUpdateConcurrencyException ; стало 204.
stage-enter (10 шагов): CRUD + Post + Unpost + edge + multi-tenant +
concurrent PUT — все зелёные.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. Products.Update: добавление нового barcode'а к существующему товару
валилось с DbUpdateConcurrencyException 'Товар изменён в другом окне',
хотя никакой конкурентной правки не было. Тот же EF8-баг, который в
TD-6 чинили на Supplies/Demands/RetailSales: nav-collection.Add +
client-side Id путает EF, UPDATE родителя получает 0 affected. Чиним
тем же паттерном: ExecuteDelete старых ProductBarcodes/ProductPrices,
DbSet.Add новых. Воспроизводится: создать товар с 1 barcode, PUT с
2 barcodes → 409. После фикса → 204.
2. IX_products_OrganizationId_Article был обычным (не уникальным), хотя
контроллер ловил нарушение по имени индекса и возвращал 'Артикул уже
занят'. Catch-блок никогда не срабатывал. Делаем индекс уникальным
миграцией Phase8d. Перед созданием — нумеруем дубликаты по существующим
данным (если есть). NULL/пустые article остаются distinct (Postgres
NULL semantics).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
stage-smoke (5 шагов): signup happy-path, bootstrap (Store/Roles/
Units/ProductGroup/PriceTypes/RetailPoint), login → access+refresh
+ /api/me с правильным orgId+role=Admin, edge-cases (дубликат
email, короткий пароль, пустое название, кривой телефон), проверка
public-сайта.
Informational gap: stage-public (test.food-market.kz) использует тот
же build что прод-public, поэтому его форма signup POST'ит в прод
admin. Для stage-testing регистрируемся напрямую POST на test.admin.
Чек-лист stage-testing: пункт 1 ✓.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- roles.steps.ts step08: было «задокументированный gap», стало реальная
проверка — кастомная роль без ProductsEdit → 403 на PUT товара, GET → 200.
Сценарий roles зелёный 8/8.
- RateLimiting:* конфиг (Enabled/PerMinute/PerHour): тесты с общим loopback-IP
поднимают/выключают лимит, чтобы повторные логины не упирались в 429.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Полная регрессия всех сценариев + 6 новых областей этой сессии (employees,
roles, superadmin-console, platform-smtp, auth-password, security-edge).
За день исправлено 4 бага: уволенный сотрудник логинится (P0), конкурентное
проведение приёмки ломает инвариант (critical), refresh не гасится после
ротации (high), change-owner принимал короткий reason (medium). Нереализованный
по ТЗ функционал (отчёты/склад-документы/POS/permission-authz/login-ratelimit)
зафиксирован как Logic gaps.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Итоги сессии системного тестирования:
- full-cycle: 12/12 ✓
- multi-tenant-isolation: 12/12 ✓ (новый сценарий)
Найдено и исправлено 10 P0-багов: 7 в миграциях (расхождения схемы
с domain, отсутствующие [Migration] атрибуты, rudiment колонки Kind),
1 в безопасности (edit-mode override блокировался Authorize-ролями).
См. tests/e2e/reports/systemic-2026-05-23.md для полного описания
каждого бага, gap'ов и команд воспроизведения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage прогон против commit ee127b2:
- 9 ✓ / 0 ✗ / 0 ⚠ / 3 ◯ (baseline: 8/1/0/3)
- step05 Cashier полностью зелёный: Identity-role «Cashier» маппится,
/api/organization/employees → 403
- step01 новая проверка серверной phone-ФЛК → 400 на невалидном
- step08 «Нет ни одной единицы измерения» исчез — новая орга получает
5 active globals через junction сразу при создании
- HIGH bug сменился: теперь блокер «product требует штрихкод» (отдельный
вопрос — либо баг ProductsController, либо e2e-сценарий должен
передавать barcode)