--- plan_id: 20260706-2046-import-paragonow title: Półautomatyczny import paragonów (AI parsowanie + uczone podpowiedzi kategorii) completed: 2026-07-06T22:30 storage: plan-first quality_radar: degraded --- # Summary: Półautomatyczny import paragonów ## Objective Umożliwić wgranie paragonu (jpg/png, pdf, json), automatyczne odczytanie pozycji przez AI (OpenAI), dopasowanie ich do kategorii wydatków z uczonymi podpowiedziami, przegląd/korektę przez użytkownika i zaksięgowanie jako operacje pogrupowane po kategorii (1 operacja `expense` / kategoria). Nic nie trafia do `operations` bez zatwierdzenia; podpowiedzi kategorii nigdy nie księgują się automatycznie. ## What Was Built | Area | Result | |------|--------| | Baza | 3 tabele: `receipts`, `receipt_items` (+ `display_name`), `receipt_category_map`; kolumna `operations.receipt_id` (FK SET NULL) | | Modele | `ReceiptModel` (`withCounts`), `ReceiptItemModel` (`forReceipt`), `ReceiptCategoryMapModel` (`normalize`/`suggest`/`remember`) | | Parser AI | `App\Libraries\ReceiptParser` — OpenAI **Responses API** (`/v1/responses` + `input_file`), model **o4-mini**, prompt świadomy kolumn paragonu, dwie nazwy (`name` surowa + `display` ładna) | | Kontroler | `Receipts` (upload→parse→review→confirm→history→file→delete) + trasy `receipts/*` | | Widoki | `receipts/{new,review,index,show}.php`; przegląd z edytowalnymi nazwami/kwotami i podglądem grupowania na żywo; link „Import paragonów" w nawigacji | | Kategorie | `CategoryModel::tree()` rozszerzony o `path_label` („Nadrzędna → Podrzędna") — jednoznaczność w podglądzie | ## Files Modified - `app/Database/Migrations/2026-07-06-2000{01..05}_*.php` - schemat modułu + `display_name` - `app/Models/{ReceiptModel,ReceiptItemModel,ReceiptCategoryMapModel}.php` - nowe modele - `app/Models/OperationModel.php` - `allowedFields` += `receipt_id` - `app/Models/CategoryModel.php` - `path_label` w `tree()` - `app/Libraries/ReceiptParser.php` - warstwa OpenAI (Responses API, o4-mini, `name`+`display`) - `app/Controllers/Receipts.php` - pełny przepływ, walidacja bez finfo, transakcja, uczenie na surowej nazwie - `app/Config/Routes.php` - grupa `receipts/*` - `app/Views/receipts/*.php`, `app/Views/layout/main.php` - UI + nawigacja - `.env` - komentarz `openai.model` (domyślnie o4-mini) ## Acceptance Criteria Results | Criterion | Status | Evidence | |-----------|--------|----------| | AC-1 Upload jpg/png/pdf/json | Pass | `/receipts/new` HTTP 200; walidacja rozszerzeń bez finfo; plik w `writable/uploads/receipts/` | | AC-2 AI parsuje pozycje + kategorie | Pass | Responses API + o4-mini odczytał realny PDF (15 poz., suma = total 68,74); błąd API → status `failed` + `error_msg` | | AC-3 Nauczone dopasowanie ma priorytet i tylko podpowiada | Pass | `suggest()` (surowa nazwa) > propozycja AI; brak zapisu do `operations` przed `confirm` | | AC-4 Przegląd grupuje po kategorii + edycja | Pass | Edytowalne nazwa/kwota, podgląd sum na żywo, `path_label` rozróżnia kategorie | | AC-5 Zatwierdzenie: 1 operacja/kategoria + uczenie | Pass | Grupowanie sum → `OperationModel::insert` z `receipt_id`; `remember(surowa_nazwa, kategoria)`; blokada podwójnego księgowania; transakcja DB | | AC-6 Historia + podgląd skanu | Pass | `/receipts` HTTP 200; `Receipts::file` serwuje spoza `public/` pod filtrem `auth` | ## Verification Results | Check | Result | Notes | |-------|--------|-------| | `php -l` (wszystkie pliki) | Pass | Zero błędów składni | | Self-check `normalize`+grupowanie | Pass | assert OK | | Migracja produkcyjna | Pass | DDL 3 tabel + `receipt_id` + `display_name` zastosowane na zdalnej bazie; wpisy w `migrations` (batch 3 i 4) | | UAT `/receipts`, `/receipts/new` | Pass | HTTP 200 po zalogowaniu (curl) | | UAT realny paragon (PDF) | Pass | Użytkownik potwierdził: pozycje/kwoty poprawne (poza wierszem rabatu — korekta ręczna), ładne nazwy OK | ## Quality Radar Results **Status:** degraded (brak indeksu codebase-memory-mcp; jscpd/ast-grep wyłączone polityką) - New risks: OCR paragonów niedeterministyczny na wierszach rabatu/gęstym druku — świadomie kryty edytowalnymi kwotami (pół-automat). Koszt tokenów OpenAI per import (o4-mini rozumujący). - Resolved risks: zapis operacji tylko przez `OperationModel` (bez duplikacji walidacji); klucz OpenAI z `.env`, nie logowany. - Deferred risks: pełna indeksacja codebase-memory-mcp (`$paul-map-codebase`); normalizacja nazw = dokładne dopasowanie (rozbudować gdy za wąska). - Raw outputs: `.paul/codebase/{impact_map,quality_risks,tooling_status}.md` ## Deviations - **Mechanizm AI zmieniony w trakcie**: chat-completions `file_data` NIE dostarczał treści PDF (model halucynował) → przełączono na **Responses API `/v1/responses` + `input_file`**. - **Model**: plan zakładał gpt-4o-mini; okazał się za słaby na gęsty druk → domyślnie **o4-mini** (rozumujący, najlepszy OCR + rabaty, tańszy niż gpt-4o). Testy: o4-mini suma pozycji = total co do grosza; gpt-4o/4.1 gorzej. - **Bez finfo**: hostido ma wyłączone `fileinfo` → własna nazwa pliku + MIME z rozszerzenia, walidacja bez `ext_in`. - **Dwie nazwy pozycji** (dodane na życzenie): `name` (surowa, klucz mapowania) + `display_name` (ładna, do wyświetlania/opisu). - **PDF = obraz** (brak warstwy tekstowej) → OCR modelu, nie ekstrakcja tekstu. - Migracje stosowane ręcznie na zdalnej bazie (deploy hostido nie uruchamia `spark migrate`). ## Key Decisions / Patterns - Przy paragonach liczy się **rozumowanie modelu, nie rozmiar** (o4-mini > gpt-4o/4.1). - **Pół-automat**: AI proponuje, człowiek weryfikuje/koryguje kwoty i nazwy przed księgowaniem. - **Uczenie na surowej nazwie z paragonu** (stabilny klucz między wizytami), wyświetlanie po ładnej. - Jedno źródło prawdy operacji — `OperationModel`; `receipt_id` łączy operacje z paragonem (FK SET NULL). ## Follow-up - Opcjonalnie: zapamiętywanie ładnej nazwy w `receipt_category_map` (spójny display przy powtórkach). - Rozważyć limit/licznik kosztów OpenAI, jeśli wolumen wzrośnie. - `$paul-map-codebase` dla pełnego indeksu przed kolejnym większym planem.