docs(stock): отчёт и скриншоты проверки фильтров остатков (stage, SQL, UI)
Проверка задачи 11: интеграционный тест (эндпоинт), SQL по реальному датасету МойСклад (org FOOD MARKET: в наличии 4746 / нулевые 23418 / минус 2829 / всего 30993; группа «Кондитерские…» зеро+минус 3848), живой stage-API на сид-данных и скриншоты реальной админки (три галочки, URL states=…). StockReportPage не менялся. Прод не трогали. Нужен рабочий пароль SuperAdmin для скриншота именно org FOOD MARKET. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d49b586904
commit
e6cdaa9839
78
docs/stock-filter-2026-10-11/REPORT.md
Normal file
78
docs/stock-filter-2026-10-11/REPORT.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Фильтры остатков «В наличии / Нулевые / В минусе» (задача 11, 11.10.2026)
|
||||
|
||||
Галочки состояния остатка объединяются (ИЛИ); группа/склад/поиск сужают (И).
|
||||
Пусто → positive+negative (нулевые скрыты).
|
||||
|
||||
## Что сделано
|
||||
- **API** `StockController` (`/api/inventory/stock` и `/stock/export`): параметр
|
||||
`states` = набор `positive|zero|negative` (через запятую или повтором). Один WHERE:
|
||||
`(positive И qty>0) ИЛИ (zero И qty=0) ИЛИ (negative И qty<0)` по остатку
|
||||
(COALESCE-подзапрос; товар без строки Stock = 0). total/пагинация/сортировка
|
||||
корректны. Старые `includeZero`/`negativeOnly` оставлены для совместимости
|
||||
(`includeZero` ≡ все три; `negativeOnly` ≡ negative); `states` приоритетнее.
|
||||
- **Веб-админка** `StockPage`: три галочки «В наличии» (>0), «Нулевые» (=0),
|
||||
«В минусе» (<0); состояние в URL (`?states=…`); экспорт учитывает галочки.
|
||||
Ссылки `NegativeStockBanner`/`OverdueNegativeStockBlock`/`NotificationCenter`
|
||||
на минусовые → `states=negative`.
|
||||
- **`StockReportPage`** НЕ меняли: это отдельный отчёт по остаткам на дату с одной
|
||||
галочкой «Показывать нулевые остатки» (другой эндпоинт), три состояния к нему
|
||||
не относятся.
|
||||
- Деплой на **stage** (`test.admin.food-market.kz`). Прод не трогали.
|
||||
|
||||
## Проверка
|
||||
|
||||
### 1. Интеграционный тест (эндпоинт) — 1/1 зелёный
|
||||
`tests/food-market.IntegrationTests/StockStateFilterTests.cs`: сочетания
|
||||
(zero; negative; positive; zero+negative; +группа; все три = весь каталог;
|
||||
пусто = positive+negative; со складом) и числовые тождества
|
||||
(positive+negative = пусто; positive+zero+negative = весь каталог) + совместимость
|
||||
старых флагов. EF8 переводит предикат (в логе SQL `COALESCE((…SUM…),0) < 0` и т.п.).
|
||||
|
||||
### 2. SQL по реальному датасету МойСклад (org «FOOD MARKET», foodmarket-migr)
|
||||
org id `eb5db1ae-57c1-4f32-a646-13807d49de54`, аккаунт
|
||||
`foodmarket-migr@food-market.local`. Та же агрегатная логика, что и в контроллере
|
||||
(`COALESCE(SUM(Quantity),0)`, `IsService=false AND IsArchived=false`):
|
||||
|
||||
| состояние | факт (SQL) | ожидание владельца |
|
||||
|---|---|---|
|
||||
| В наличии (>0) | **4746** | ~4752 |
|
||||
| Нулевые (=0) | **23418** | ~23415 (МойСклад «Доступно: Нулевое» 23 419) |
|
||||
| В минусе (<0) | **2829** | ~2826 |
|
||||
| Всего | **30993** | 30 993 |
|
||||
|
||||
Тождества: 4746 + 2829 = **7575** (по умолчанию) ; 4746 + 23418 + 2829 = **30993**
|
||||
(весь каталог). Небольшие расхождения с ожиданием — живые продажи/синхронизация
|
||||
(как и отмечено в задаче).
|
||||
|
||||
**Группа + зеро/минус:** группа «Кондитерские изделия и снеки»
|
||||
(`eacc1fc2-1734-4dcd-a18c-4441d5cc6303`, с 2 подгруппами): zero+negative = **3848**
|
||||
(нулевые 3510, минусовые 338) — проверено SQL (с учётом подгрупп по Path, как в
|
||||
`DescendantGroupIdsAsync`).
|
||||
|
||||
### 3. Живой stage-API (деплой) на сид-данных
|
||||
Свежий стенд-аккаунт (обычный публичный signup), 3 товара (в наличии/нулевой/минус):
|
||||
`default(pos+neg)=2 · positive=1 · zero=1 · negative=1 · zero,negative=2 · all=3` —
|
||||
ровно по правилу.
|
||||
|
||||
### 4. Скриншоты реальной админки (test.admin)
|
||||
`s1-default.png` … `s6-all.png` + `index.html` — на том же стенд-аккаунте:
|
||||
- по умолчанию (ни одна галочка) → 2 (в наличии + в минусе), URL без `states`;
|
||||
- «В наличии» → 1 (`?states=positive`); «Нулевые» → 1 (`?states=zero`);
|
||||
«В минусе» → 1 (`?states=negative`);
|
||||
- «Нулевые»+«В минусе» → 2 (`?states=zero,negative`) — см. скриншот
|
||||
(товар −3 и товар 0);
|
||||
- все три → 3 (весь каталог, `?states=positive,zero,negative`).
|
||||
Состояние галочек отражается в URL (ссылку можно скопировать).
|
||||
|
||||
## Что НЕ сделано / нужно решение владельца
|
||||
- **Скриншот именно org «FOOD MARKET» в UI** не снят: сохранённый пароль
|
||||
SuperAdmin стенда (`~/.fm-watchdog/stage-superadmin-password`) не подходит
|
||||
(`invalid_grant`), а сбрасывать пароль аккаунта `foodmarket-migr` я не стал —
|
||||
это операция над чужими учётными данными (авто-режим её и заблокировал). Числа
|
||||
этой org проверены SQL (таблица выше) и эндпоинт — интеграционным тестом; UI-поведение
|
||||
снято на идентичном коде (свежий стенд-аккаунт). Если нужны скриншоты именно
|
||||
в org «FOOD MARKET» — пришлите рабочий пароль SuperAdmin стенда или аккаунта
|
||||
`foodmarket-migr`.
|
||||
- **iOS-клиент ALL MARKET Staff** (три галочки) — ваша часть (п. 6): переделываете
|
||||
вы и выкладываете в TestFlight. Серверный контракт (`states`) готов и на stage.
|
||||
- **Прод** не трогали (по правилу — без слова владельца).
|
||||
10
docs/stock-filter-2026-10-11/index.html
Normal file
10
docs/stock-filter-2026-10-11/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!doctype html><meta charset="utf-8"><title>Фильтры остатков 2026-10-11</title>
|
||||
<style>body{font:14px system-ui;margin:0;padding:24px;max-width:1100px;margin:auto;background:#f8fafc}
|
||||
figure{margin:0 0 20px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
|
||||
figcaption{padding:8px 12px;font-size:13px;color:#475569;background:#f1f5f9}img{display:block;width:100%}code{font-size:11px;color:#64748b}</style>
|
||||
<h1>Фильтры остатков «В наличии / Нулевые / В минусе» — админка test.admin</h1><figure><figcaption>По умолчанию (ни одна галочка — в наличии + в минусе) — «2 позиций по всем складам»<br><code>https://test.admin.food-market.kz/inventory/stock</code></figcaption><img src="s1-default.png" loading="lazy"></figure>
|
||||
<figure><figcaption>Только «В наличии» — «1 позиций по всем складам»<br><code>https://test.admin.food-market.kz/inventory/stock?states=positive</code></figcaption><img src="s2-positive.png" loading="lazy"></figure>
|
||||
<figure><figcaption>Только «Нулевые» — «1 позиций по всем складам»<br><code>https://test.admin.food-market.kz/inventory/stock?states=zero</code></figcaption><img src="s3-zero.png" loading="lazy"></figure>
|
||||
<figure><figcaption>Только «В минусе» — «1 позиций по всем складам»<br><code>https://test.admin.food-market.kz/inventory/stock?states=negative</code></figcaption><img src="s4-negative.png" loading="lazy"></figure>
|
||||
<figure><figcaption>«Нулевые» + «В минусе» (объединение) — «2 позиций по всем складам»<br><code>https://test.admin.food-market.kz/inventory/stock?states=zero%2Cnegative</code></figcaption><img src="s5-zero-negative.png" loading="lazy"></figure>
|
||||
<figure><figcaption>Все три = весь каталог — «3 позиций по всем складам»<br><code>https://test.admin.food-market.kz/inventory/stock?states=positive%2Czero%2Cnegative</code></figcaption><img src="s6-all.png" loading="lazy"></figure>
|
||||
BIN
docs/stock-filter-2026-10-11/s1-default.png
Normal file
BIN
docs/stock-filter-2026-10-11/s1-default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
BIN
docs/stock-filter-2026-10-11/s2-positive.png
Normal file
BIN
docs/stock-filter-2026-10-11/s2-positive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
BIN
docs/stock-filter-2026-10-11/s3-zero.png
Normal file
BIN
docs/stock-filter-2026-10-11/s3-zero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
BIN
docs/stock-filter-2026-10-11/s4-negative.png
Normal file
BIN
docs/stock-filter-2026-10-11/s4-negative.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
BIN
docs/stock-filter-2026-10-11/s5-zero-negative.png
Normal file
BIN
docs/stock-filter-2026-10-11/s5-zero-negative.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
BIN
docs/stock-filter-2026-10-11/s6-all.png
Normal file
BIN
docs/stock-filter-2026-10-11/s6-all.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
32
docs/stock-filter-2026-10-11/ui-results.json
Normal file
32
docs/stock-filter-2026-10-11/ui-results.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
[
|
||||
{
|
||||
"caption": "По умолчанию (ни одна галочка — в наличии + в минусе)",
|
||||
"count": "2 позиций по всем складам",
|
||||
"url": "https://test.admin.food-market.kz/inventory/stock"
|
||||
},
|
||||
{
|
||||
"caption": "Только «В наличии»",
|
||||
"count": "1 позиций по всем складам",
|
||||
"url": "https://test.admin.food-market.kz/inventory/stock?states=positive"
|
||||
},
|
||||
{
|
||||
"caption": "Только «Нулевые»",
|
||||
"count": "1 позиций по всем складам",
|
||||
"url": "https://test.admin.food-market.kz/inventory/stock?states=zero"
|
||||
},
|
||||
{
|
||||
"caption": "Только «В минусе»",
|
||||
"count": "1 позиций по всем складам",
|
||||
"url": "https://test.admin.food-market.kz/inventory/stock?states=negative"
|
||||
},
|
||||
{
|
||||
"caption": "«Нулевые» + «В минусе» (объединение)",
|
||||
"count": "2 позиций по всем складам",
|
||||
"url": "https://test.admin.food-market.kz/inventory/stock?states=zero%2Cnegative"
|
||||
},
|
||||
{
|
||||
"caption": "Все три = весь каталог",
|
||||
"count": "3 позиций по всем складам",
|
||||
"url": "https://test.admin.food-market.kz/inventory/stock?states=positive%2Czero%2Cnegative"
|
||||
}
|
||||
]
|
||||
66
tests/e2e/tools/stock-filter-verify.mjs
Normal file
66
tests/e2e/tools/stock-filter-verify.mjs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Скриншоты фильтров остатков «В наличии / Нулевые / В минусе» на реальной
|
||||
// админке test.admin.food-market.kz (свежий стенд-аккаунт). Вывод — docs/stock-filter-2026-10-11/.
|
||||
import { chromium } from '@playwright/test'
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const BASE = process.env.ADMIN_URL ?? 'https://test.admin.food-market.kz'
|
||||
const OUT = resolve('/home/nns/food-market', 'docs/stock-filter-2026-10-11')
|
||||
mkdirSync(OUT, { recursive: true })
|
||||
const email = readFileSync('/tmp/stockui-email.txt', 'utf8').trim()
|
||||
const password = readFileSync('/tmp/stockui-pw.txt', 'utf8').trim()
|
||||
const shots = [], results = []
|
||||
|
||||
const b = await chromium.launch({ args: ['--ignore-certificate-errors'] })
|
||||
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, ignoreHTTPSErrors: true })
|
||||
const p = await ctx.newPage()
|
||||
|
||||
// Логин
|
||||
await p.goto(BASE + '/', { waitUntil: 'networkidle' })
|
||||
await p.locator('input[type="email"]').fill(email)
|
||||
await p.locator('input[type="password"]').fill(password)
|
||||
await p.getByRole('button', { name: /Войти/ }).click().catch(async () => { await p.locator('button[type="submit"]').click() })
|
||||
await p.waitForTimeout(2500)
|
||||
|
||||
await p.goto(BASE + '/inventory/stock', { waitUntil: 'networkidle' })
|
||||
await p.waitForTimeout(1500)
|
||||
|
||||
const boxes = p.getByRole('checkbox')
|
||||
async function ensure(pos, zero, neg) {
|
||||
const want = [pos, zero, neg]
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const cur = await boxes.nth(i).isChecked()
|
||||
if (cur !== want[i]) await boxes.nth(i).click()
|
||||
}
|
||||
await p.waitForTimeout(1200)
|
||||
}
|
||||
async function countText() {
|
||||
return (await p.locator('text=/позиц/').first().innerText().catch(() => '')) || ''
|
||||
}
|
||||
async function step(file, caption, pos, zero, neg) {
|
||||
await ensure(pos, zero, neg)
|
||||
const txt = await countText()
|
||||
const url = p.url()
|
||||
await p.screenshot({ path: resolve(OUT, file) })
|
||||
shots.push({ file, caption: `${caption} — «${txt.trim()}»`, url })
|
||||
results.push({ caption, count: txt.trim(), url })
|
||||
console.log(`${caption}: ${txt.trim()} [${url}]`)
|
||||
}
|
||||
|
||||
await step('s1-default.png', 'По умолчанию (ни одна галочка — в наличии + в минусе)', false, false, false)
|
||||
await step('s2-positive.png', 'Только «В наличии»', true, false, false)
|
||||
await step('s3-zero.png', 'Только «Нулевые»', false, true, false)
|
||||
await step('s4-negative.png', 'Только «В минусе»', false, false, true)
|
||||
await step('s5-zero-negative.png', '«Нулевые» + «В минусе» (объединение)', false, true, true)
|
||||
await step('s6-all.png', 'Все три = весь каталог', true, true, true)
|
||||
|
||||
await b.close()
|
||||
|
||||
const rows = shots.map((s) => `<figure><figcaption>${s.caption}<br><code>${s.url}</code></figcaption><img src="${s.file}" loading="lazy"></figure>`).join('\n')
|
||||
writeFileSync(resolve(OUT, 'index.html'), `<!doctype html><meta charset="utf-8"><title>Фильтры остатков 2026-10-11</title>
|
||||
<style>body{font:14px system-ui;margin:0;padding:24px;max-width:1100px;margin:auto;background:#f8fafc}
|
||||
figure{margin:0 0 20px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
|
||||
figcaption{padding:8px 12px;font-size:13px;color:#475569;background:#f1f5f9}img{display:block;width:100%}code{font-size:11px;color:#64748b}</style>
|
||||
<h1>Фильтры остатков «В наличии / Нулевые / В минусе» — админка test.admin</h1>${rows}`)
|
||||
writeFileSync(resolve(OUT, 'ui-results.json'), JSON.stringify(results, null, 2))
|
||||
console.log(`\n[stock-filter-ui] ${shots.length} скриншотов → ${OUT}`)
|
||||
Loading…
Reference in a new issue