This commit is contained in:
2026-07-12 19:21:35 +02:00
parent f2d944b117
commit 41cb412cb0
49 changed files with 3170 additions and 52 deletions
@@ -0,0 +1,91 @@
---
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.