food-market/tests/e2e/scripts/make-invoice-fixture.ts
nns f434e87d9b
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 Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
fix(hotfix): invoice500 + фоновые 403 на reload + regression e2e
Owner caught on stage (2026-06-07 18:57):
1) POST /api/purchases/invoices/upload → 500: migration Phase31a создала
   invoice_uploads.UpdatedAt как NOT NULL, но конвенция проекта — DateTime?
   nullable + стамп только для Modified в AppDbContext.StampTimestamps.
   EF на INSERT отправлял UpdatedAt=NULL → 23502. Фикс: Phase31b DROP NOT NULL
   + Phase31b для invoice_aliases.
2) Фоновые 403 → тост «Нет доступа» при F5 на любой странице:
   - SavedPresets фетчил /api/user/presets (RequiresAdvancedMode) из любой
     страницы с фильтрами в простом режиме → 403;
   - useOnboardingProgress фетчил /api/organization/employees (Admin only)
     для кладовщика/кассира на '/' → 403.
   Фикс:
   - api.ts: GET-403 silent, тост только на действия юзера (POST/PUT/DELETE);
   - SavedPresets gated на advancedMode (useMe + enabled);
   - useOnboardingProgress canSeeEmployees-gate.
3) tests/food-market.IntegrationTests/InvoiceUploadHttpTests.cs — HTTP-прогон
   POST /upload через Testcontainers-Postgres (дыра: s31 pipeline tests
   проверяли matcher, но ни разу не инсертили в invoice_uploads).
4) tests/e2e/scenarios/stage-ui-menu-walk.spec.ts — regress: каждая демо-роль,
   каждый пункт меню, F5, assert 0 unexpected 4xx/5xx + 0 error-тостов +
   0 console.error. В quality-watchdog step 9 с 4-часовой каденцией.
5) tests/e2e/scenarios/stage-ui-invoice-upload.spec.ts — demo-keeper
   загружает реальное фото накладной (tests/fixtures/invoice-sample.jpg,
   сгенерирована Playwright-ом из HTML, 1800×2400 JPEG) → ждёт recognition
   → экран проверки. При не-настроенном Claude CLI тест fail'ится с ясным
   сообщением (это тоже баг).

Прогон локально 4/4 (InvoicePipelineTests 2/2 + InvoiceUploadHttpTests 2/2).
docs/hotfix-invoice500-403-progress.md — ход работы 1-4/5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-05 00:21:41 +05:00

94 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Генератор фикстуры «фото накладной»: рендерит HTML-таблицу через
// Playwright Chromium → JPEG → кладёт в tests/fixtures/invoice-sample.jpg.
//
// Запуск: pnpm exec tsx scripts/make-invoice-fixture.ts
//
// Фикстуру коммитим в репо (однажды сгенерирована — используется всеми
// e2e-прогонами, в т.ч. в quality-watchdog). Пересобирать при изменении
// HTML.
import { chromium } from 'playwright'
import { writeFileSync, mkdirSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dir = dirname(fileURLToPath(import.meta.url))
const OUT = resolve(__dir, '..', '..', 'fixtures', 'invoice-sample.jpg')
const html = `<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<style>
* { box-sizing: border-box }
body { font-family: Helvetica, Arial, sans-serif; color: #111; margin: 0; padding: 24px; background: white }
.meta { display: flex; justify-content: space-between; margin-bottom: 18px; font-size: 13px }
.title { font-size: 22px; font-weight: 700; margin: 6px 0 14px }
.sub { font-size: 13px; color: #444; margin-bottom: 20px }
table { width: 100%; border-collapse: collapse; font-size: 12px }
th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top }
th { background: #eee; text-align: left; font-weight: 700 }
td.n { text-align: right; font-variant-numeric: tabular-nums }
tfoot td { font-weight: 700 }
.footnote { margin-top: 18px; font-size: 11px; color: #333 }
</style>
</head>
<body>
<div class="meta">
<div>
<div><b>Поставщик:</b> ТОО "Тестовый Поставщик"</div>
<div><b>БИН:</b> 123456789012</div>
<div><b>Адрес:</b> г. Алматы, ул. Абая 100</div>
</div>
<div style="text-align: right">
<div><b>Накладная № Т-00042</b></div>
<div><b>Дата:</b> 03.10.2026</div>
<div><b>Валюта:</b> KZT</div>
</div>
</div>
<div class="title">Товарная накладная</div>
<div class="sub">Покупатель: ТОО "Food Market Demo" · Склад: Центральный</div>
<table>
<thead>
<tr>
<th>№</th>
<th>Наименование</th>
<th>Штрихкод</th>
<th>Код пост.</th>
<th>Ед.</th>
<th class="n">Кол-во</th>
<th class="n">Цена</th>
<th class="n">Сумма</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>Молоко «Домик в деревне» 3.2% 950мл</td><td>4870022200015</td><td>MLK-095</td><td>шт</td><td class="n">24</td><td class="n">420.00</td><td class="n">10080.00</td></tr>
<tr><td>2</td><td>Хлеб «Деревенский» 500г</td><td>4870022200022</td><td>BRD-500</td><td>шт</td><td class="n">30</td><td class="n">180.00</td><td class="n">5400.00</td></tr>
<tr><td>3</td><td>Яйцо куриное С1, лоток 10шт</td><td>4870022200039</td><td>EGG-10</td><td>уп</td><td class="n">15</td><td class="n">720.00</td><td class="n">10800.00</td></tr>
<tr><td>4</td><td>Сахар-песок 1кг «Рафинад»</td><td>4870022200046</td><td>SUG-1K</td><td>кг</td><td class="n">40</td><td class="n">360.00</td><td class="n">14400.00</td></tr>
<tr><td>5</td><td>Масло подсолн. рафинир. 1л</td><td>4870022200053</td><td>OIL-1L</td><td>шт</td><td class="n">12</td><td class="n">980.00</td><td class="n">11760.00</td></tr>
</tbody>
<tfoot>
<tr><td colspan="7">ИТОГО:</td><td class="n">52440.00</td></tr>
</tfoot>
</table>
<div class="footnote">
Отпустил: Иванов И. И. _______________ · Принял: _______________ ·
М.П.
</div>
</body>
</html>`
async function main() {
mkdirSync(dirname(OUT), { recursive: true })
const browser = await chromium.launch()
const context = await browser.newContext({ viewport: { width: 900, height: 1200 }, deviceScaleFactor: 2 })
const page = await context.newPage()
await page.setContent(html, { waitUntil: 'load' })
const buffer = await page.screenshot({ fullPage: true, type: 'jpeg', quality: 92 })
writeFileSync(OUT, buffer)
console.log('fixture written:', OUT, buffer.length, 'bytes')
await context.close()
await browser.close()
}
main().catch((e) => { console.error(e); process.exit(1) })