173 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
05e7fdecef |
feat(s31): CameraScanner + scan-mode приёмки с last-purchase price (3/8)
CameraScanner (components/CameraScanner.tsx) — порт из sklad'a @zxing/
browser + @zxing/library. Непрерывное сканирование: окно не закрывается
между сканами, EAN13/EAN8/UPC/Code128/ITF. Подавление повтора 1.5с (в
кадре товар распознаётся десятки раз в секунду). Вибрация 40мс + flash
зелёный/красный + анимация scanPop/scanRing.
SupplyScanPage (/purchases/supplies/scan):
- Выбор поставщика (search по /counterparty-search), склада, валюты.
- Кнопка 'Сканировать камерой' → <CameraScanner /> на полный экран.
- Alt: аппаратный сканер — <input> в details-блоке, Enter = +1.
- Каждый скан: GET /product-by-barcode → если нашёлся, +1 к
существующей строке или новая сверху. Цена подставляется last-
purchase-price (из последней Posted supply-line по этому товару).
- Неизвестный штрихкод: feedback 'Нет в каталоге', вибрация ошибки.
Товар создавать — отдельный переход на /products/new (future wiring).
- Прогресс сохраняется в localStorage ('fm.supply-scan.rows') —
перезагрузка не теряет; очищается после успешного сохранения.
- Save → POST /api/purchases/supplies (существующий endpoint), статус
Draft; редирект в /purchases/supplies/:id для проверки/проведения.
InvoicesController.ProductByBarcode теперь возвращает ещё и
lastPurchasePrice (из последней Posted supply-line).
@zxing/browser@0.1.5 + @zxing/library@0.23.0 добавлены в deps.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
11b43aefe5 |
feat(s31): приёмка по фото — upload → Claude CLI → match → Supply Draft (1/8)
Backend-pipeline по образцу ~/ref/sklad/sklad/Invoices/, но документы
создаются в НАШЕЙ базе (Supply), не в МойСкладе.
Домен (food-market.domain/Purchases/InvoiceUpload.cs):
- InvoiceUpload: файл + статус (pending/parsing/needs_review/applied/failed)
+ распознанная шапка + ParsedJson (jsonb) + ссылка на созданный Supply.
- InvoiceAlias: запомненное соответствие (SupplierKey, SourceCode|
SourceNameNorm) → ProductId. Два частично-уникальных индекса (filter
SourceCode/SourceNameNorm IS NOT NULL).
Миграция Phase31a (идемпотентная ADD/CREATE IF NOT EXISTS; [Migration]+
[DbContext]).
Infrastructure/Invoices/:
- IInvoiceExtractor + ParsedInvoice/InvoiceLine DTO.
- ClaudeCliInvoiceExtractor — порт из sklad: 'claude -p --output-format
json', промпт один-в-один (копировать все строки, null если не читается,
barcode != supplier_code, сверка с «Итого»). Поддерживает local и
Invoices:ClaudeCli:RemoteHost (ssh+scp+cleanup).
- InvoiceMatcher — barcode → alias → none. По названию НЕ ищем
(sklad-урок: 2-5 ошибок из 10, подмена хуже пустой строки).
- InvoiceProcessor + InvoiceWorker (BackgroundService):
pending → parsing → needs_review | failed. SweepAsync раз в час
удаляет файлы старше Invoices:KeepFilesDays (default 7); строки
остаются. Multi-tenant через IgnoreQueryFilters + явный OrgId.
Controller api/purchases/invoices:
- POST /upload (SuppliesEdit, 60 MB max): файлы в uploads/invoices/{yyyy-
MM}/, запись в pending.
- GET /, GET /{id} — список + детали со строками.
- GET /{id}/file — отдать файл.
- DELETE /{id} — удалить запись + файл.
- PUT /{id}/lines — правки шапки + строк + авто-upsert InvoiceAlias
для каждой строки с MatchedBy=manual|alias (два ключа: по code и по
name_norm).
- POST /{id}/apply — создать Supply (Draft) с данным поставщиком/
складом/валютой, статус → applied, SupplyId ссылается, файл удаляется.
- GET /product-by-barcode, /counterparty-search — помощь UI.
Build зелёный. Web + тесты — следующими шагами.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e1718ec972 |
feat(s30): sentinel pwd для SPA — реальный мастер-пароль только в server-env
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 Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (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
Координатор отротировал мастер-пароль стенда: теперь значение живёт в ~/.fm-watchdog/stage-shared-password на dev-vm (не коммитится) и в QUICK_LOGIN_MASTER_PASSWORD в stage-env на prod-vm. Старый дефолт '123456' на обоих сторонах убран — он мог вводить в заблуждение и был записан в бандл. Клиент-серверный контракт: - Server (AuthorizationController.IsMasterPassword) принимает ДВА пароля как мастер-ключ: (a) QuickLoginOptions.MasterPassword (константное сравнение, FixedTimeEquals); (b) публичный sentinel QuickLoginOptions.SentinelPassword = '__QUICK_LOGIN_PWLESS__'. Оба гейтованы EffectivelyEnabled — вне стенда отбиваются как любой чужой пароль. - SPA (LoginPage.handleQuickLogin) при клике карточки шлёт sentinel в поле password. Реальный пароль в JS-бандл больше не попадает; ротация секрета больше не требует rebuild'а веба. - Форму мы НЕ наполняем sentinel'ом — поле password остаётся пустым, чтобы ни одно значение не светилось в DOM. Default QuickLoginOptions.MasterPassword = "" (пустая строка не матчится в IsMasterPassword). deploy/docker-compose.yml дефолт QUICK_LOGIN_MASTER_PASSWORD тоже пустой. Тесты: - integration (3/3 ok) — добавлены sentinel-кейсы: пропускает активного при Enabled=true, отвергается при Enabled=false. Admin12345!-санити- логин в Disabled-тесте перенесён вперёд, чтобы снять lockout-счётчик от соседей по коллекции (Identity lockoutOnFailure=true). - e2e stage-quick-login (4/4 ok) — ql02 шлёт sentinel вместо '123456' (симулирует реальный SPA flow). docs/quick-login.md: - без упоминания реального пароля; хранилище — ~/.fm-watchdog/ stage-shared-password; ротация-рецепт через python читает файл в-процессе (не экспонирует значение в ps/логи). - раздел «Доступ к стенду» — host-nginx basic-auth для внешнего интернета, 192.168.1.x бьёт без неё. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f817abc8c7 |
fix(s30): QuickLoginDemoSeeder — свежий scope на каждый sub-step
Разбил монолитный SeedAsync на 4 метода, каждый со своим DI-scope и DbContext'ом: 1) EnsureDemoOrgAsync — org Демо-магазин; 2) EnsureTenantReferencesAsync — единицы/магазин/роли через DevDataSeeder.SeedTenantReferencesAsync; 3) TryFillCatalogAsync — DemoTenantSeeder (товары/приёмки/продажи); 4) EnsureDemoUsersAsync — трио демо-юзеров. Корень проблемы на стенде: DemoTenantSeeder падает с «Tenant not set» (при старте нет HTTP-контекста → ITenantContext.OrganizationId пуст → StockService.ApplyMovementAsync бросает). Если переиспользовать один scope, следующий UserManager.CreateAsync возвращает DbUpdateConcurrencyException — DbContext транзакционно сломан. Теперь: catalog-step изолирован и его падение не отравляет создание юзеров. Catalog нужен только для показательного UI — он опциональный, с явным логом «ОК — создать можно через POST /api/admin/seed-demo после логина». Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
e3a8db3206 |
fix(s30): QuickLoginDemoSeeder больше не кладёт API при сбое
Весь StartAsync обёрнут try/catch — dev-удобство для стенда не должно класть API. Если сид упал (DbUpdateConcurrencyException и т.п.) — Warning в лог, SuperAdmin + уже существующие аккаунты по мастер-паролю работают. Demo-trio можно будет досеять руками или через POST /api/admin/seed-demo если нужно. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ee24a5ed8e |
fix(s30): прод-хост в QuickLoginOptions — точное совпадение, не suffix (шаг 9/9)
Критический фикс: раньше safety-override срабатывал на host.EndsWith( ".admin.food-market.kz") — это ВКЛЮЧАЛО сам стейдж (test.admin.food-market.kz заканчивается на ".admin.food-market.kz"). Деплой на стенд это и показал: Warning в логе "test.admin.food-market.kz — это ПРОД" и /api/dev/quick- login/accounts возвращал 404. Теперь проверка: host == "admin.food-market.kz" (точное совпадение). Любой поддомен (test.admin.*, dev.admin.*) — стенд, быстрый вход включается. Если в будущем появится второй прод-хост — дописать условие явно. Также добавлено: - deploy/docker-compose.yml: пробрасывает QuickLogin__Enabled и QuickLogin__MasterPassword из env (default off). - docs/quick-login.md: как работает, предохранители, как выключить, как проверить прод. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c69114f8ab |
feat(s30): PruneAutoTestEntities + одноразовая зачистка стейджа (шаг 7/9)
HousekeepingJobs.PruneAutoTestEntitiesAsync — Hangfire-recurring (02:45 UTC
daily, Cron:PruneAutoTestEntities). Удаляет:
- orgs по regex autotest-паттерна в имени (quality-, soak-, UI-a11y-,
SmokeOrgA/B-, PerfBaseline-, Reg-, RT-ratetest-, Probe Org, cat3*,
visual-, factory-smoke-, mt6*, doc4*, auth2*, rep5*, rt8*, wiz9*,
i18n*, twofa*, iso*, mob1*, axdet-, load-, s19-test- + любое имя
с [0-9]{10,}) старше 3 дней + все их таблицы с OrganizationId;
- orphan autotest-юзеров (без удалённой org) по regex в email и доменам
@e.kz / @test-fm.local.
Защищены: SuperAdmin (OrganizationId IS NULL), admin@food-market.local,
admin@demo-market.local, demo-*@food-market.local, market@email.com,
«Demo Market» и «Демо-магазин» — явные исключения.
deploy/stage-prune-autotest.sql — ровно та же логика как SQL-скрипт для
ручного запуска. Прогнал на stage: было 8653 user'ов / 8634 orgs →
стало 300 users / 305 orgs. SuperAdmin/demo/market остались.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
35f633e206 |
feat(s30): QuickLoginDemoSeeder — организация + Admin/Cashier/Keeper (шаг 4/9)
Hosted-сервис, который на старте, когда EffectivelyEnabled=true, создаёт:
- организацию «Демо-магазин» (отдельная от «Demo Market» DevDataSeeder'а,
чтобы не ломать e2e, завязанные на DemoMarket);
- tenant references через существующий SeedTenantReferencesAsync (единицы,
магазин, PriceType, системные роли);
- каталог демо-товаров через DemoTenantSeeder (50 товаров + 30 продаж +
приёмки/контрагенты) — идемпотентно, маркер DEMO-*;
- три учётки с ролями Admin/Cashier/Storekeeper:
demo-admin@food-market.local
demo-cashier@food-market.local
demo-keeper@food-market.local
(пароль Admin12345!, но мастер-пароль 123456 тоже подойдёт — через
QuickLoginOptions в AuthorizationController).
На проде (EffectivelyEnabled=false) сервис тихо выходит в StartAsync —
ничего не трогает, ничего не логирует.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ed5089bac3 |
feat(s30): /api/dev/quick-login/accounts — анонимный список для стенда (шаг 3/9)
Анонимный эндпоинт, который ВИДЕН только когда QuickLogin эффективно включён (EffectivelyEnabled=true). В проде отдаёт 404, чтобы по наличию эндпоинта нельзя было вычислить среду. Фильтрация: - IsActive=false → не показываем; - автотестовые (prefix cat31-/visual-/factory-/mt6*/doc4*/auth2*/rep5*/ rt8*/wiz9*/quality-/i18n*/twofa*/a11y-/mob14-/mob15-/iso*/axdet-/ smoke-/ratetest-/perf-/probe-/load-/s19-test-/admin-/cashier-/ keeper-) → не показываем; - email@e.kz, @test-fm.local — служебные домены diag-probes, тоже скрыты; - длинный числовой суффикс [0-9]{10,} в email → автотест. Порядок: SuperAdmin → demo → остальные (ThenBy email для стабильности). Лимит 50 + поиск ?q по email/full-name/org-name/role (case-insensitive). Карточка: email, FullName, роль (SuperAdmin / EmployeeRole.Name), название org, флаги IsSuperAdmin/IsDemo для сортировки/отображения. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
7647af7527 |
feat(s30): мастер-пароль в /connect/token при EffectivelyEnabled (шаг 2/9)
AuthorizationController теперь после провала обычного CheckPasswordSignIn даёт второй шанс через QuickLoginOptions.IsMasterPassword — FixedTimeEquals, чтобы не светить длину. Включается только когда EffectivelyEnabled()=true (т.е. Enabled=true И Issuer не prod). Что сохранено: - неактивные пользователи отшиваются до пароля (user.IsActive сверху); - 2FA / org-check — после master-password пути ВСЁ ЕЩЁ работают; - rate-limit (UseAuthFormUsernameKey + UseRateLimiter) отрабатывает ДО нас, никак не ослабляется; - собственный пароль продолжает работать — не ломает автотесты и seeded admin'ов, которые используют Admin12345!. Warning в лог при каждом срабатывании — чтобы в прод-логах (если кто-то по ошибке активировал) было видно. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1f70d90485 |
feat(s30): QuickLoginOptions с safety-override по Issuer (шаг 1/9)
Dev-only флаг «быстрого входа». Enabled=false по умолчанию, подхватывает QuickLogin:Enabled и QuickLogin:MasterPassword из конфига (планируется проброс через env в stage-compose). EffectivelyEnabled() — жёсткий предохранитель: даже с Enabled=true, если OpenIddict:Issuer указывает на admin.food-market.kz (прод) — считаем выключенным. Пустой Issuer тоже отбивает — не рискуем в неизвестном окружении. Program.cs логирует Warning на старте: включили → «QuickLogin включён для стенда», Enabled=true но прод → «не активирован», Enabled=false — молчим. Чтобы ошибка конфигурации всегда была видна в логах прод-инстанса. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
921bca076c |
feat(s29): [RequiresAdvancedMode] + гейтинг скрытых модулей (шаг 3/8)
- RequiresAdvancedModeAttribute (TypeFilterAttribute → IAsyncAuthorizationFilter): читает Organization.IsAdvancedMode по org_id-claim через AppDbContext, кэш 30 с, инвалидация после переключения. SuperAdmin bypass. На отказ возвращает 403 ProblemDetails с code=advanced-mode-required. - Атрибут навешен на контроллеры: Sales/DemandsController, Inventory/TransfersController, Loyalty/LoyaltyCardsController + LoyaltyProgramsController, Promotions/PromotionsController, Reports/AbcReportController, Admin/OrgAuditLogController, Telegram/TelegramBindingController, Common/UserPresetsController. - И на отдельные действия: Catalog/PriceTypesController POST/PUT/DELETE (GET доступен — товары используют retail-прайс), Catalog/ProductsController POST /bulk-update. - SuperAdmin toggle (шаг 2) инвалидирует кэш фильтра после SaveChangesAsync. - SSO (Auth/ExternalAuthController) гейтится UI, а не атрибутом: endpoint инициируется анонимом без org_id в токене. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
202763e0d0 |
feat(s29): SuperAdmin advanced-mode toggle + /api/me.advancedMode (шаг 2/8)
- PUT /api/super-admin/organizations/{id}/advanced-mode { enabled: bool }
— только SuperAdmin, админ орги → 403 от [Authorize(Roles=...)].
- Idempotent: совпадающее состояние → 200 без записи в аудит; разное →
запись SuperAdminAuditLog (EnableAdvancedMode / DisableAdvancedMode)
с before/after + IP. Архивную org переключать нельзя (409).
- /api/me теперь отдаёт advancedMode (bool) — читается из Organization
с IgnoreQueryFilters (SuperAdmin без org → false — работает через
/api/super-admin/...).
- OrgRow / OrgDetail в SuperAdmin list / detail дополнены IsAdvancedMode
для бейджа «Простой/Расширенный» (шаг 6).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
99b84132ba |
fix(s28): api-reference handle ~/path ASP.NET convention
Some checks failed
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) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
ASP.NET Core convention для HttpX-атрибутов: `~/path` означает 'absolute from root, ignore class [Route]'. До фикса генератор клеил `base-route` + `~/path` → невалидный `/~/connect/token`. Теперь tilde корректно срезается, /connect/token виден в reference. Также добавлен unit test ApiReferenceDocsJobTests (Sprint 28) для lock-down regex behavior на double-nested generics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ed140cb819 |
docs(s28): api-reference 195→240 + observability + integration #7 + CI
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
Overnight progress while 4h-soak runs in background:
1. ApiReferenceDocsJob.cs + scripts/gen-api-reference.py — return-type
regex теперь ловит nested generics любой глубины. Было 195
endpoint'ов в auto-gen reference; стало 240 (+45). EmployeesController
GET /api/organization/employees был пропущен из-за
Task<ActionResult<PagedResult<EmployeeDto>>>.
2. docs/observability.md — добавлен food_market_disk_free_bytes (Sprint 20)
+ раздел "quality-watchdog метрики" (5 метрик textfile exporter'a из
Sprint 26: run_total, step_failure_total, endpoint_p95_ms,
last_run_status, incidents_total). Готовые dashboards теперь содержат
оба JSON (food-market.json + quality-watchdog.json).
3. tests/integration/07-import-export-flows.spec.ts — POST 1C-CSV import
(semicolon-CSV cp1251) → создаются продукты с группой автоматом;
POST /api/org/export (НЕ /api/admin/org-export) → возвращает
{id, status}; orgB не видит export orgA. Прогон 8.2s.
4. tests/food-market.IntegrationTests/PruneQualityTestOrgsTests.cs —
2 [Fact]'a для метода из Sprint 25. Удаляет только quality-* старше
threshold, не трогает реальные org. Требует Testcontainers.
5. .forgejo/workflows/regression.yml — добавлен шаг integration suite
после flows+visual. Telegram: "35 flows + 60 visual + 8 integration".
Soak-real (4h @ 50 RPS) запущен в setsid-detach session, продолжается.
Итоговые числа добавлю в sprint28-progress.md после завершения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
019c57ae3b |
feat(s25): autonomous continuous quality monitoring (8/8)
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
Hourly smoke watchdog + auto-fix loop + dashboard + multi-tenant guard + perf regression + cleanup job + README badge. 1. ~/quality-watchdog.sh (cron 5 * * * *) — 8 checks (~60s): /health/ready, signup→login→/api/me, GET products, Playwright UI smoke (3.1 product CRUD), /metrics format, /hubs/notifications negotiate with token, multi-tenant isolation, perf p95. 2. Auto-fix loop: 2× consecutive red → ~/.fm-watchdog/incident-*.txt + queue/0000-incident-* to bump it ahead of Server-Claude's sprint queue. fm-watchdog.sh sees prefix 0000- as next. 3. scripts/quality-dashboard.py — renders docs/quality-status.md (current emoji, 8-step table, perf baseline, 7-day history, 24-run sparkline) + injects README badge 🟢/🟡/🔴. 4. Multi-tenant smoke: signup 2 orgs `quality-{epoch}-A/B`, create product in A, verify B sees 404/403 + total=0. 5. Perf regression: p95 over 10 reqs for /api/me, products, sales/retail/stats. Baseline = median of last 10 samples (robust to noise). >50% from baseline → alert. First 5 runs always green (warm-up). 6. HousekeepingJobs.PruneQualityTestOrgsAsync (cron 30 2 * * * UTC): finds orgs `quality-%` older than 24h, dynamically scans information_schema for tables with OrganizationId, iteratively DELETEs with FK-violation retry (up to 10 passes), then cleans AspNetUser*/OpenIddict* by email pattern `quality-%@test-fm.local`, finally users + organizations. 7. README badge: <!-- quality-badge --> marker updated each run. Validated: stage deploy ✓, Hangfire job registered ✓, dry-run SQL on 24 stage candidates → 0 remaining ✓, 3 cron-triggered runs all 8/8 green (12:42/12:45/12:48 +05) ✓. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
72d0a71307 |
docs(s24): docs cross-check + auto-gen + onboarding + test gap-fill (8/8 ✓)
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
1. Docs cross-check — обновил performance-baseline.md (Sprint 18/20/23 фиксы), secrets.md (16 новых env-vars из Sprint 20+ — Authentication Google/Microsoft, Monitoring, Cleanup, Hangfire:Cron, Telegram, Maintenance, App, Storage, PUBLIC_GA_ID/YM_ID). 2. Auto-gen api-reference — ApiReferenceDocsJob (Hangfire weekly вс 05:30 UTC) + Python-эквивалент `/tmp/gen-api-ref.py` для commit actual snapshot. docs/api-reference.md = 195 endpoints, 57 controllers. 3. Coverage gap-fill — Sprint18To23FeaturesTests.cs (16 Facts): - bulk-update + cross-tenant isolation - UserPresets CRUD - inline-edit price PATCH - CSV import 2 строки транзакцией - OrgExport create + list isolation - 1C-CSV import с русскими заголовками - audit-log export CSV streaming + BOM check - MoySklad sync-status stub - SSO providers + 503 unconfigured + 400 unknown provider - bug-001 NUL byte → 400 - bug-004 tiny price → 400 - export CSV BOM Покрывает все новые контроллеры Sprint 18-23 + regression-protect для критичных багов. 4. Contract tests — deploy/swagger-diff.sh: pull /swagger/v1/swagger.json с двух URL, diff endpoints+schemas через python3. Exit 0/1/2 для blue-green safety gate. Multi-path auto-detect. 5. docs/error-codes.md — каталог HTTP-кодов API (200-503) + humanizeError pattern для фронта + retry-policy таблица. 6. docs/glossary.md — 50+ доменных терминов (Tenant/Organization/Stock/ StockMovement/RetailSale/Counterparty/Owner/Employee/Role/Permission/ advisory lock/Serializable/…) с ссылками на code-сущности. 7. docs/ONBOARDING.md — first 3 days для нового разработчика (install → запуск → структура → первый PR + FAQ). 8. README.md — обновил под текущее состояние: React 19, Sprint-history 1-24, ссылки на ключевые docs, корректный 5-min quick start. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
284ad095c1 |
fix(s23): adversarial bug-hunt — 4 bugs found, all fixed
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
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> |
||
|
|
1af4290313 |
fix(s22): 1C-CSV detect charset из Content-Type + UpdatedAt в migration
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
- Parse1cCsv ловил Windows-1251 на UTF-8 bodies без BOM. Теперь смотрит Content-Type charset первым, потом BOM, потом WIN-1251. - Migration Phase22a_OrgExports забыл UpdatedAt колонку (Entity base имеет её). ADD COLUMN IF NOT EXISTS внутри миграции для уже созданных таблиц. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4c1ac37a08 |
fix(s22): OrgExportJob.WriteCollection<T> — where T : class
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
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
aa83f82dc5 |
feat(s22): data tooling — export/import + schema docs + anon dump (7 пунктов)
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
1. GDPR org export — domain OrgExport + Phase22a миграция, OrgExportJob
собирает ZIP с JSON по каждой сущности через IObjectStorage,
DownloadToken 64-hex + 24h TTL + email-notify.
POST /api/org/export, GET /api/org/export[/{id}], GET download/{token}.
2. 1C CSV import — POST /api/catalog/products/import/1c-csv:
Windows-1251/UTF-8 BOM auto-detect, разделитель ;/, русские заголовки
(Артикул/Наименование/Единица/Цена/Группа/Штрихкод) или английские.
Нормализация unit-кодов (шт/кг/г/л/мл/упак). Делегирует на ImportCsv
(транзакция, multi-tenant). docs/imports.md.
3. deploy/anonymize-prod.sh — pg_dump прода → restore во временную БД →
UPDATE PII (email→user{N}@example.kz, phone→+7700111{N:04}, password→
тестовый hash, BIN/IIN синтетические, MoySkladToken=NULL, аудиты
TRUNCATE) → pg_dump → gz файл.
4. DbSchemaDocsJob (weekly вс 05:00 UTC) — information_schema → md с
таблицами + колонками + FK + mermaid ER-диаграммой (топ-20 таблиц).
Сохраняет в content-root db-schema-generated.md.
5. POST /api/admin/audit-log/export?format=csv|jsonl — streaming через
AsAsyncEnumerable. UTF-8 BOM для CSV, JSONL для grep'a. Multi-tenant.
6. GET /api/moysklad/sync-status — агрегат по import_jobs:
{ configured, lastSuccessAt, errorCountLast7Days, pendingCount,
byKind: { products: KindStatus, counterparties: KindStatus } }.
Stub если MoySkladToken=null.
7. docs/ARCHITECTURE.md — финальный итог 22 спринтов:
- Sprint 13-22 changes-сводка
- «Реализовано полностью» секция
- «Scaffolding» таблица с указанием что нужно от user'а
- «Не реализовано» секция (прод, SSO callback, KZ-перевод, POS-тест)
- Актуальная файловая структура
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
346b7bfd48 |
feat(s20): Mapster + SSO scaffold + maintenance automation (7 пунктов)
Some checks failed
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 Public / Build + push Public (push) Has been cancelled
Docker Public / Deploy Public on stage (push) Has been cancelled
1. TD-3 Mapster — Application/Mapping/MapsterConfig.cs с
TypeAdapterConfig для Product, Counterparty + collections.
ProductsController.List/Get/GetInternalAsync + CounterpartiesController.
List/Get переведены на .ProjectToType<TDto>(MapsterConfig.Config).
Inline Projection-Expression удалён.
2. SSO scaffold — Microsoft.AspNetCore.Authentication.Google + .MicrosoftAccount
пакеты, условная регистрация в Program.cs (только если ClientId задан).
ExternalAuthController с GET /api/auth/external/{provider} (Challenge или
503 если не настроено), /callback (501 с email — invite-flow TODO),
/providers (булевый список). docs/sso.md инструкция.
3. Stale-data cleanup — HousekeepingJobs расширен:
PruneOrgAuditLogAsync (>90д из Cleanup:OrgAuditLogDays),
PruneDraftsAsync (Supply/RetailSale/Demand старше 30д),
PruneRevokedRefreshTokensAsync (raw SQL DELETE из OpenIddictTokens).
3 новых cron'a в HangfireJobsConfigurator (03:00-03:20 UTC).
4. DB VACUUM automation — DatabaseMaintenanceJobs.VacuumTopTablesAsync:
pg_total_relation_size → топ-5 таблиц → VACUUM (ANALYZE) per table
с замером времени. Default cron еженедельно вс 04:00 UTC.
5. Disk usage monitoring — DiskMonitoringJob ежечасно: DriveInfo.AvailableFreeSpace
на пути из Monitoring:DiskPaths (default "/opt,/var/lib/docker").
<1GB → Telegram-alert на Monitoring:SuperAdminTelegramChatIds.
Anti-spam cooldown 6h. Gauge food_market_disk_free_bytes{mount}.
6. Performance regression detection — ~/nightly-perf-check.sh после
nightly-verify. Парсит /metrics, считает db_avg_ms, сравнивает с
baseline в ~/.fm-watchdog/perf-baseline.json. Δ>30% → Telegram alert
+ baseline НЕ обновляется (sliding window).
7. Public-site analytics placeholder — Astro BaseLayout рендерит
gtag/Yandex.Metrika только если задан PUBLIC_GA_ID / PUBLIC_YM_ID;
иначе <script data-id="REPLACE_ME" data-doc="docs/analytics.md">
маркер. docs/analytics.md с инструкцией подключения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
83793fd6dd |
fix(s19): SaleExportRow.Payment — string (enum), не decimal
Some checks are pending
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6940aa40df |
feat(s19): bulk-операции + presets + power-user UX (7 пунктов)
Some checks failed
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) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
1. Bulk-обновление товаров — Product.IsArchived + IsAvailableForSale
(Phase19a миграция), POST /api/catalog/products/bulk-update {ids, op, params}
с операциями price-adjust (% / абсолют), change-group, archive/unarchive,
toggle-sale. Одной транзакцией, multi-tenant через query-filter.
Frontend: checkbox-колонка, sticky bulk-bar, модалка.
2. SavedPresets — domain UserPreset (Phase19b: jsonb ConfigJson,
unique по OrgId+UserId+PageKey+Name). /api/user/presets CRUD per-user.
<SavedPresets> компонент с chip-bar и сохранением. Применено к /reports/
sales/stock/profit + /catalog/products.
3. QuickActionsPalette — Cmd+J открывает отдельную палитру с 14 действиями
+ история topa-10 в localStorage.fm.quickActions.recent. ↑↓/Enter/Esc
keyboard nav. Cmd+K (поиск) и Cmd+J (действия) — разные палитры.
4. Inline-edit цены — PATCH /api/catalog/products/{id}/price новый endpoint
с RoundIfNeeded. <InlinePriceCell> с dblclick → input, optimistic update
+ revert при ошибке.
5. CSV import товаров — POST /api/catalog/products/import-csv (rows[]).
Клиент парсит CSV (auto-detect разделитель ,/;), сервер commit'ит
транзакцией. autoCreateGroup для новых групп. <ProductsCsvImport>
модалка с preview и подсветкой ошибочных строк.
6. CSV/XLSX export — endpoint'ы /export на 5 контроллерах (products,
counterparties, stock, retail-sales, supplies). Reuse существующего
ReportExport.Csv/Xlsx. <ExportButton> dropdown с двумя форматами.
7. Keyboard-first nav в DataTable — ↑↓/Home/End/Enter/Space/Delete props
keyboardNav/onSelect/onDelete. Подсветка focused-строки. Документация
в src/help/keyboard-shortcuts.md + 2 новые HelpTopic'a.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9bd4375ae4 |
feat(s18): TODO cleanup — P0 race fix + helpTooltip + whats-new + contrast + currency + audit filters + notifications
Some checks are pending
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 Public / Build + push Public (push) Waiting to run
Docker Public / Deploy Public 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
7 пунктов cleanup-спринта: 1. P0: race в GenerateNumberAsync — DocumentNumberRetry helper с WithOrgAdvisoryLockAsync (pg_advisory_xact_lock per orgHash/docTypeHash) + SaveWithRetryAsync exponential backoff. RetailSalesController POST обёрнут в lock. После — 23505 errors 53% → 0 на k6 baseline-replay. 2. HelpTooltip integration — ListPageShell расширен `helpTopic` пропом. Применено к 4 страницам (Promotions, Loyalty×2, AuditLog) + inline на MoySkladImportPage. 3. WhatsNewBanner — узкий emerald-toast сверху AppLayout. Опрашивает /api/whats-new (staleTime=1h), сравнивает buildVersion с localStorage.fm.lastSeenBuildVersion. Dismiss сохраняет версию. 4. Color contrast sweep — text-slate-400 в body-text узлах (empty-state, table-cells, hints, help) заменён на text-slate-500 dark:text-slate-400. 19 файлов. Иконки оставлены (decorative, не покрыты axe color-contrast). 5. useFormatCurrency() хук в lib/useFormatCurrency.ts. Берёт defaultCurrencySymbol из useOrgSettings + локаль из i18next. DashboardWidgets (TopProducts/RecentSales/Margin) переведены — `₸` больше не захардкоден. 6. Audit log UI filters — OrgAuditLogPage расширен полями «Кто» (Select сотрудников), «Дата с» / «по» (date-input'ы), кнопка «Сбросить фильтры». Backend уже умел эти параметры. 7. NotificationCenter — bell-icon в sidebar footer'е с unread badge, popover с 30 последних событий (Sale/Supply/LowStock через useNotificationsHub). Each item clickable → документ. In-memory. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f56c6efab1 |
feat(s17): onboarding wizard + help kb + feedback + diagnostic + whats-new
Sprint 17 — onboarding-контур: 4-шаг wizard, контекстный help, in-app
feedback, admin self-diagnostic, /whats-new из CHANGELOG.md.
Ключевые цифры:
- Wizard: 4 шага + skip каждого, 7 e2e тестов ✓ за 20 секунд.
- Diagnostic: 7 параллельных проверок, ~80ms на stage.
- Bundle impact: initial +4 KB gzip (только FeedbackWidget +
HelpTooltip + EmptyStateWithDemo в основном bundle; страницы lazy).
- Regression-suite: 35 → 42 flows + 60 → 66 visual snapshots.
Backend (новые endpoint'ы):
- /api/admin/diagnostic/run — 7 параллельных проверок (DB, SMTP,
MinIO, Hangfire, диск, сертификаты, бэкап). Task.WhenAll, ~80ms.
- /api/feedback — POST {category, message}, email на FromEmail +
Telegram (если SupportTelegram:* настроены). Rate-limit 5/час.
- /api/whats-new — парсер CHANGELOG.md, возвращает {buildVersion,
items}. Dockerfile.api копирует CHANGELOG.md в content-root +
пишет VERSION из GIT_SHA build-arg.
Frontend:
- /onboarding-wizard — 4-step builder, состояние в useState,
localStorage.fm.wizardCompleted после завершения.
- <HelpTooltip topic="key"/> — popover на каждой странице, mapping
src/lib/help-topics.ts (13 keys).
- /help — knowledge base, 7 markdown topics через import.meta.glob,
mini-renderer без heavy deps, fuzzy search.
- /whats-new — список из /api/whats-new, иконки по типу (feat/fix).
- /admin/diagnostic — Admin/SuperAdmin only, 🟢/🟡/🔴 индикаторы.
- <FeedbackWidget> в sidebar footer + ссылки на /help и /whats-new.
- <EmptyStateWithDemo> placeholder для будущих видео-демо.
scripts/generate-changelog.sh — git log feat:/fix: за 90 дней
→ CHANGELOG.md (307 строк сгенерировано).
Wizard UX-screenshots в docs/sprint17-screenshots/ (6 PNG: 4 шага +
help + diagnostic).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e13dd6937f |
perf(s14): индексы + N+1 fix + bundle -50% + WebP variants + pool + Hangfire timing
Sprint 14 — производительность с реальными замерами до/после. Ключевые цифры: - Sales-report SQL: 9.53ms → 7.09ms mean (-25%) после N+1 fix + индексов. - Initial JS bundle: 1456 KB → 706 KB raw (-51%); gzip 389 KB → 196 KB (-50%) через React.lazy на 30 редких страниц + Recharts. - Lighthouse /login: Perf 89, A11y 92, BP 100 (target ≥85/90/90 ✓). Подробности по каждому пункту + методология замеров — в docs/sprint14-progress.md. Что сделано: 1. Phase14a_PerfIndexes — composite (Org,Status,Date), partial (WHERE Status=1 AND NOT IsReturn) + INCLUDE, и composite stock_movements(Org,OccurredAt). 2. SalesReportController.FetchAsync — раньше каждая строка результата делала CASE WHEN ELSE (SELECT ... LIMIT 1) correlated subquery на RetailPoint.Name и User.FullName. Заменено на 2 IN-batch'a + dictionary lookup в C#. 3. App.tsx React.lazy для отчётов, audit-log, loyalty, super-admin, settings, all rare edit pages. Recharts вынесен в lazy chunk Dashboard'а (KPI рендерятся сразу). 4. SixLabors.ImageSharp v3.1.6 + ImageVariantService — генерирует thumb 256/medium 800 WebP@80 при загрузке. UploadsController ?size=thumb|medium с fallback. React <ProductImage> — <picture> + srcset. 5. ApplyDefaultPoolConfig на старте: Max=100, Min=10 (грей пул), Idle=300, Max Auto Prepare=20. 6. Lighthouse на /login /forgot-password /reset-password — все три проходят пороги. 7. JobTimingFilter + HangfireGlobalFilterRegistrar — каждый recurring job логирует длительность; >30s = Warning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
8e54e2e0d6 |
feat(s13): security headers + rate-limits + sensitive-ops audit + session revoke + Grafana
Sprint 13 — security + observability deep. 7 пунктов чек-листа ✓.
Подробности — docs/sprint13-progress.md и docs/food-market-server-postgres-role.md.
Главное:
- food-market-server (back.food-market.kz, legacy backend) теперь
работает на dedicated PG-роли food_market_server_app (NOSUPERUSER /
NOCREATEDB / NOCREATEROLE / NOREPLICATION / NOBYPASSRLS) с CRUD-only
грантами. Раньше использовался postgres-superuser с паролем 1q2w3e4r.
Бэкап конфига сохранён, rollback одной командой.
- SecurityHeadersMiddleware навешивает CSP / X-Frame-Options DENY /
X-Content-Type-Options nosniff / Referrer-Policy strict-origin /
Permissions-Policy. HSTS 365d + includeSubDomains + preload.
Те же заголовки в deploy/nginx.conf для SPA HTML.
- Rate-limit:
• Signup-IP — 3/час + 10/день (на stage'е переопределено через
.env RATE_SIGNUP_HOUR=30 чтобы не ломать e2e).
• Forgot-password — per-email 3/час + per-IP 10/час.
- SensitiveOpsAudit сервис, wired в:
• TwoFactor enroll/disable
• Employees.Update при смене RoleId (action=AssignRole,
payload с prev/next role + полный RolePermissions)
• MeAccount.ChangePassword (новый endpoint)
• MeSessions.RevokeAll (новый endpoint)
- POST /api/me/sessions/revoke-all — через
IOpenIddictAuthorizationManager.FindBySubjectAsync + TryRevokeAsync.
Integration-тест: refresh после revoke → 400/401.
- Hangfire dashboard — nginx-route добавлен (раньше /hangfire ловился
SPA-fallback'ом). Фильтр SuperAdmin'ом уже был. Тест: anon/tenant →
401/403/404.
- Grafana dashboard JSON (deploy/grafana/dashboards/food-market.json,
9 панелей) + инструкции импорта в docs/observability.md.
Проверено на stage'е: все 6 security-заголовков видны на /;
/hangfire → 401 (закрыт); 4-я форгот → 429; stage-smoke (5 этапов) ✓.
Тесты: 68 unit + 9 integration (включая 3 новых: SessionRevokeTests,
HangfireAccessTests).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
0d3ef81f72 |
feat(s11): ОФД-scaffolding — IFiscalProvider + 4 провайдера + UI/тесты
Sprint 11 — каркас для интеграции с операторами фискальных данных РК.
Реальные ApiKey'и появятся у user'а позже; задача — построить такой
фрейм, чтобы подключение оператора сводилось к вписыванию кредов в UI
без правок кода/деплоя.
Что сделано:
- IFiscalProvider (Application/Common/Fiscal) + FiscalResult,
FiscalProviderKind (None/Mock/Webkassa/Kassa24/OfdSolo),
IFiscalProviderFactory, FiscalNotConfiguredException.
- 4 реализации в Infrastructure/Fiscal:
• MockFiscalProvider — фейк MOCK-<8hex> через 300мс, идемпотентный
по Sale.Id (используется dev/stage и интеграционными тестами);
• WebkassaProvider — полный HTTP-pipeline Authorize→Check, парсинг
JSON-ответа, NDS-в-ставке, retry-safe через ExternalCheckNumber;
• Kassa24Provider / OfdSoloProvider — скелет с тем же контрактом,
RegisterAsync бросает FiscalNotConfiguredException (нужны
спецификации API от user'а, NDA-only).
- Миграция Phase11a: 5 колонок в retail_sales (FiscalNumber, QrCode,
Url, ProviderTxId, ProviderKind) + 5 в organizations (FiscalProvider
NOT NULL default 0, ApiKey/Secret encrypted, CashboxUniqueNumber,
ApiBaseUrl). Default 0 = обратная совместимость, существующие чеки
и продажи без фискализации работают как раньше.
- RetailSalesController.Post — TryFiscalizeAsync после commit'а
stock-транзакции. Best-effort: сетевые/HTTP-ошибки логируются, чек
остаётся проведённым. Идемпотентность по IsNullOrEmpty(FiscalNumber).
- OrgFiscalSettingsController: GET/PUT настройки + GET /providers
(опции для select'а) + POST /test-send (фейк-чек к выбранному
провайдеру, не сохраняет в БД).
- UI: FiscalSection в OrganizationSettingsPage с password-input'ами
для ApiKey/Secret (шифруются DataProtection.purpose=foodmarket.fiscal,
в GET — только has-* флаги), спец-значение "__clear__" для снятия,
кнопка «Тестовая отправка».
- Тесты: 11 unit (Mock 5 + Webkassa payload 6) + 3 integration
(Mock сохраняет FiscalNumber, test-send даёт MOCK-номер, None
не фискализует).
- docs/ofd-integration.md — гид с архитектурой, шагами подключения
Webkassa (полный pap), TODO для Касса24/ОФД-Соло, безопасностью
кредов, retry-сценариями.
Все 68 unit + 8 integration в Fiscal/Loyalty/RetailOversell — зелёные.
Web vite build — зелёный.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f9fa028fe5 |
feat(s10-3): глобальная Cmd+K палитра + GET /api/search/global
Some checks failed
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 Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
S10-3: командная палитра для быстрой навигации и поиска. Backend GlobalSearchController: - GET /api/search/global?q=… ищет в 3 источниках: товары (name/article/ barcode startsWith), контрагенты (name/bin contains), документы (Supply.Number, RetailSale.Number, Demand.Number) — по ≤5 в каждой группе. Tenant-scoped, требует ≥2 символа в q. - Lower-cased contains; EF8 OrderBy на record-projection ломается, поэтому проектируем в anonymous, потом маппим в DocumentHit. Frontend CommandPalette.tsx: - Глобальный хоткей Cmd+K / Ctrl+K (listener на document в AppLayout). - Статический список 20 страниц для навигации без меню (даже без API). - Дебаунс query 200мс → GET /api/search/global при q ≥ 2 символов. - Recent items: localStorage 'fm.cmdk.recent', последние 10 выбранных показываются когда q пустой. - Подсветка совпадений через RegExp split + <mark>. - Хоткеи: ↑↓ Enter Esc; группированный список (Recent / Pages / Товары / Контрагенты / Документы). Проверено на стэйдже: q='колбас' → 3 продукта, q='Алматы' → 2 контрагента (поставщики), q='ПР-Y1-00019' → 5 retail-sale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1044818fbb |
feat(s10): year-demo seeder + 4 dashboard виджета + week-stats
Some checks are pending
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
S10-1: YearDemoSeeder — POST /api/admin/seed-demo?years=1. - 8 групп × 25 товаров = 200, 30 контрагентов, 80 приёмок равномерно по году, 1500 розничных продаж с месячной сезонностью (Dec пик ×1.6, Jul-Aug спад ×0.7), 20 customer-returns, 8 demands, 10 losses, 3 transfers, 5 inventories. - Маркер артикулов Y1- (параллельно с DEMO-короткий сидер). Гард на существующую активность чтобы не лить хаос поверх ручной работы. - Bulk StockMovement + переагрегация Stocks в конце транзакции — 16.5s на dev-vm vs 60+s если бы per-document SaveChanges. S10-2: DashboardController + 4 виджета: - GET /api/dashboard/top-products?days&limit — top-N по gross-выручке (без net-вычета returns; для точного есть /api/reports/sales). - GET /api/dashboard/low-stock?limit — Stock.Quantity ≤ Product.MinStock. - GET /api/dashboard/recent-sales?limit — последние N посt'ed чеков. - GET /api/dashboard/margin?days — Σ(LineTotal) - Σ(qty × Product.Cost), marginPercent к выручке. - /api/sales/retail/stats расширен revenueThisWeek + transactionsThisWeek. - Frontend: components/DashboardWidgets.tsx с 4 виджетами через React.lazy + Suspense. SignalR SalePosted инвалидирует все 4. - KPI блок: today / week / month + avg-ticket (4 плитки, prev-month стал delta на month-плитке). Проверено на стэйдже год-демо: top-5 за 365 дн. — «Колбаса сервелат 300г» 286440 ₸ / 32 транзакции. Margin 40% за 30 дн. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
43a5552772 |
fix(stage-tests): IP-limit 60/min, locale ru-RU в playwright, исправлены payload'ы verify-spec'ов
Some checks failed
После предыдущего фикса 5/мин per-username — per-IP 30/мин всё равно ломал stage e2e (multi-tenant специ делают 4 signup+token подряд → накапливается за минуту). Поднял до 60/мин token, 600/час; per-username 5/мин остаётся как анти-bruteforce. Также: playwright.config.ts добавлен locale: 'ru-RU' — без этого Chromium шлёт en-US, i18next отдаёт английский sidebar, а тесты ищут русские лейблы (2.2 'Главная', 6.1 'Поставщик/Склад/Дата'). Verify-spec'и V-14 (POS Sync) и V-15 (Stock race) — починены payload'ы под актуальную схему API (/api/catalog/stores не /api/inventory/stores, quantity не qty, unitCost не costPrice, polnyy retail-sale body с retailPointId/currencyId/payment/isReturn). Проверено: - V-14: 1-й POS-батч 200 (accepted=1), 2-й replayedFromCache=true с тем же serverSaleId; detail GET показывает notes=pos:<csid-N> ✓ - V-15: 5 параллельных Post на остаток=3 → ровно 3 успешных (204), 2 конфликта (409 'Недостаточно остатка'). Stock=0 после dust settles. ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9d48ca6483 |
fix(rate-limit): per-username 5/мин + per-IP 30/мин — brute-force на конкретный аккаунт ловится, CI/NAT не страдают
Some checks are pending
Регрессия после
|
||
|
|
ba54155225 |
fix(stage): rate-limit 5/min на /connect/token, nginx route /metrics+/swagger, Swagger в Production через IncludeSwagger
Some checks failed
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) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
Verify-Sprint баги A-D: - A: на stage docker-compose.yml был "RateLimiting__PerMinute=200" — убран, теперь работают дефолты (5/мин, 20/час). 6-я попытка с тем же IP/паролем → 429. - B: web-контейнер nginx не имел location = /metrics → запрос ловился SPA fallback'ом (index.html, 947 байт). Добавлен proxy_pass на api:8080. - C: web-nginx не имел location /swagger/ → swagger.json возвращал SPA HTML. Добавлены /swagger/ + редирект /swagger → /swagger/. - D: Swagger подключался только в Development. Добавлен флаг IncludeSwagger (env IncludeSwagger=true) — Program.cs включает UseSwagger() и в Production если флаг выставлен. На prod admin.food-market.kz флаг не ставим. Проверено через https://test.admin.food-market.kz: - 6 неверных логинов подряд: 1-5 → 400, 6-7 → 429 ✓ - /metrics → 14967 байт prometheus exposition ✓ - /swagger/v1/swagger.json → 422 КБ openapi 3.0.1 ✓ - /swagger/ → swagger-ui (redirect на /swagger/index.html) ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
91128a7ed0 |
feat(loyalty+promotions): P2-12 + P2-13 — лояльность и промокоды (Sprint 9 п.1-2)
Some checks failed
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 Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
Domain:
- LoyaltyProgram { Type=Percentage|FixedAmount|PointsAccrual, Rate,
MinSubtotal, IsActive } — org-scoped.
- LoyaltyCard { ProgramId, CounterpartyId, CardNumber unique per org,
Balance, IsBlocked }.
- Promotion { Type=Percent|FixedDiscount, Value, Scope=All|ProductGroups|
Products, Code unique per org, period, ProductGroupIds/ProductIds (jsonb) }.
- RetailSale: LoyaltyCardId, LoyaltyBonusApplied, LoyaltyPointsAccrued,
PromotionId, PromotionCode (snapshot), PromotionDiscount.
EF:
- SalesConfigurations: indexes, FK Restrict, jsonb-converters для Guid-
списков Promotion (ValueComparer для change-tracker).
- Phase9b миграция: 3 таблицы + 6 колонок на retail_sales.
- RolePermissions: LoyaltyManage, PromotionsManage добавлены (попадают
в All() для Admin).
API:
- /api/loyalty/programs CRUD (Get/List/Create/Update/Delete; запрет delete
при существующих картах → 409).
- /api/loyalty/cards CRUD + /issue + /{id}/block + /{id}/unblock + /lookup
(POS использует при оплате — 404 если нет, 409 если blocked/inactive).
- /api/promotions CRUD; код уникален per org (БД-индекс + 23505 → 409).
- RetailSale.Create/Update: новые поля input.LoyaltyCardNumber +
input.PromotionCode. Метод ApplyLoyaltyAndPromotionAsync:
• Lookup карты, проверка active/blocked/MinSubtotal.
• Расчёт скидки или баллов в зависимости от Type.
• Lookup промокода, проверка периода/MinSaleAmount/scope.
• MatchingSubtotal для Scope=ProductGroups/Products считаем по
input.Lines (sale.Lines ещё пустой в этот момент).
• Финальный Total = Subtotal - DiscountTotal - LoyaltyBonusApplied
- PromotionDiscount, max(0).
- RetailSale.Post: начисление баллов на LoyaltyCard.Balance (внутри
транзакции, чтобы rollback не оставил orphan баллы).
UI:
- /loyalty/programs — list + create/edit modal с Type/Rate/MinSubtotal.
- /loyalty/cards — list + issue modal (Program select + AsyncSelect
counterparty + CardNumber).
- /promotions — list + create/edit modal (Type/Value/период/MinSaleAmount/Code).
- Sidebar: новый блок «Продажи» с пунктами Промокоды/Программы/Карты
(Admin-only).
- i18n: ru.json + en.json пополнены nav-ключами.
Тесты:
- LoyaltyFlowTests (3/3 ✓): percentage уменьшает Total на 10%, points-accrual
пополняет Balance после Post, multi-tenant lookup→404 чужой org.
- PromotionFlowTests (2/2 ✓): SALE20 уменьшает Total на 20%, невалидный
код→400 с понятной message и field=promotionCode.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
7de159d5f2 |
feat(storage): IObjectStorage abstraction (Local + MinIO) — P2-15
Some checks are pending
Backend:
- src/food-market.api/Storage/IObjectStorage.cs — абстракция
(SaveAsync, OpenAsync, DeleteAsync, PublicUrl). Имя реализации (Kind)
для логов.
- LocalObjectStorage — ContentRoot/uploads/{key}. По умолчанию.
- MinioObjectStorage — S3-совместимый bucket, ключ-объекта совпадает с
тем что хранится в БД (products/{id:N}/{guid}.png). PutObject с явным
size (для NetworkStream копируем в MemoryStream).
- StorageOptions: Type=Local|Minio, Endpoint, AccessKey, SecretKey,
UseSsl, Bucket=food-market-uploads.
- StorageBootstrap.AddObjectStorage — DI-регистрация с runtime fallback
на Local если MinIO-config пустой; MinioBootstrap (IHostedService)
создаёт bucket на старте, ловит ошибку и не валит API.
- UploadsController: GET /uploads/{**path} → стримит из IObjectStorage
(cache-control 7 дней). Нужен когда MinIO активен — для Local nginx
раздаёт быстрее, но фолбэк работает.
- ProductImagesController отрефакторен на IObjectStorage; URL'ы в БД
остаются /uploads/products/{id}/{guid}.ext.
Тесты:
- StorageAbstractionTests (3/3 ✓): Local default, round-trip bytes,
PublicUrl pattern.
Stage-готовность:
- deploy/docker-compose.yml на стейдже обновлён (через scp): добавлен
minio container, depends_on в api, env переменные Storage__*.
Bucket автосоздаётся.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
3088237ea7 |
feat(telegram): OwnerDailySummaryJob + bot binding (P2-14)
Some checks are pending
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
Backend: - Organization.OwnerTelegramChatId (long?) — миграция Phase9a. - TelegramOptions / TelegramBotClient (Telegram Bot API sendMessage). Disabled-mode когда токен пустой (Dev/CI). HTML parse_mode. - OwnerDailySummaryJob.RunAsync — пробегает по org с привязанным chatId, рендерит сводку (выручка вчера, чеков, средний чек, топ-3 по выручке, low-stock 5 строк) и шлёт. Best-effort на каждой org. RenderSummaryAsync — publicный для тестов. - HangfireJobsConfigurator: cron "0 6 * * *" UTC = 09:00 МСК. - TelegramBindingController: GET /status (botEnabled, username, chatId, deepLink), PUT /bind (тестовое сообщение → проверка chatId → save), DELETE (unbind). Конфиг: - Telegram:BotToken — env Telegram__BotToken. - Telegram:BotUsername — для deep-link. UI: - OrganizationSettings.TelegramSection: показывает статус (bot enabled? bound?), deep-link к боту, пошаговая инструкция (start → userinfobot → ввести chat_id → проверка). Toast на привязку/отвязку через meta.successMessage. Тесты: - TelegramOwnerSummaryTests: рендер содержит org_name, метрики, HTML. ✓ 1/1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
dd2e1e7af2 |
feat(realtime): SignalR hub /hubs/notifications per-org + dashboard live
Some checks are pending
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
P2-7 Sprint 8 пункт 1.
Backend:
- src/food-market.api/Realtime/NotificationsHub.cs — SignalR-хаб, группы
org:{orgId:N}. JWT через Authorization-хедер (стандартно) или через
query ?access_token=... (для WebSocket — браузерные не могут слать
кастомные хедеры). SuperAdmin override через ?orgOverride=<id>.
- NotificationsPublisher.cs — singleton, IHubContext-обёртка.
- Program.cs — AddSignalR + MapHub. Middleware копирует ?access_token=
в Authorization для /hubs/* до UseAuthentication.
- RetailSalesController.Post → публикует SalePosted + LowStockPayload
если после движения товара остаток < MinStock. Best-effort: notify
ошибка не валит транзакцию.
- SuppliesController.Post → SupplyPosted.
Events (camelCase в JSON):
- SalePosted { saleId, number, total, storeId, cashierName, retailPointId, postedAt }
- SupplyPosted { supplyId, number, total, supplierId, supplierName, postedAt }
- LowStock { productId, productName, storeId, storeName, quantity, minStock }
Web:
- @microsoft/signalr 10.0.0 client.
- src/lib/useNotificationsHub.ts — hook с автореконнектом, accessTokenFactory.
- DashboardPage:
• liveRevenueDelta / liveCountDelta — оптимистическое приращение
«Выручка сегодня» сразу при SalePosted (до refetch stats);
• toast.info на SupplyPosted; toast.error на LowStock;
• Wifi/WifiOff индикатор в header.
Тесты:
- SignalRNotificationsTests: A постит retail-sale → A получает SalePosted,
B (другая org) НЕ получает — multi-tenant. ✓ 1/1 локально.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ad09b56013 |
feat(stage): demo-data seeder для test.admin.food-market.kz
Some checks failed
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 Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
Item 1 Sprint 7 — кнопка «Заполнить демо-данными» в OrganizationSettingsPage. Что заполняет (за одну транзакцию, ~3с на стейдже): - 5 групп товаров (Молочные / Хлеб / Напитки / Бакалея / Снеки) - 50 товаров с барштрихкодами EAN-13 + retail-ценой (article DEMO-NN-MM) - 10 контрагентов (5 поставщиков + 5 покупателей-юрлиц с BIN) - Второй склад «Резерв» (если нет) для transfer'a - 5 приёмок (Posted) за последние 30 дней с moving-average cost - 30 розничных продаж (Posted) за последний месяц, Cash/Card случайно - 1 опт-отгрузка (Demand, Posted) с 15% скидкой - 1 списание (Loss, Posted, причина Expired) - 1 перемещение (Transfer, Posted) между складами - 1 инвентаризация (Posted) с небольшим diff +/- 1 Идемпотентность: маркер — наличие Product с Article startsWith "DEMO-". Повторный POST → возвращает summary без вставок. API: - GET /api/admin/seed-demo/status — счётчики (Admin policy) - POST /api/admin/seed-demo — запустить (Admin policy) UI: OrganizationSettingsPage.tsx, секция «Демо-данные» с Sparkles-иконкой, counts grid и кнопкой (disabled когда уже заполнено). Тесты: tests/e2e/scenarios/stage-demo-seed (5/5 ✓ локально). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
466595b4d5 |
fix(swagger): operationId + schemaId — генерация OpenAPI работает
Some checks are pending
В 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> |
||
|
|
6a5bb52b13 |
test(stage): пункт 11 — OrgAuditLog 7/7 ✓ + UTC fix
Some checks are pending
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> |
||
|
|
97d5ae5eb0 |
fix(reports): 3 фикса по итогам stage-тестирования
Some checks are pending
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> |
||
|
|
4e15359378 |
fix(docs): EF8 nav-collection bug в Enters/Losses/Transfers/SupplierReturns/Inventories.Update
Some checks are pending
Тот же баг что в 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>
|
||
|
|
d54e1cb968 |
fix(catalog): EF8 nav-collection bug в Products.Update + unique IX на Article
Some checks are pending
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> |
||
|
|
7b7a7091b9 |
feat(auth): TOTP 2FA для админов через AuthenticatorTokenProvider (P2-4)
ASP.NET Identity AuthenticatorTokenProvider (RFC 6238 — Google
Authenticator, Authy, 1Password OTP). TwoFactorEnabled и SecurityStamp
уже были в users-таблице из Identity-схемы.
Endpoints (Bearer-auth):
- GET /api/me/2fa/status — { enabled: bool }.
- POST /api/me/2fa/enroll — генерирует SecretKey (если ещё нет),
возвращает otpauth-URI для QR + сам shared-key. Пока 2FA включён,
enroll возвращает alreadyEnabled=true без секрета.
- POST /api/me/2fa/verify { code } — валидирует и включает 2FA.
- POST /api/me/2fa/disable { code } — выключает + ResetAuthenticatorKeyAsync.
Требует текущий code как защиту от случайного отключения.
AuthorizationController.Exchange (password grant): после успеха проверки
пароля смотрит TwoFactorEnabledAsync; если true и нет otp_code в
запросе — возвращает invalid_grant с error_description="2fa_required";
если otp_code невалиден — "2fa_invalid"; иначе токен выдаётся.
Опционально для всех ролей — User самостоятельно решает включать или нет.
Для админов рекомендуется (отдельная политика — следующий шаг).
Тесты: 4 интеграционных (enroll+verify+status, неверный code → 400,
token-endpoint require otp_code, disable с code). Тесты сами генерируют
TOTP через ручную RFC 6238 имплементацию (HMAC-SHA1, 30-сек step).
Bonus: добавлены DI-заглушки UnusedSupplyWriter / UnusedRetailSalePoster
для CQRS-handler'ов из TD-1 — handler'ы пока не подключены к
контроллерам, заглушки нужны чтобы DI-validation на старте не падала.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ef8c4a3222 |
feat(cqrs): MediatR partial — 3 handler-образца (TD-1)
Подключён MediatR в food-market.api с авторегистрацией из сборки food-market.application. Цель — показать паттерн, не полный рефакторинг контроллеров (это отдельный спринт). Образцы handler'ов в food-market.application: - Purchases/Commands/CreateSupplyCommand + CreateSupplyHandler — создание Draft-приёмки с делегированием персистентности через ISupplyWriter абстракцию (testable без EF). - Sales/Commands/PostRetailSaleCommand + PostRetailSaleHandler — проведение чека с валидацией платежа (переиспользует RetailPaymentValidator из Sprint 1) и делегированием stock-операции через IRetailSalePoster. - Sales/Queries/GetSalesReportQuery + GetSalesReportHandler — агрегация плоских sale-строк по period:day/period:month/product. Pure-функция, безопасно тестируется в памяти. Контроллеры пока используют прежние flow (контроллер → EF напрямую) — поэтапная миграция, не big-bang. Эти handler'ы — образец, на который оглядываемся при следующих feature'ах. Тесты: 6 unit (2 GetSalesReportHandler, 1 CreateSupplyHandler, 3 PostRetailSaleHandler). 57 unit total зелёных. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
443eebe862 |
feat(logging): структурные log-fields в Serilog (TD-4)
LogEnrichmentMiddleware: после Authentication+Authorization вытягивает из
ClaimsPrincipal OrgId (claim org_id) и UserId (sub/NameIdentifier), плюс
CorrelationId из заголовка X-Correlation-ID (или генерирует Guid). Все три
кладутся в Serilog LogContext через PushProperty — каждая ILogger.Log*
внутри пайплайна автоматически получает эти поля как структурные
properties (не текст), пригодные для фильтрации в Loki/ELK без regex.
Эхо CorrelationId в response-header — клиент видит id для support.
Business-логи (структурные плейсхолдеры, не string interpolation):
- Supply.Post → "Supply posted: {SupplyNumber} supplier={SupplierId}
store={StoreId} lines={LinesCount} total={Total}".
- RetailSale.Post → "RetailSale posted: {SaleNumber} store={StoreId}
payment={Payment} lines={LinesCount} total={Total}".
docs/logging.md — паттерн, anti-pattern'ы (string interpolation, PII в
логах, токены/пароли), correlation-id workflow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
eacf7e5cc8 |
feat(validation): FluentValidation + ValidationFilter для DTO (TD-2)
Подключён FluentValidation (уже был в Directory.Packages.props, теперь
активно используется):
- AddValidatorsFromAssemblyContaining<Program>() — авторегистрация всех
IValidator<T> из сборки food-market.api.
- ValidationFilter (IAsyncActionFilter) глобально подключён через
MvcOptions: на каждый action ищет IValidator<TArg> по рантайм-типу
body-параметра, гоняет, fail → 400 ValidationProblemDetails (RFC 7807).
Не используем FluentValidation.AspNetCore — официально deprecated
(см. docs.fluentvalidation.net/aspnet); current recommendation —
DI-extensions + manual filter, как у нас.
Валидаторы (для 5 DTO):
- SupplyInputValidator — Supplier/Store/Currency ≠ Empty, Date ≤ tomorrow,
Lines non-empty, line.Quantity > 0, line.UnitPrice ≥ 0.
- RetailSaleInputValidator — Store/Currency ≠ Empty, Date ≤ tomorrow,
PaidCash/PaidCard ≥ 0, Lines non-empty с per-line проверками.
- ProductInputValidator — Name required, Vat∈[0,100], MinStock ≤ MaxStock.
- CounterpartyInputValidator — Name required, BIN/ИИН regex \d{12},
Email формат (EmailAddress).
- EmployeeInputValidator — LastName/FirstName required, RoleId ≠ Empty,
SendInvite → требует CreateAccount + Email, CreateAccount → требует Email.
Сообщения по-русски (фронт ждёт RU).
Тесты: 16 юнит-тестов на валидаторы (5 на SupplyInput, 2 на RetailSaleInput,
4 на ProductInput, 2 на CounterpartyInput, 3 на EmployeeInput). Полный
прогон unit-тестов зелёный.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ec0cff7fc4 |
feat(concurrency): RowVersion на документах через Postgres xmin (TD-6)
Optimistic concurrency через системную колонку Postgres xmin — никакой
дополнительной колонки и миграции не нужно, xmin есть у каждой таблицы
и автоматически обновляется при UPDATE.
Конфигурация:
- IVersionedEntity (маркер) + uint Xmin на Supply, Demand, RetailSale,
Transfer, InventoryDoc.
- e.UseXminAsConcurrencyToken() в EF-конфиге для каждой — создаёт shadow
property "xmin" с IsConcurrencyToken + ValueGeneratedOnAddOrUpdate.
- e.Ignore(x => x.Xmin): .NET-property живёт только для транспорта в DTO,
не маппится в БД (xmin тащим shadow'ом).
- GetInternal в SuppliesController читает xmin через
EF.Property<uint>(s, "xmin") в LINQ-проекции и складывает в DTO.
Wire-up:
- SuppliesController.Update принимает input.Xmin (uint?), сверяет с
shadow xmin загруженного supply через EF.Entry().Property("xmin").
Несовпадение → 409 с code=concurrency_conflict. null/0 от клиента →
legacy compat, проверки нет.
- SaveOrFkErrorAsync ловит DbUpdateConcurrencyException → 409 (двойная
защита: и явная сверка, и EF auto-check в SaveChanges).
Bonus: Supply.Update перешёл на тот же паттерн что Demand/RetailSale —
ExecuteDelete старых строк + AddRange новых напрямую в DbSet. Старый
RemoveRange-then-Add через nav-collection ломал EF concurrency check
(UPDATE supply_lines одной из старых строк падал 0 affected внутри той
же SaveChanges-транзакции).
Тесты: 2 интеграционных:
- two parallel updates with same xmin → один 204, другой 409; retry
с новым xmin тоже 204.
- legacy clients без xmin → PUT работает без concurrency-проверки.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b963adfa2e |
feat(import-jobs): persisted ImportJobRegistry в БД (TD-5)
Раньше прогресс фоновых импортов жил в ConcurrentDictionary внутри
Singleton-сервиса: рестарт процесса терял всю историю, активные
джобы навсегда оставались в статусе Running.
Теперь:
- Domain.Integrations.ImportJob (TenantEntity) — таблица import_jobs,
миграция Phase8c_ImportJobs (jsonb для ErrorsJson, индексы по
OrgId+StartedAt / OrgId+Status / FinishedAt).
- ImportJobRegistry рефакторен: Create() пишет строку немедленно,
SaveAsync() обновляет, Get/RecentlyFinished читают из БД. API
совместимое со старой in-memory версией — MoySkladImportService
и контроллеры не меняются.
- MoySkladImportController.RunInBackgroundAsync теперь:
* Periodic flush через Timer каждые 2 секунды — UI видит
реальный progress (Stage/Created/Total), а не Create-snapshot;
* Финальный flush в finally — обязательный для terminal state.
- AdminCleanupController.WipeAllAsync — то же финальное сохранение.
- SkipAudit=true для import-job записей — служебные, в OrgAuditLog
не пишем.
Tenant-isolation: query-filter работает прозрачно, B не видит джоб A.
Тесты: 3 интеграционных (survives across scope, RecentlyFinished
читает из БД, tenant-isolation).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|