274 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
3c731ba532 |
fix(s18): audit-log employee filter — правильный endpoint и DTO
Some checks are pending
При первом деплое /api/employees вернул 404 — реальный endpoint /api/organization/employees. Также DTO содержит lastName/firstName/ middleName отдельно, не fullName — собираем строку на клиенте. 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>
|
||
|
|
9588d03bf4 |
test(s15): axe a11y + focus traps + unit coverage 80% + property tests + backup drill
Sprint 15 финальный — реальные axe + coverage + pg_restore numbers.
Ключевые цифры:
- axe-core: critical=0 on 10 страниц stage'а; serious 12→9
после фиксов (sidebar contrast + 8 icon-only back-arrow aria-labels).
- Unit coverage: Application 56%→83%, Domain 11%→79%, combined
60%→80%. Тестов 68→147 (+79).
- Backup recovery drill: RTO ~25 секунд end-to-end
(pg_dump 2s + pg_restore 4s + dotnet startup 19s).
Что сделано:
1. @axe-core/playwright + stage-ui-15 (10 страниц) + stage-ui-16
(SR smoke на login: getByLabel, role=alert, aria-describedby,
keyboard nav).
2. useFocusTrap hook (WCAG 2.4.3 + 2.1.2): return-focus, mount-focus,
Tab cycle. Подключён к Modal + ConfirmDialog с opt-in
defaultFocus='cancel'|'confirm'. ConfirmDialog по дефолту фокусит
Cancel для destructive actions (safer чем Enter→Delete).
3. A11y фиксы:
• text-slate-400→text-slate-500 в sidebar (contrast 2.63→4.61).
• 8 страниц edit с back-arrow Link — aria-label + aria-hidden
на иконке + текст-slate-500 цвет.
• Modal close button — то же.
• LoginPage — aria-invalid/aria-describedby/role=alert на
ошибках валидации.
• Field component — role="alert" на error span (announce'ит SR).
4. 8 файлов unit-тестов: PhoneNormalization, PagedRequest,
RequiredGuid, RolePermissions (Domain), DomainPocoSmoke,
DomainFullPropertyTouch, CatalogDtosSmoke, StockServiceProperty
(4 seeds × 4 size + batch + 2-product isolation).
5. Backup-drill: pg_dump со stage'а → fresh postgres:16-alpine →
pg_restore → dotnet run против восстановленной БД → /health/ready
Healthy. Команды и timing в RUNBOOK.md.
6. Docs review:
• MULTI-TENANCY чеклист «добавить tenant-сущность» расширен с 6
до 19 шагов (Domain → EF Config → Migration с Xmin →
RolePermissions → Validation → Controller + RequiresPermission →
Audit + SensitiveOpsAudit → property tests).
• ARCHITECTURE.md — Sprint 13-15 changes таблица.
• DEVELOPER-GUIDE.md — «что добавилось после первого guide'а» +
a11y pitfalls в «что НЕ делать».
Stage smoke ✓. Это финальный автономно-безопасный спринт. Дальше
нужен вход от user'а (ОФД keys, MoySklad tokens, Windows для POS,
прод-деплой план, kz-перевод, реальный SMTP).
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>
|
||
|
|
786dacb081 |
feat(s10-4): dark mode полировка + Cmd+K палитра + аудит-spec
Some checks failed
CI / Backend (.NET 8) (push) Has been cancelled
CI / Web (React + Vite) (push) Has been cancelled
CI / POS (WPF, Windows) (push) Has been cancelled
Docker Web / Build + push Web (push) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
S10-4: script-патчер обработал 29 файлов (pages + components).
Подход: посимвольный скан каждой строки с className. Если есть
text-slate-{500..900} / bg-white / bg-slate-{50,100} / border-slate-{100,200,300}
БЕЗ dark:* для того же префикса (text/bg/border/divide/hover-bg) — добавляем
соответствующий dark-companion рядом. Идемпотентен.
Стратегия маппинга:
- text-slate-500 → +dark:text-slate-400
- text-slate-700 → +dark:text-slate-200
- text-slate-900 → +dark:text-slate-100
- bg-white → +dark:bg-slate-900
- bg-slate-50 → +dark:bg-slate-800/60
- border-slate-200 → +dark:border-slate-800
- hover:bg-slate-50 → +dark:hover:bg-slate-800/50
- … и аналогичные.
Skip если на той же строке уже есть dark:<prefix>-* (например
dark:bg-blue-500) — не трогаем чужие осознанные dark-выборы.
stage-ui-s10-dark-audit.spec.ts снимает 20 скриншотов (10 страниц
× light/dark) в reports/dark-mode/. Визуально проверены Dashboard,
ABC-report, Products — контраст ок, brand-зелёный сохранён,
sidebar/таблицы/виджеты читаемы.
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> |
||
|
|
12d833f035 |
fix(pwa): bump cache version + filter SignalR-race errors in PWA test
Some checks failed
CI / Backend (.NET 8) (push) Has been cancelled
CI / Web (React + Vite) (push) Has been cancelled
CI / POS (WPF, Windows) (push) Has been cancelled
Docker Web / Build + push Web (push) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6f9dd11b0a |
fix(pwa): SW не вмешивается в /hubs/* — SignalR negotiate сломался
Some checks are pending
SignalR через web sockets/long-poll стримит данные, его нельзя кешировать. В пред-версии SW не интерсептировал POST, но GET-fetch для negotiate проходил через SW pipeline и валился TypeError'ом. Фикс: явный return на /hubs/* перед всеми стратегиями. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
76a175f491 |
feat(pwa+mobile+s9): PWA owner read-only + mobile tweaks + S9 stage specs
Some checks are pending
Sprint 9 пункт 3 (mobile-адаптация):
- DataTable: min-w-max sm:min-w-[640px] → узкие таблицы (Loyalty, Promotions)
влезают на 375px без horizontal-scroll, широкие (Products) скроллятся
внутри overflow-auto родителя.
- Mobile-audit спека (stage-ui-s9-mobile-audit) — 20 screenshot'ов в
reports/mobile/ (375 + 768 viewport × 10 страниц + seed-demo).
Smoke: no console-errors, layouts читаемы.
Sprint 9 пункт 4 (P2-9 PWA):
- public/manifest.webmanifest — read-only PWA владельца. Shortcuts:
Дашборд, Sales/Profit/Stock отчёты. display=standalone (homescreen icon).
- public/sw.js — service worker:
• SPA navigate: network-first + offline-fallback на /offline.html.
• GET /api/*: network-first + cache-fallback (read-only кеш).
• CSS/JS/SVG: stale-while-revalidate.
• Мутации (POST/PUT/DELETE): не вмешиваемся, сеть.
- public/offline.html — статический fallback с кнопкой «Открыть дашборд».
- index.html: <link rel='manifest'>, apple-touch-meta, lang=ru-KZ.
- main.tsx: navigator.serviceWorker.register('/sw.js') в production only
(dev hot-reload не мешает).
- deploy/nginx.conf: /sw.js no-cache, /manifest.webmanifest правильный
content-type, /offline.html static.
Stage e2e:
- stage-ui-s9-loyalty.spec (4/4 ✓): programs/cards/promotions endpoints
+ UI рендер + SALE20 на 500₸ → total=400 (валидно через API).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
dc68c997c9 |
fix(loyalty): убрать unused imports (TS6133)
Some checks are pending
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>
|
||
|
|
301bf15924 |
feat(i18n): react-i18next ru/en + language switcher (P2-6a — базовая)
Some checks are pending
Подключён react-i18next с inline-ресурсами (без fetch'a). Языки ru/en с полным переводом базовых ключей (common/nav/dashboard/products/ settings/demoSeed/shortcuts/toast). kz пока fallback на ru — нужен живой переводчик (TODO). Конфигурация: - src/lib/i18n.ts — i18next + browser-language-detector. localStorage ['fm.lang'] хранит выбор пользователя. - src/components/LanguageSwitcher.tsx — Languages-иконка + <select>. - В AppLayout: ниже user-footer'a, рядом с кнопкой Выход. Переведены пока что: - AppLayout: 11 sidebar-разделов + 26 nav-ссылок + aria-label'ов. - DashboardPage: PageHeader, KPI-карточки, mini-cards каталога, заголовок графика, пустое состояние. TODO для следующих итераций (~25 страниц): - Products/Counterparties/Enters/Losses/Transfers/Inventories/Demands/ SupplierReturns/RetailSales/Supplies (edit + list — заголовки, кнопки, breadcrumbs). - OrganizationSettings (формы — все labels). - Shortcuts overlay, EmptyState texts, ConfirmDialog default labels. - kz перевод требует живого переводчика РК. 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>
|
||
|
|
f36fb146b6 |
fix(employees): после create — invalidate list query (не показывался сразу)
Найдено через UI-deep: после успешного создания нового сотрудника
EmployeesPage не вызывал refetch/invalidate на list-query, и список
показывал старые данные до ручного refresh страницы. Причина:
direct api.post вместо useCatalogMutations.create (нужен custom response
shape с generatedPassword для one-shot модалки).
Фикс: await qc.invalidateQueries({queryKey:[URL]}) сразу после успеха.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
87e60e7309 |
fix(employees): error display через humanizeError, не «Request failed»
Найдено через UI-deep: EmployeesPage в catch'е save'a доставал
err.response.data.error || err.message и показывал в модалке. На 400-ках
с ProblemDetails (errors.{field}:[msg]) error отсутствовал и попадал
generic axios «Request failed with status code 400».
Фикс: используем общий humanizeError() (тот же что в toast'е).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
3cdb819331 |
fix(catalog): уберём cache-touch после Delete — просто navigate
Предыдущий фикс с qc.removeQueries({queryKey:['/api/catalog/products', id]})
+ invalidateQueries(exact:true) — оказался не до конца верным:
1) removeQueries на ещё-mounted ProductEditPage с активной подпиской на этот
key триггерит refetch (TanStack заполняет пустой cache на active subscriber).
2) invalidateQueries({queryKey:['/api/catalog/products'], exact:true}) на
самом деле не матчит ни list (ключ имеет 6 элементов с пагинацией), ни item
(ключ из 2 элементов) — exact=true ищет ровно [...] из 1 элемента.
Правильно: просто navigate('/catalog/products'). React Query refetchOnMount
сам обновит list при заходе на ProductsPage (staleTime=0 default).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
61ca7fee90 |
fix(catalog): после Delete не refetch'аем удалённый товар
Найдено через UI-deep: после Удалить ProductEditPage делал
qc.invalidateQueries({queryKey:['/api/catalog/products']}) до navigate'a.
React Query refetch'ил конкретно ['/api/catalog/products', id] (тот что
живёт на этой же странице) → 404 → axios interceptor показывал toast
«Не найдено» поверх редиректа на список.
Фикс: сначала navigate('/catalog/products'), потом
qc.removeQueries для item-кеша + invalidate список с exact=true чтобы не
матчить вложенный item-key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
cee92d86ce |
fix(catalog): ProductEditPage — race на currencies.data + читаемая ошибка
Найдено через UI-deep тестирование (Playwright): Баг 1: race-condition. Если юзер быстро кликает Сохранить до того как прогрузился справочник currencies, цена-MoneyInput добавляет строку с currencyId='' (фолбэк `?? ''`). Сервер возвращает 400 с криптичным JSON-validation: "$.prices[0].currencyId" не парсится. Фикс: MoneyInput для цен disabled пока !currencies.data; вместо фолбэка '' возвращаемся из onChange (no-op). canSave дополнительно проверяет row.currencyId — двойная страховка. Баг 2: при ошибке сохранения page показывал "Request failed with status code 400" — generic axios message. Toast при этом показывал человеко-читаемый текст через humanizeError (api interceptor). Фикс: exporting humanizeError из @/lib/api, ProductEditPage onError использует тот же helper. Теперь form-level error == toast-сообщение. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1418c79b04 |
fix(a11y): Modal — role=dialog + aria-modal + aria-label на крестике
Найдено через UI-deep тестирование: Modal не имел ARIA-роли, screen reader не определял его как диалог. Также добавил aria-label='Закрыть' на X-кнопку. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c2ebbcc1bd |
fix(web): useShortcuts — бэр-клавиши не зависят от Shift
Some checks are pending
'?' на US-раскладке вводится через Shift+/, поэтому при нажатии e.shiftKey=true. Старая логика требовала wantShift === e.shiftKey и блокировала '?' (wantShift=false). Теперь для одиночных клавиш (без '+' в spec) сравниваем только e.key — это правильно и для '/', и для '?', и для 'n', и не ломает 'mod+s'. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
76cbe78257 |
feat(web): keyboard shortcuts на edit + list страницах + «?» overlay
Some checks are pending
Item 7 Sprint 7 — финальный пункт.
Хук: src/lib/useShortcuts.ts — поддерживает 'mod+s' (Ctrl/Cmd), Escape,
одиночные клавиши ('/', 'n', '?'). Бэр-клавиши скипают input/textarea/
contenteditable чтобы не ломать ввод. preventDefault() автоматически,
второй параметр enabled=true для конфликтов с диалогами.
Edit-страницы (9: Product + 8 doc-edit):
- mod+s = save (через canSave/canSubmit и save.isPending)
- Escape = navigate(<list-path>)
- enabled = !dialogProps.open — чтобы Esc не пересекался с ConfirmDialog
(иначе Esc бы и закрыл диалог, и навигировал на список).
List-страницы (10: Products + 9 doc-list):
- '/' = searchRef.current?.focus()
- 'n' = navigate('/<entity>/new')
(на CounterpartiesPage — открыть create-modal, т.к. там нет роута)
- SearchBar переведён на forwardRef для ref-проброса в input.
«?»-Overlay: src/components/ShortcutsOverlay.tsx — глобальный модал со
шпаргалкой. Открывается '?', закрывается Esc или кликом снаружи.
Смонтирован в AppLayout один раз.
tsc clean. На стейдже задеплоено.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
821bc4ed8d |
feat(web): Breadcrumbs на edit-страницах (Каталог / Товары / Молоко 3.2%)
Some checks are pending
Item 6 Sprint 7 — реюзабельный <Breadcrumbs items={...}> над h1 на 9 edit-pages.
Компонент: src/components/Breadcrumbs.tsx — Lucide ChevronRight как
разделитель, последний item — текущий (semibold темнее, без линка), не-
последние — кликабельные Link'и react-router (если задан to). Truncate с
title-tooltip для длинных названий.
Применено:
- ProductEditPage: Каталог / Товары / <name|Новый товар>
- SupplyEditPage: Закупки / Приёмки / <number|Новая приёмка>
- EnterEditPage / LossEditPage / TransferEditPage / InventoryEditPage:
Остатки / <тип> / <number>
- SupplierReturnEditPage: Закупки / Возвраты поставщикам / <number>
- DemandEditPage: Продажи / Оптовые отгрузки / <number>
- RetailSaleEditPage: Продажи / Чеки / <number>
Side-effect: на doc-edit pages убрана дублирующая subtitle «Черновик —
товар не списан, пока не проведёшь» (breadcrumbs дают контекст). Зелёная
плашка «Проведён <дата>» сохранилась — она несёт реальную инфу.
tsc clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
8d532927e2 |
feat(web): Empty states с CTA на list-страницах
Some checks are pending
Item 5 Sprint 7 — заменил сухое «Нет данных» в DataTable на дружелюбный центрированный блок: иконка + заголовок + объяснение + CTA «Создать первый …». Показывается только когда нет поиска/фильтров (truly fresh org), иначе обычный fallback DataTable.empty. Компонент: src/components/EmptyState.tsx — Lucide-иконка, optional actionLabel + onAction, optional secondaryLabel + onSecondary. Применено (14 страниц): - Catalog: Products (Package → /catalog/products/new), Counterparties (Users → открыть create-modal через setForm) - Inventory: Enters (PackagePlus), Losses (Trash2), Transfers (ArrowLeftRight), Inventories (ClipboardList) - Purchases: Supplies (PackagePlus), SupplierReturns (Undo2) - Sales: Demands (Truck), RetailSales (Receipt) - Reports: Sales (BarChart3), Stock (Warehouse), Profit (TrendingUp), Abc (BarChart3) — без CTA, текст «отчёт построится когда…» Тексты — конкретные («Списания фиксируют выбытие — просрочка, брак»), не «нажмите чтобы создать». tsc clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
faa13521e8 |
feat(web): loading skeletons вместо «Загрузка…» в DataTable + edit-pages
Some checks are pending
Item 4 Sprint 7 — shimmer-плейсхолдеры вместо текстовых лоадеров. Компоненты (src/components/Skeleton.tsx): - <Skeleton variant='line'|'block'|'circle' /> — базовый pulse-блок. - <TableSkeleton rows cols /> — 8 строк × N колонок с псевдослучайной шириной плейсхолдеров, чтобы превью таблицы выглядело естественно. - <FormSkeleton /> — заголовок + 2 секции по 6 полей. DataTable: при isLoading=true теперь рендерит TableSkeleton (а не «Загрузка…»). На list-страницах layout остаётся стабильным. Edit-pages: добавил guard if (!isNew && existing.isLoading) return <FormSkeleton /> на 9 doc-edit pages (ProductEdit, DemandEdit, EnterEdit, InventoryEdit, LossEdit, SupplierReturnEdit, TransferEdit, SupplyEdit, RetailSaleEdit) + OrganizationSettingsPage. До этого они показывали пустые поля formы или «Загрузка…». DashboardPage: график выручки во время загрузки теперь Skeleton block 72rem высоты (вместо текста «Загрузка…»). tsc clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
27ce8dddfc |
feat(web): toast-система — error на 4xx/5xx + success на мутации (через meta)
Some checks are pending
Item 3 Sprint 7 — заменил молчаливый rej в src/lib/api.ts на toast.error для всех 4xx/5xx (кроме 401, где идёт auto-refresh). Тосты успеха — для мутаций через meta.successMessage, чтобы избежать спама на queries. Компоненты: - src/lib/toast.ts — мин singleton API (toast.success/error/info), без deps. Дедуп подряд идущих одинаковых сообщений. Autoclose через setTimeout. - src/components/Toaster.tsx — фиксированный top-right контейнер. На мобиле растягивается до экрана с margin. Кнопка X для ручного закрытия. - src/lib/api.ts — interceptor 4xx/5xx: humanizeError() читает ProblemDetails (errors.X[0] → detail → message → title); title по статусу («Нет доступа» / «Не найдено» / «Конфликт» / «Проверьте поля» / «Слишком много запросов» / «Ошибка сервера»). Опт-аут через config.__silent=true. Глобальный mutation onSuccess (App.tsx) подтягивает meta.successMessage и показывает toast. meta.successMessage=false → опт-аут. Применено (через meta): - useCatalogMutations: create=«Создано», update=«Сохранено», remove=«Удалено» (автоматически для всех list-pages: Counterparties, Stores, Countries, PriceTypes, ProductGroups, RetailPoints, EmployeeRoles, ...) - Doc-edit pages (Demand/Enter/Inventory/Loss/SupplierReturn/Transfer/Supply/ RetailSale): save=«Сохранено», post=«Проведено», unpost=«Снято с проведения», remove=«Удалено». - ProductEditPage: save=«Сохранено», remove=«Удалено». - OrganizationSettingsPage save: «Настройки сохранены». tsc --noEmit clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
17a6da2f8b |
feat(web): ConfirmDialog компонент + useConfirm hook вместо window.confirm()
Some checks are pending
Item 2 Sprint 7 — заменил все нативные confirm() в фронте на собственный
<ConfirmDialog> с понятной типографикой, Esc=cancel, focus-on-Cancel
(чтобы случайный Enter не подтверждал удаление), tone='danger' | 'warning'.
Компоненты:
- src/components/ConfirmDialog.tsx — UI поверх Modal-overlay, AlertTriangle
иконка, primary/danger кнопки. Текст description конкретный («Удалить
товар «Молоко 3.2%»? Действие необратимо»). aria-labelledby выставлен.
- src/lib/useConfirm.ts — хук-обёртка: const { confirm, dialogProps } =
useConfirm(); if (await confirm({...})) action(). Возвращает Promise<bool>.
Button: переведён на forwardRef, чтобы dialog мог поставить фокус на Cancel.
Применено (17 страниц + 1 компонент):
- ProductEditPage (delete product)
- DemandEditPage / EnterEditPage / InventoryEditPage / LossEditPage /
SupplierReturnEditPage / TransferEditPage / SupplyEditPage / RetailSaleEditPage:
delete draft + post + unpost (всего 3 диалога на форму)
- EmployeesPage: уволить (warning) / удалить навсегда (danger), сохранена
динамика по статусу
- CounterpartiesPage / StoresPage / ProductGroupsPage / RetailPointsPage /
CountriesPage / PriceTypesPage / EmployeeRolesPage / SuperAdminUnitsOfMeasurePage:
delete с именем сущности в description
- ProductImageGallery: delete image
tsc --noEmit: clean. Текстов: в описаниях есть имена сущностей (товар,
номер документа), чтобы было ясно что именно удаляется.
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>
|