# Sprint 9 — лояльность, акции, mobile-адаптация, PWA

Цель: программы лояльности (баллы/% скидка) для постоянных покупателей,
промокоды/акции в чеке, починить узкие экраны (телефон/планшет),
PWA-обёртка владельца для отчётов с homescreen-икоником.

Старт: 2026-06-01. Исполнитель: Claude Opus 4.7 (автономный режим).

Это последний автономно-безопасный спринт. Дальше нужен человек:
ОФД-интеграция, MoySklad-токены, POS WPF на Windows, kz-локализация,
прод-деплой.

## Принципы

- Multi-tenant обязателен (`OrganizationId` на каждой новой таблице, query filter).
- Каждый пункт: `dotnet build` + локальные тесты + `~/deploy-stage.sh` + retest на `https://test.admin.food-market.kz` (включая mobile viewport 375x667).
- НЕ трогать: `global.json`, прод-стек (admin.food-market.kz), POS WPF.

## Чек-лист

- [x] **1. P2-12 Loyalty (программы + карты)** — Phase9b миграция. `LoyaltyProgramsController` + `LoyaltyCardsController` (/issue, /lookup, /block). RetailSale: input.LoyaltyCardNumber → расчёт скидки/баллов; Post начисляет в card.Balance. UI: `/loyalty/programs`, `/loyalty/cards`. Тесты: 3/3 integration + 2/2 stage.
- [x] **2. P2-13 Promotions (промокоды/акции)** — `Promotion` (Percent|FixedDiscount, Scope, jsonb-массивы Guid, период, Code unique per org). `PromotionsController`. RetailSale: input.PromotionCode → lookup+matchingSubtotal+snapshot. UI: `/promotions`. Тесты: 2/2 integration + 2/2 stage.
- [x] **3. Mobile-адаптация** — `stage-ui-s9-mobile-audit.spec.ts` снял 20 screenshot'ов (375 + 768 viewport × 10 ключевых страниц с seed-demo) в `tests/e2e/reports/mobile/`. DataTable: `min-w-max sm:min-w-[640px]` — узкие таблицы (Loyalty/Promotions) укладываются без horizontal scroll, широкие — скроллятся внутри. EmptyState/ConfirmDialog/Drawer уже mobile-friendly из Sprint 7. Реальные layout-break'и не найдены.
- [x] **4. P2-9 PWA владельца (read-only)** — `public/manifest.webmanifest` (start_url=/dashboard, shortcuts на отчёты, display=standalone). `public/sw.js` (network-first SPA navigate + offline-fallback, GET /api/* network-first+cache, статика SWR, /hubs/* skip). `public/offline.html` static fallback. `index.html`: <link rel=manifest>, apple-touch-meta, lang=ru-KZ. `main.tsx` регистрирует SW только в PROD. nginx: /sw.js no-cache, /manifest.webmanifest правильный mime. Тесты: `stage-ui-s9-pwa.spec.ts` 3/3 ✓ + SignalR regression ✓.

## Журнал

### 2026-06-01 — старт

Sprint 8 закрыт (`docs/sprint8-progress.md`, 4/4 ✓, 8/8 stage e2e). Перехожу к пункту 1 (Loyalty).

### 2026-06-01 — итог

**Все 4 пункта ✓** на `https://test.admin.food-market.kz`. Stage 8/8 e2e зелёные.

| # | Тема | Тесты | Коммит |
|---|---|---|---|
| 1 | Loyalty programs+cards | int 3/3 + stage 2/2 | `91128a7` |
| 2 | Promotions | int 2/2 + stage 2/2 | `91128a7` |
| 3 | Mobile-адаптация | 20 screenshot'ов + 1/1 audit | `76a175f` |
| 4 | PWA (read-only) | stage 3/3 | `76a175f` + `12d833f` |

**Сделано:**

1. **Loyalty**: Phase9b migration (3 таблицы + 6 колонок на retail_sales). `LoyaltyProgram` (Percentage|FixedAmount|PointsAccrual) + `LoyaltyCard` с unique CardNumber. `LoyaltyProgramsController` + `LoyaltyCardsController` (CRUD + /issue + /lookup + /block). `RetailSalesController.Create/Update` + `ApplyLoyaltyAndPromotionAsync`: input.LoyaltyCardNumber → discount/points, snapshot в LoyaltyCardId/LoyaltyBonusApplied/LoyaltyPointsAccrued; Post начисляет баллы внутри транзакции. UI: `/loyalty/programs` + `/loyalty/cards`. `LoyaltyManage` permission.
2. **Promotions**: `Promotion` (Percent|FixedDiscount, Scope=All|ProductGroups|Products, jsonb-массивы Guid, Code unique per org через 23505→409). `PromotionsController` (CRUD). RetailSale: input.PromotionCode → period+MinSaleAmount+matchingSubtotal+snapshot. UI: `/promotions`. `PromotionsManage` permission.
3. **Mobile-адаптация**: аудит-spec прогоняет 20 страниц в 375 + 768 viewport, складывает screenshots в `reports/mobile/`. DataTable: `min-w-max sm:min-w-[640px]` для узких таблиц. Layout-bug'ов не найдено (Sprint 7 уже сделал AppLayout/Drawer/EmptyState mobile-ready).
4. **PWA**: manifest + sw.js + offline.html + nginx-правила. SW стратегии: navigate=network+offline-fallback, /api/*=network+cache, статика=SWR, /hubs/*=skip (иначе SignalR negotiate рвался). PROD-only регистрация. Lighthouse-аудит не запускали — это требует Chrome DevTools, у нас Playwright headless; basic checks (manifest mime, SW active, offline page) проходят.

**Что осталось на следующий sprint (не автономно):**
- ОФД-интеграция (требует регистрации фискального оператора).
- MoySklad-токены для прода.
- POS WPF: тестирование на реальном Windows + Кассе с весами.
- kz-локализация (качественный перевод нужен от живого).
- Lighthouse PWA-аудит → score (CI integration).
- Прод-деплой на admin.food-market.kz.
