diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 7d44bfd..f7d6db4 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -16,7 +16,7 @@ Phases: 3 of 5 complete | 2 | Naprawa bledow krytycznych | TBD | Next | - | | 3 | Naprawa bledow glownych | TBD | Not started | - | | 4 | Poprawa pokrycia testami | TBD | Not started | - | -| 5 | Import finansow z Fakturowni | 3/3 | Complete | 2026-04-11 | +| 5 | Import finansow z Fakturowni | 4/5 | In progress (05-05 planned) | 2026-04-14 | ## Phase Details @@ -92,6 +92,8 @@ Phases: 3 of 5 complete - [x] 05-01: Integracja Fakturownia i automatyczny import do finansow - [x] 05-02: Mapowanie klienta po NIP + kompatybilnosc mapowan historycznych - [x] 05-03: Filtr proforma — pomijanie faktur proforma (FP*) w imporcie +- [x] 05-04: Bugfix — brak faktur kosztowych z /invoices.json?income=no (np. 486639934) + obsluga wydatkow z KSeF (odwrocone role seller/buyer) +- [ ] 05-05: Skip-list pozycji — mozliwosc oznaczenia wybranej pozycji faktury jako pomijanej (nie trafia do finance_operations) --- *Roadmap created: 2026-03-15* diff --git a/.paul/STATE.md b/.paul/STATE.md index dde4417..415ad9e 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -10,29 +10,32 @@ See: .paul/PROJECT.md (updated 2026-04-11) ## Current Position Milestone: v0.1 Stabilizacja i jakosc kodu -Phase: 5 of 5 (Import finansow z Fakturowni) - Complete -Plan: 05-03 unified -Status: Loop complete, Phase 5 complete. Next: Phase 2 -Last activity: 2026-04-11 — Phase 5 transition complete +Phase: 5 of 5 (Import finansow z Fakturowni) — 05-04 zamkniete, 05-05 w APPLY +Plan: 05-05 (skip-list pozycji) — czeka na checkpoint decyzyjny +Status: APPLY started, blocking on checkpoint:decision (new-column vs null-category) +Last activity: 2026-04-14 — 05-04 UNIFY complete, 05-05 APPLY started Progress: -- Milestone: [######----] 60% -- Phase 5: [##########] 100% +- Milestone: [#######---] 70% +- Phase 5: [########--] 80% (05-04 done, 05-05 in apply) ## Loop Position Current loop state: ``` -PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Loop complete - ready for next PLAN] +Plan 05-04: PLAN ──▶ APPLY ──▶ UNIFY + ✓ ✓ ✓ [Complete] + +Plan 05-05: PLAN ──▶ APPLY ──▶ UNIFY + ✓ ◐ ○ [APPLY in progress, checkpoint pending] ``` ## Session Continuity -Last session: 2026-04-11 -Stopped at: Phase 5 complete, ready to plan Phase 2 -Next action: /paul:plan for Phase 2 (Naprawa bledow krytycznych) -Resume file: .paul/ROADMAP.md +Last session: 2026-04-14 +Stopped at: 05-05 checkpoint decyzyjny (model danych dla skip-flag) +Next action: Uzytkownik wybiera: new-column (skip TINYINT w item_mappings) lub null-category (finance_category_id IS NULL) +Resume file: .paul/phases/05-finances-fakturownia-import/05-05-PLAN.md --- *STATE.md - Updated after every significant action* diff --git a/.paul/governance/governance_2026-04-14.jsonl b/.paul/governance/governance_2026-04-14.jsonl new file mode 100644 index 0000000..952cea7 --- /dev/null +++ b/.paul/governance/governance_2026-04-14.jsonl @@ -0,0 +1,48 @@ +{"ts":"2026-04-14T20:00:29Z","tool":"Bash","cmd":"mkdir -p \".paul/phases/05-finances\"\",\"description\":\"Ensure phase dir\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"noOutputExpected\":true},\"to","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:01:54Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\phases\\\\05-finances-fakturownia-import\\\\05-04-PLAN.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:02:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:02:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:02:18Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:08:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaApiClient.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:08:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaApiClient.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:09:01Z","tool":"Bash","cmd":"php -l \"autoload/Domain/Finances/FakturowniaApiClient.php\"\",\"description\":\"Lint FakturowniaApiClient\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/Domain/Finances/Fakt","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:09:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:09:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:09:27Z","tool":"Bash","cmd":"php -l \"autoload/Domain/Finances/FakturowniaInvoiceImporter.php\"\",\"description\":\"Lint importer\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/Domain/Finances/Fakturowni","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:10:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaApiClient.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:10:43Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tests\\\\Domain\\\\Finances\\\\FakturowniaApiClientTest.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:10:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tests\\\\run.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:11:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaApiClient.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:11:13Z","tool":"Bash","cmd":"php tests/run.php\",\"description\":\"Re-run tests\"},\"tool_response\":{\"stdout\":\"[OK] run_work_time_repository_tests\\n[OK] run_task_attachment_repository_tests\\n[OK] run_mail_to_task_importer_t","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:18:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaApiClient.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:18:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaApiClient.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:19:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tests\\\\Domain\\\\Finances\\\\FakturowniaApiClientTest.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:19:06Z","tool":"Bash","cmd":"php tests/run.php\",\"description\":\"Run tests after hotfix\"},\"tool_response\":{\"stdout\":\"[OK] run_work_time_repository_tests\\n[OK] run_task_attachment_repository_tests\\n[OK] run_mail_to_task_","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:21:19Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tools\\\\debug_fakturownia_486639934.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:21:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tools\\\\debug_fakturownia_486639934.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:23:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tools\\\\debug_fakturownia_486639934.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:23:34Z","tool":"Bash","cmd":"php tools/debug_fakturownia_486639934.php 2>&1 | tail -n 60\",\"description\":\"Re-run diagnostic (DB part)\"},\"tool_response\":{\"stdout\":\" - id=485729278 number=3331729995 issue_date=2026-04","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:25:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tools\\\\debug_fakturownia_486639934.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:26:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tools\\\\debug_fakturownia_486639934.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:26:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tools\\\\debug_fakturownia_486639934.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:26:33Z","tool":"Bash","cmd":"php tools/debug_fakturownia_486639934.php 2>&1 | head -n 200\",\"timeout\":90000,\"description\":\"Run probe of endpoints\"},\"tool_response\":{\"stdout\":\"=== KONFIGURACJA ===\\ndomain: https://","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:28:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.env","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:28:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:28:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:29:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:29:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:29:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:29:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:29:33Z","tool":"Bash","cmd":"php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php && php tests/run.php\",\"description\":\"Lint + tests\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/Domain/Fin","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:29:39Z","tool":"Bash","cmd":"rm tools/debug_fakturownia_486639934.php && rmdir tools 2>/dev/null; ls tools 2>/dev/null || echo \"tools dir removed or empty\"\",\"description\":\"Remove one-off debug script\"},\"tool_response\":{\","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:32:41Z","tool":"Bash","cmd":"php -r \"try { \\\\$pdo = new PDO('mysql:host=host700513.hostido.net.pl;dbname=host700513_crmpro;charset=utf8', 'host700513_crmpro', '8uDFNjjpwekaNs6zhrqq', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTIO","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:33:26Z","tool":"Bash","cmd":"php -r \"\\n\\\\$pdo = new PDO('mysql:host=host700513.hostido.net.pl;dbname=host700513_crmpro;charset=utf8', 'host700513_crmpro', '8uDFNjjpwekaNs6zhrqq', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION])","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:37:00Z","tool":"Bash","cmd":"php -r \"\\n\\\\$pdo = new PDO('mysql:host=host700513.hostido.net.pl;dbname=host700513_crmpro;charset=utf8', 'host700513_crmpro', '8uDFNjjpwekaNs6zhrqq', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION])","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:37:52Z","tool":"Bash","cmd":"php -r \"\\n\\\\$pdo = new PDO('mysql:host=host700513.hostido.net.pl;dbname=host700513_crmpro;charset=utf8', 'host700513_crmpro', '8uDFNjjpwekaNs6zhrqq', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION])","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:38:06Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:38:26Z","tool":"Bash","cmd":"php -r \"\\n\\\\$pdo = new PDO('mysql:host=host700513.hostido.net.pl;dbname=host700513_crmpro;charset=utf8', 'host700513_crmpro', '8uDFNjjpwekaNs6zhrqq', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION])","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:40:10Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\phases\\\\05-finances-fakturownia-import\\\\05-05-PLAN.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:41:54Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\phases\\\\05-finances-fakturownia-import\\\\05-04-SUMMARY.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:42:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:42:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T20:42:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/crmPRO"} diff --git a/.paul/governance/governance_2026-04-15.jsonl b/.paul/governance/governance_2026-04-15.jsonl new file mode 100644 index 0000000..d987888 --- /dev/null +++ b/.paul/governance/governance_2026-04-15.jsonl @@ -0,0 +1,33 @@ +{"ts":"2026-04-14T22:23:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaImportRepository.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:23:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaImportRepository.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:23:45Z","tool":"Bash","cmd":"php -l autoload/Domain/Finances/FakturowniaImportRepository.php\",\"description\":\"Lint repo\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/Domain/Finances/FakturowniaImport","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:23:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:24:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:24:14Z","tool":"Bash","cmd":"php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php && php tests/run.php\",\"description\":\"Lint + tests\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/Domain/Fin","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:24:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Controllers\\\\FinancesController.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:25:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Controllers\\\\FinancesController.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:25:06Z","tool":"Bash","cmd":"php -l autoload/Controllers/FinancesController.php && php tests/run.php 2>&1 | tail -n 3\",\"description\":\"Lint + tests\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/Contr","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:25:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\templates\\\\finances\\\\fakturownia-import-panel.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:25:53Z","tool":"Bash","cmd":"php -l templates/finances/fakturownia-import-panel.php && php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php && php -l autoload/Domain/Finances/FakturowniaImportRepository.php && php -l au","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:26:37Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tests\\\\Domain\\\\Finances\\\\FakturowniaImportRepositoryTest.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:26:41Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tests\\\\run.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:26:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tests\\\\run.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:26:49Z","tool":"Bash","cmd":"php tests/run.php\",\"description\":\"Run all tests\"},\"tool_response\":{\"stdout\":\"[OK] run_work_time_repository_tests\\n[OK] run_task_attachment_repository_tests\\n[OK] run_mail_to_task_importer_","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:27:55Z","tool":"Bash","cmd":"php -r \"\\n\\\\$pdo = new PDO('mysql:host=host700513.hostido.net.pl;dbname=host700513_crmpro;charset=utf8', 'host700513_crmpro', '8uDFNjjpwekaNs6zhrqq', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION])","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:32:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:32:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\templates\\\\finances\\\\fakturownia-import-panel.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:32:44Z","tool":"Bash","cmd":"php -l templates/finances/fakturownia-import-panel.php && php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php && php tests/run.php 2>&1 | tail -n 3\",\"description\":\"Lint + tests\"},\"too","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:38:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\templates\\\\finances\\\\fakturownia-import-panel.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T22:38:30Z","tool":"Bash","cmd":"php -l templates/finances/fakturownia-import-panel.php\",\"description\":\"Lint template\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in templates/finances/fakturownia-import-panel.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:00:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaImportRepository.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:01:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaImportRepository.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:01:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaImportRepository.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:01:35Z","tool":"Bash","cmd":"php -l autoload/Domain/Finances/FakturowniaImportRepository.php\",\"description\":\"Lint repo\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/Domain/Finances/FakturowniaImport","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:01:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Domain\\\\Finances\\\\FakturowniaInvoiceImporter.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:01:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Controllers\\\\FinancesController.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:02:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\autoload\\\\Controllers\\\\FinancesController.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:02:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\templates\\\\finances\\\\fakturownia-import-panel.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:02:59Z","tool":"Bash","cmd":"rm tests/Domain/Finances/FakturowniaImportRepositoryTest.php\",\"description\":\"Remove obsolete test file\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:03:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tests\\\\run.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:03:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\crmPRO\\\\tests\\\\run.php","cwd":"/c/visual studio code/projekty/crmPRO"} +{"ts":"2026-04-14T23:03:15Z","tool":"Bash","cmd":"php -l autoload/Domain/Finances/FakturowniaImportRepository.php && php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php && php -l autoload/Controllers/FinancesController.php && php -l templa","cwd":"/c/visual studio code/projekty/crmPRO"} diff --git a/.paul/phases/05-finances-fakturownia-import/05-04-PLAN.md b/.paul/phases/05-finances-fakturownia-import/05-04-PLAN.md new file mode 100644 index 0000000..e78753e --- /dev/null +++ b/.paul/phases/05-finances-fakturownia-import/05-04-PLAN.md @@ -0,0 +1,196 @@ +--- +phase: 05-finances-fakturownia-import +plan: 04 +type: execute +wave: 1 +depends_on: [] +files_modified: + - autoload/Domain/Finances/FakturowniaApiClient.php + - autoload/Domain/Finances/FakturowniaInvoiceImporter.php + - tests/Domain/Finances/FakturowniaInvoiceImporterTest.php +autonomous: false +delegation: off +--- + + +## Goal +Naprawic import faktur kosztowych z Fakturowni tak, by dokumenty typu "invoice" z `income=false` (np. faktura 486639934 pod URL /invoices/486639934) byly niezawodnie pobierane, normalizowane i importowane do finansow. + +## Purpose +Zgloszony bug: faktura kosztowa 486639934 nie pojawila sie w finansach po imporcie. Phase 5 dostarczyla mechanizm importu, ale obecna logika `FakturowniaApiClient::fetchCostDocuments()` i `FakturowniaInvoiceImporter::processDocumentType()` ma trzy potencjalne dziury, przez ktore dokumenty kosztowe typu `invoices.json?income=no` moga zostac calkowicie pominiete. + +## Output +- Poprawiony `FakturowniaApiClient` zawsze odpytujacy `/invoices.json?income=no` dla kosztow (z `/costs.json` i `/expenses.json` jako uzupelnienie, a nie jako early-exit). +- Poprawiony `FakturowniaInvoiceImporter`: usuniete pressie "wczesnego przerwania" na podstawie daty z listy oraz warunkowe `period=this_month` dla kosztow. +- Test jednostkowy odtwarzajacy brak faktury 486639934 i potwierdzajacy fix. +- UAT: ponowny import zwraca faktura 486639934 do panelu mapowan lub do finansow. + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/05-finances-fakturownia-import/05-01-SUMMARY.md +@.paul/phases/05-finances-fakturownia-import/05-03-SUMMARY.md + +## Source Files +@autoload/Domain/Finances/FakturowniaApiClient.php +@autoload/Domain/Finances/FakturowniaInvoiceImporter.php +@autoload/Domain/Finances/FakturowniaImportRepository.php +@tests/Domain/Finances/FakturowniaInvoiceImporterTest.php + + + + +## AC-1: Koszty pobierane zawsze z /invoices.json?income=no +```gherkin +Given token Fakturowni jest skonfigurowany +And faktura kosztowa istnieje pod /invoices/486639934 (income=false) +When importer wywoluje fetchCostDocuments +Then zapytanie do /invoices.json?income=no jest wykonane niezaleznie od odpowiedzi /costs.json i /expenses.json +And lista zwrocona importerowi zawiera rekord 486639934 +``` + +## AC-2: Brak filtra period=this_month dla kosztow +```gherkin +Given FAKTUROWNIA_START_DATE jest pierwszym dniem biezacego miesiaca +When fetchCostDocuments buduje query +Then parametr period nie jest wysylany +And API zwraca takze faktury z wczesniejszych dni miesiaca wg issue_date +``` + +## AC-3: Importer nie przerywa paginacji na podstawie dat listy +```gherkin +Given pierwsza strona z API zawiera dokumenty posortowane malejaco po updated_at +And wszystkie dokumenty na stronie maja issue_date starsze niz startDate +When processDocumentType przetwarza strone +Then petla kontynuuje do kolejnej strony dopoki API zwraca pelna strone (per_page) +And faktura 486639934 nie jest pomijana wylacznie z powodu sortowania +``` + +## AC-4: Test regresyjny +```gherkin +Given test z fake'owym api clientem zwracajacym liste dokumentow z jednym kosztem income=false +When FakturowniaInvoiceImporter::import zostaje wywolany +Then dokument trafia do kolejki unmapped lub do finance_operations (nie jest skipped) +``` + + + + + + + Task 1: Napraw fetchCostDocuments w FakturowniaApiClient + autoload/Domain/Finances/FakturowniaApiClient.php + + Przebuduj fetchCostDocuments tak, by: + - Zawsze probowal /invoices.json?income=no (to jest primary source — URL faktury to /invoices/ID). + - Uzupelniajaco probowal /costs.json i /expenses.json i laczyl wyniki (dedup po id), aby nie gubic dokumentow obecnych pod jednym endpointem a nieobecnych pod innym. + - NIE stosowal period=this_month dla kosztow (usun canUseCurrentMonthPeriod z tej sciezki; sales moze zostac bez zmian albo tez ma byc usuniete — zdecyduj w ramach Task 1 spojnie i udokumentuj w komentarzu). + - Zachowal softFail dla endpointow pobocznych (/costs.json, /expenses.json zwracajacych 404), ale traktowal 2xx z pusta lista jako "ta sciezka pusta" i szedl dalej zamiast zwracac wynik. + - W razie gdy wszystkie trzy sciezki zwroca puste listy BEZ bledu HTTP, zwraca [] (nie rzuca wyjatku). Wyjatek tylko gdy wszystkie trzy rzucily bledy. + Unikaj: dodawania nowych zaleznosci, zmiany sygnatur publicznych metod, ukrywania bledow HTTP. + + + php -l autoload/Domain/Finances/FakturowniaApiClient.php + Manualne wywolanie z tokenem testowym: var_dump($client->fetchCostDocuments('2026-04-01', 1)) zawiera faktura 486639934. + + AC-1, AC-2 spelnione + + + + Task 2: Popraw paginacje i filtr dat w FakturowniaInvoiceImporter + autoload/Domain/Finances/FakturowniaInvoiceImporter.php + + W processDocumentType: + - Usun wczesne przerywanie petli na podstawie !hasRelevantDateInPage. API moze sortowac po updated_at lub created_at, nie po issue_date — wczesne przerwanie gubi faktury z odroznionym issue_date. + - Zachowaj twardy limit paginacji ($page > 100) jako safety-valve. + - Zostaw filtr "za stare" na poziomie processSingleDocument (tam gdzie porownuje $document['date'] z startDate). Ten filtr dziala per-dokument i jest bezpieczny. + Dodatkowo w normalizeDocument: dla dokumentow bez positions w liscie (gdy API zwraca skrocona forme), upewnij sie ze fallback do apiClient->fetchInvoiceDetails dziala i nie konsumuje zbyt wielu requestow (logika juz jest w resolvePositions — zweryfikuj ze nie regresuje przy naprawie). + Unikaj: rekurencyjnego pobierania wszystkich stron w nieskonczonosc, cichego polykania wyjatkow API. + + + php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php + php tests/run.php (istniejace testy przechodza). + + AC-3 spelnione + + + + Task 3: Test regresyjny dla faktury kosztowej typu invoice + tests/Domain/Finances/FakturowniaInvoiceImporterTest.php + + Dodaj test case "cost_invoice_from_invoices_endpoint_is_imported": + - Zamockuj FakturowniaApiClient (interface lub testowa podklasa) tak, by fetchCostDocuments zwracalo [[ + 'id' => 486639934, + 'number' => 'FV 100/04/2026', + 'kind' => 'vat', + 'income' => false, + 'issue_date' => '2026-04-05', + 'seller_name' => 'Dostawca Sp. z o.o.', + 'seller_tax_no' => '5252344567', + 'positions' => [[ 'name' => 'Uslugi IT', 'total_price_net' => 1230.00, 'quantity' => 1 ]] + ]]. + - Uruchom importer z czystymi tabelami mapowan. + - Oczekuj: summary['unmapped'] === 1 (bo brak mapowania klienta) oraz wpis w kolejce unmapped dla client_key='tax:5252344567'. + - Dodatkowy test: z pre-zalozonym mapowaniem klienta i pozycji — oczekuj summary['imported'] === 1 i rekordu w finance_operations z amount = -1230.00. + Unikaj: zaleznosci od prawdziwego API, pomijania czyszczenia bazy testowej miedzy testami. + + php tests/run.php — oba nowe testy zielone. + AC-4 spelnione + + + + Poprawiony import kosztow z Fakturowni + test regresyjny. + + 1. Uruchom import recznie: otworz /finances/main_view/ i kliknij "Importuj z Fakturowni" (lub wywolaj cron.php). + 2. Sprawdz komunikat podsumowania — powinno byc co najmniej 1 nowy rekord w kategoriach imported/unmapped. + 3. W bazie: SELECT * FROM fakturownia_import_documents WHERE external_id = '486639934' — rekord istnieje. + 4. Jesli klient niezmapowany: panel mapowan w /finances/main_view/ pokazuje faktura 486639934 w kolejce unmapped. + 5. Po zmapowaniu klienta i pozycji: ponowny import tworzy rekord w finance_operations z ujemna kwota (koszt). + + Napisz "approved" lub opisz problem do naprawy. + + + + + + +## DO NOT CHANGE +- FakturowniaImportRepository.php (schemat tabel zablokowany — zmiany tylko jesli niezbedne do poprawki bugu). +- Logika mapowania klientow po NIP (05-02) — nie modyfikuj buildClientKey / normalizeTaxNo. +- Logika filtra proforma (05-03) — isProformaDocument zostaje bez zmian. +- Publiczny kontrakt FakturowniaInvoiceImporter::import() (zwracany shape summary). + +## SCOPE LIMITS +- Nie dodajemy nowych endpointow API Fakturowni (tylko naprawiamy uzycie istniejacych). +- Nie zmieniamy UI panelu mapowan w /finances/main_view/. +- Nie wprowadzamy nowego cachingu ani retry-logic — tylko poprawa logiki pobierania/paginacji. +- Nie refaktoryzujemy importera pod testy E2E z prawdziwym API. + + + + +- [ ] php -l dla wszystkich zmienionych plikow +- [ ] php tests/run.php — wszystkie testy zielone +- [ ] Manualny import pobieral faktura 486639934 (checkpoint human-verify) +- [ ] Wszystkie AC zaspokojone + + + +- Faktura 486639934 pojawia sie w wynikach importu (imported lub unmapped, zaleznie od mapowan). +- Nowy test regresyjny przechodzi i zostaje w suite. +- Zadne istniejace testy nie regresuja. +- Brak nowych warningow php -l. + + + +Po zakonczeniu utworz `.paul/phases/05-finances-fakturownia-import/05-04-SUMMARY.md` z: +- opisem zmian w API client i importerze, +- root cause bugu, +- przyklad payloadu z API Fakturowni dla faktury 486639934, +- wynikami UAT. + diff --git a/.paul/phases/05-finances-fakturownia-import/05-04-SUMMARY.md b/.paul/phases/05-finances-fakturownia-import/05-04-SUMMARY.md new file mode 100644 index 0000000..13afbf3 --- /dev/null +++ b/.paul/phases/05-finances-fakturownia-import/05-04-SUMMARY.md @@ -0,0 +1,82 @@ +--- +phase: 05-finances-fakturownia-import +plan: 04 +status: complete +completed_at: 2026-04-14 +--- + +# Plan 05-04 Summary — Bugfix importu faktur kosztowych z Fakturowni + +## Goal (recap) +Naprawic import faktur kosztowych, ktore nie byly pobierane (zgloszony przyklad: faktura 486639934 = FV 1417/04/2026 z KSeF). + +## Root cause (2 bugi) + +**Bug A — `FakturowniaApiClient::fetchCostDocuments`:** +Kod probowal kolejno `/costs.json`, `/expenses.json`, `/invoices.json?income=no` i zwracal wynik pierwszej sciezki, ktora odpowiedziala HTTP 200. Problem: `/costs.json` na tym koncie Fakturowni zwracal HTTP 200 z PUSTA lista, wiec logika uznawala "done, zwroc puste" i nigdy nie trafiala do `/invoices.json?income=no`, gdzie lezala faktura 486639934. + +**Bug B — quirk Fakturowni dla wydatkow z KSeF:** +Dla faktur pobranych z KSeF (`gov_kind='ksef'`, `approval_status='received'`) Fakturownia zapisuje dane wlasnej firmy w polach `seller_*`, a prawdziwego dostawce w `buyer_*` — odwrotnie niz dla klasycznych faktur kosztowych z innych zrodel. Importer dla `documentType=cost` bral `seller_tax_no` jako klucz klienta → trafialo na wlasny NIP → faktura szla do `fakturownia_unmapped_queue` lub byla mapowana na wlasna firme. + +**Bonus (drugi raunde): hang crona.** +Po naprawie Bug A, ale przed dodaniem filtra dat po stronie API, usuniecie wczesnego przerywania paginacji powodowalo pobieranie calej historii Fakturowni (100 stron x 100 dok x 3 endpointy = potencjalnie 30k requestow). Cron wisial. Rozwiazanie: `period=more&date_from=startDate&date_to=today` — filtr po stronie API, paginacja konczy sie naturalnie po wyczerpaniu zakresu dat. + +## Changes + +### autoload/Domain/Finances/FakturowniaApiClient.php +- `fetchCostDocuments` przebudowane: odpytuje wszystkie 3 endpointy i merge'uje wyniki z dedup po `id`. Pusta odpowiedz z jednego endpointu NIE skraca wyszukiwania. +- `fetchSalesDocuments` + `fetchCostDocuments` uzywaja nowej metody `buildDateRangeQuery` → `period=more&date_from=X&date_to=today`. +- Usunieta dead-code metoda `canUseCurrentMonthPeriod`. +- `request()` zmieniona z `private` na `protected` — umozliwia subklasowanie w testach. + +### autoload/Domain/Finances/FakturowniaInvoiceImporter.php +- `processDocumentType`: usuniete wczesne przerywanie paginacji na podstawie dat w liscie (API moze sortowac po updated_at, nie issue_date). Zostawiony twardy cap 100 stron jako safety. +- Usunieta dead-code metoda `isDateRelevantForImport`. +- `normalizeDocument` dla `documentType='cost'`: gdy `seller_tax_no == FAKTUROWNIA_OWN_TAX_NO` (z `.env`), kontrahent jest pobierany z `buyer_*` zamiast `seller_*` (obsluga odwroconych rol KSeF). +- Nowa metoda `taxNoEqualsOwn($taxNo)` porownujaca NIP po normalizacji. +- `resolveConfig` czyta nowy klucz `FAKTUROWNIA_OWN_TAX_NO`. +- Nowe pole `$ownTaxNo` ustawiane z configa. + +### .env +- Dodane `FAKTUROWNIA_OWN_TAX_NO=5170167517`. + +### tests/Domain/Finances/FakturowniaApiClientTest.php (nowy) +- 7 asercji pokrywajacych: merge endpointow, brak short-circuit na pustym /costs.json, dedup po id, obecnosc `period=more+date_from+date_to` w query, throw gdy wszystkie 3 sciezki zwracaja HTTP 4xx/5xx, pusta liste gdy wszystkie zwracaja 200+[]. +- `FakeFakturowniaApiClient` — subklasa nadpisujaca `request()` do testow bez prawdziwego API. + +### tests/run.php +- Zarejestrowany nowy test suite. + +## Production cleanup (destrukcyjny, wymagal zgody uzytkownika) + +Wczesniejsze buggowate importy wyprodukowaly brudne dane. Po zgodzie uzytkownika: + +- `finance_operations`: usunietych 80 rekordow z Fakturowni (sum 7761.43 zl). Wszystkie referencjonowane przez `finance_operation_ids` w `fakturownia_imported_documents`. Reczne wpisy (6 szt.) zostaly nietkniete. +- `fakturownia_imported_documents`: wyczyszczone (60 rekordow). +- `fakturownia_client_mappings`: wyczyszczone (36 rekordow) — uzytkownik poprosil o reset wszystkich mapowan. +- `fakturownia_item_mappings`: wyczyszczone (60 rekordow). +- `fakturownia_unmapped_queue`: wyczyszczone (112 rekordow). +- `fakturownia_import_state.last_import_summary`: usuniete. + +## Verification + +- `php -l` na wszystkich zmienionych plikach: OK. +- `php tests/run.php`: 8 test suites, wszystkie zielone (w tym nowy `run_fakturownia_api_client_tests`). +- Diagnoza na zywym API Fakturowni potwierdzila ze faktura 486639934 jest zwracana przez `fetchCostDocuments` i ma `gov_kind='ksef'` + `approval_status='received'` + odwrocone role seller/buyer. + +## Deviations vs plan + +- Task 3 (test regresyjny) zostal zwezony: zamiast testu integracyjnego importera (ktory wymagalby mockowania DB), napisany zostal test jednostkowy dla `FakturowniaApiClient` — pokrywa faktyczny root cause bugu (merge endpointow + filtr dat po stronie API). Test fix KSeF w `normalizeDocument` nie zostal dodany, bo importer nie ma injectable dependency. Przeniesione do 05-05 jako wymagany refactor (dependency injection) przy okazji skip-list. + +- Pomiedzy T1-T2 a checkpoint pojawil sie hang crona. Drugi raund napraw: dodanie `buildDateRangeQuery` z `period=more&date_from&date_to`. Zaktualizowane testy ACto odpowiednio. + +- Cleanup bazy poza planem — wymagany poniewaz faktura 486639934 byla juz w `fakturownia_imported_documents` ze zlym mapowaniem klienta, a bez wyczyszczenia `isDocumentImported` zwracalo true i fix KSeF nie mial szansy zadzialac. + +## Follow-ups + +- **05-05 (zaplanowany):** skip-list pozycji faktury — mozliwosc oznaczenia wybranej pozycji jako pomijanej w imporcie. +- **Debt:** `FakturowniaInvoiceImporter` powinien pozwalac na wstrzykniecie `FakturowniaImportRepository` i `FakturowniaApiClient` przez konstruktor — umozliwi pelne testy integracyjne bez DB. Rozwazyc w ramach 05-05. + +## Next action + +Wdrozenie na serwer (FTP sync) i reczny reimport. Uzytkownik wykonuje samodzielnie — cleanup bazy juz zrobiony. diff --git a/.paul/phases/05-finances-fakturownia-import/05-05-PLAN.md b/.paul/phases/05-finances-fakturownia-import/05-05-PLAN.md new file mode 100644 index 0000000..08c2262 --- /dev/null +++ b/.paul/phases/05-finances-fakturownia-import/05-05-PLAN.md @@ -0,0 +1,233 @@ +--- +phase: 05-finances-fakturownia-import +plan: 05 +type: execute +wave: 2 +depends_on: ["05-04"] +files_modified: + - autoload/Domain/Finances/FakturowniaImportRepository.php + - autoload/Domain/Finances/FakturowniaInvoiceImporter.php + - autoload/controls/Finances.php + - templates/finances/main_view.php + - tests/Domain/Finances/FakturowniaApiClientTest.php +autonomous: false +delegation: off +--- + + +## Goal +Umozliwic uzytkownikowi oznaczenie wybranej pozycji faktury z Fakturowni jako "pomijanej" — taka pozycja NIE jest zapisywana do `finance_operations` i nie wplywa na statystyki finansowe, nawet jesli reszta faktury jest prawidlowo zaimportowana. + +## Purpose +Niektore pozycje faktury (np. koszty transportu przerzucane do klienta, zwrotne opakowania, pozycje techniczne) nie powinny byc ksiegowane jako oddzielne operacje finansowe. Dzisiaj kazda pozycja z mapowaniem trafia do `finance_operations` — nie ma sposobu, zeby pojedyncza pozycja byla widoczna w Fakturowni, ale nie liczona w statystykach CRM. + +## Output +- Nowa kolumna `skip` w `fakturownia_item_mappings` (lub pole `finance_category_id IS NULL` jako sygnal skip-u — decyzja w Task 1). +- Rozszerzony importer: pozycje z mapowaniem typu "skip" sa pomijane na etapie `processSingleDocument`, ale liczone w `skipped_positions` metadanych dokumentu (dla audytu). +- Rozszerzony panel `/finances/main_view/` o przycisk "Pomijaj" przy pozycji w kolejce unmapped + widok listy pozycji oznaczonych jako skip z mozliwoscia cofniecia. +- Test regresyjny: pozycja oznaczona jako skip nie tworzy `finance_operations`, ale dokument nadal jest oznaczany jako zaimportowany (jesli pozostale pozycje OK) lub jako fully-skipped (jesli wszystkie pozycje skip). + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/05-finances-fakturownia-import/05-04-PLAN.md + +## Source Files +@autoload/Domain/Finances/FakturowniaImportRepository.php +@autoload/Domain/Finances/FakturowniaInvoiceImporter.php +@autoload/controls/Finances.php +@templates/finances/main_view.php + + + + +## AC-1: Oznaczenie pozycji jako skip +```gherkin +Given faktura kosztowa z 3 pozycjami jest w kolejce unmapped (queue_type=item) +When uzytkownik klika przycisk "Pomijaj" obok pozycji w panelu /finances/main_view/ +Then rekord `fakturownia_item_mappings` zostaje utworzony ze znacznikiem skip +And pozycja znika z kolejki unmapped (jest rozwiazana) +``` + +## AC-2: Import faktury ze skip-position +```gherkin +Given istnieje `fakturownia_item_mappings` dla pozycji "Transport" ze znacznikiem skip +And faktura 500000001 ma 3 pozycje: "Produkt A" (kategoria 1), "Produkt B" (kategoria 2), "Transport" (skip) +When uruchomiony jest import z Fakturowni +Then `finance_operations` dostaje 2 rekordy (Produkt A, Produkt B), NIE 3 +And `fakturownia_imported_documents` dla id=500000001 zawiera w `meta_json` liste pominietych pozycji (item_key + name) +And summary['imported'] === 1 (dokument zaimportowany jako calosc) +``` + +## AC-3: Wszystkie pozycje skip +```gherkin +Given faktura 500000002 ma 1 pozycje "Transport" oznaczona jako skip +When uruchomiony jest import +Then faktura zostaje oznaczona jako zaimportowana (document_key wpisany do fakturownia_imported_documents) +And NIE powstaje zaden rekord w finance_operations +And summary['imported'] === 1 (zeby nie powtarzala sie co import) +``` + +## AC-4: Cofniecie skip +```gherkin +Given pozycja "Transport" jest oznaczona jako skip w fakturownia_item_mappings +When uzytkownik klika "Cofnij pomijanie" w panelu /finances/main_view/ +Then znacznik skip jest usuniety +And przy nastepnym imporcie pozycja ponownie trafia do kolejki unmapped (bo nie ma finance_category_id) +``` + +## AC-5: Idempotencja +```gherkin +Given faktura ze skip-position zostala juz raz zaimportowana +When uruchomiony jest import po raz drugi (bez zmian w danych) +Then summary['skipped'] === 1 dla tej faktury (isDocumentImported=true) +And nie powstaja duplikaty finance_operations +``` + + + + + + + Model danych dla skip-flag + Jak reprezentowac skip w bazie — nowa kolumna czy konwencja pola juz istniejacego? + + + + + Select: new-column or null-category + + + + Task 1: Rozszerzyc FakturowniaImportRepository o obsluge skip + autoload/Domain/Finances/FakturowniaImportRepository.php + + W zaleznosci od wyboru z checkpointu: + - option new-column: dodac migracja w `ensureTables()` — ALTER TABLE fakturownia_item_mappings ADD COLUMN IF NOT EXISTS skip TINYINT(1) NOT NULL DEFAULT 0. Dodac metode `markItemAsSkipped($itemKey, $itemName)` wstawiajaca rekord ze skip=1 i finance_category_id=NULL. Dodac `isItemSkipped($itemKey)` zwracajaca bool. + - option null-category: dodac metode `markItemAsSkipped($itemKey, $itemName)` wstawiajaca rekord z finance_category_id=NULL. `getItemMapping` juz zwraca rekord; trzeba dodac helper `isSkipMapping($row)` sprawdzajacy czy finance_category_id jest null. + Zmodyfikowac istniejaca metode `getItemMapping` tak, zeby kod konsumujacy mogl odroznic: a) brak mapowania (unmapped), b) skip, c) normalne mapowanie z kategoria. + Unikaj: breaking changes w publicznym kontrakcie, zmian w logice mapowan klientow. + + php -l autoload/Domain/Finances/FakturowniaImportRepository.php; php tests/run.php + AC-1 spelnione w warstwie repozytorium + + + + Task 2: Dodac obsluge skip w FakturowniaInvoiceImporter + autoload/Domain/Finances/FakturowniaInvoiceImporter.php + + W `processSingleDocument`, pomiedzy resolveItemMapping a insertem do finance_operations: + - Dla kazdej pozycji sprawdzic czy `itemMap` jest skip (nowa kolumna LUB null category, zaleznie od decyzji). + - Jesli skip: NIE dodawac pozycji do `resolvedPositions`, ale dodac do lokalnej listy `skippedPositions` (dla meta_json). + - Jesli po filtrze pozostaje 0 pozycji `resolvedPositions` ALE byly `skippedPositions` → traktowac dokument jako imported (zapisac w fakturownia_imported_documents z pusta lista operation_ids i meta_json zawierajacym skipped_positions). AC-3. + - Jesli pozostala co najmniej 1 pozycja → zapis jak dotychczas, ale meta_json dodatkowo zawiera `skipped_positions` dla audytu. + Unikaj: zmiany semantyki summary['skipped'] (oznacza "pominiete dokumenty", nie "pominiete pozycje" — dodaj osobny kontener meta). + + php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php; php tests/run.php + AC-2, AC-3, AC-5 spelnione + + + + Task 3: Przycisk "Pomijaj" i widok pominietych w panelu finances/main_view + autoload/controls/Finances.php, templates/finances/main_view.php + + W `controls\Finances`: + - Dodac akcje `fakturownia_skip_item($item_key, $item_name)` wywolujaca repo->markItemAsSkipped + oznaczajaca rekord w kolejce unmapped jako resolved. + - Dodac akcje `fakturownia_unskip_item($item_key)` usuwajaca skip-mapping (lub ustawiajaca skip=0 w wariancie new-column). + - Dodac widok `fakturownia_skipped_items_list()` zwracajacy tablice aktywnych skip-mapowan. + W `templates/finances/main_view.php`: + - W sekcji unmapped queue, przy kazdej pozycji (queue_type=item) dodac przycisk "Pomijaj w imporcie" obok istniejacego selecta kategorii. + - Dodac osobna sekcje "Pozycje pomijane" pokazujaca liste z przyciskiem "Cofnij pomijanie". + - UI w jezyku polskim, spojne z obecnym stylem Bootstrap. + Unikaj: duplikowania logiki mapowania, zmian w mapowaniach klientow (client queue pozostaje bez zmian). + + + php -l dla obu plikow. + Recznie: otworzyc /finances/main_view/, kliknac "Pomijaj" na pozycji — rekord pojawia sie w sekcji pomijanych. + + AC-1, AC-4 spelnione w warstwie UI + + + + Task 4: Test regresyjny skip-position + tests/Domain/Finances/FakturowniaApiClientTest.php (lub nowy test osobny dla importera, jesli wymaga DB-less mocka) + + Dodac test case `skip_position_is_excluded_from_finance_operations`: + - Stworzyc in-memory mock FakturowniaImportRepository (stub) implementujacy getItemMapping zwracajacy skip-mapping dla "Transport". + - Stworzyc mock FakturowniaApiClient z faktura zawierajaca 3 pozycje: Produkt A, Produkt B, Transport. + - Uruchomic FakturowniaInvoiceImporter z tymi mockami (dependency injection przez setter lub konstruktor — moze wymagac drobnego refactoru importera, zeby mozna bylo wstrzyknac repo). + - Oczekiwac: 2 pozycje w resolvedPositions (A, B), 1 w skippedPositions (Transport), summary['imported'] === 1. + Dodatkowy test: wszystkie pozycje skip → imported=1, 0 finance_operations. + Unikaj: zaleznosci od prawdziwej bazy; mockowanie tylko tego, co niezbedne. + + php tests/run.php — nowy test zielony. + AC-2, AC-3 pokryte testami + + + + Mechanizm skip-list dla pozycji faktur Fakturownia. + + 1. Otworz /finances/main_view/ — zobacz kolejke unmapped. + 2. Przy dowolnej pozycji (np. "Transport") kliknij "Pomijaj w imporcie". + 3. Pozycja znika z kolejki i pojawia sie w sekcji "Pozycje pomijane". + 4. Uruchom ponownie import. Faktura ktora miala te pozycje importuje sie BEZ niej (sprawdz `finance_operations` po `description LIKE '%Transport%'` — brak). + 5. Kliknij "Cofnij pomijanie" — pozycja wraca do kolejki unmapped przy nastepnym imporcie. + + Napisz "approved" lub opisz problem. + + + + + + +## DO NOT CHANGE +- Logika filtra proforma (05-03). +- Logika mapowania klientow po NIP (05-02). +- Fix KSeF `taxNoEqualsOwn` (05-04). +- `FakturowniaApiClient` (sciezki API) — skip jest czysta logika po stronie importera. +- Publiczny kontrakt `FakturowniaInvoiceImporter::import()` (zwracany shape summary). + +## SCOPE LIMITS +- Nie dodawac skip-list dla klientow (tylko pozycje). +- Nie dodawac masowego oznaczania (pojedyncze pozycje tylko). +- Nie ruszac eksportu/raportow finansowych — skip dziala na etapie importu, nie przy wyswietlaniu. +- Nie dodawac historii zmian skip-list (simple on/off wystarczy). + + + + +- [ ] php -l dla wszystkich zmienionych plikow +- [ ] php tests/run.php — wszystkie testy zielone +- [ ] Manualny flow: oznacz pozycje jako skip, uruchom import, sprawdz brak w finance_operations +- [ ] Cofniecie skip dziala i pozycja wraca do kolejki +- [ ] Wszystkie AC zaspokojone + + + +- Uzytkownik moze pojedynczym klikiem pominac pozycje w imporcie. +- Pominiete pozycje nie wplywaja na statystyki finansowe. +- Skip jest odwracalny. +- Audit trail: meta_json zaimportowanej faktury zawiera liste pominietych pozycji. +- Zaden istniejacy test nie regresuje. + + + +Po zakonczeniu utworz `.paul/phases/05-finances-fakturownia-import/05-05-SUMMARY.md` z: +- wybranym wariantem modelu danych (new-column vs null-category) i uzasadnieniem, +- opisem zmian w UI, +- screenshotami lub krotkim opisem flow "oznacz → import → odtworz", +- wynikami UAT. + diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index cb550de..04e9634 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -168,8 +168,8 @@ }, "FakturowniaInvoiceImporter.php": { "type": "-", - "size": 14907, - "lmtime": 1775219349455, + "size": 15552, + "lmtime": 1775895202500, "modified": false }, "FinanceRepository.php": { diff --git a/autoload/Controllers/FinancesController.php b/autoload/Controllers/FinancesController.php index 82ee9d3..629abac 100644 --- a/autoload/Controllers/FinancesController.php +++ b/autoload/Controllers/FinancesController.php @@ -414,4 +414,38 @@ class FinancesController header( 'Location: /finances/main_view/' ); exit; } + + public static function fakturowniaDocumentPositionSkip() + { + if ( !self::requireAuth() ) + return false; + + if ( !\S::csrf_verify() ) + { + \S::alert( 'Nieprawidlowy token bezpieczenstwa. Odswiez strone i sproboj ponownie.' ); + header( 'Location: /finances/main_view/' ); + exit; + } + + $externalId = trim( (string)\S::get( 'external_id' ) ); + $documentType = trim( (string)\S::get( 'document_type' ) ); + $externalItemKey = trim( (string)\S::get( 'external_item_key' ) ); + $itemName = trim( (string)\S::get( 'item_name' ) ); + + if ( $externalId === '' || $documentType === '' || $externalItemKey === '' ) + { + \S::alert( 'Nie udalo sie pominac pozycji — brakuje danych faktury.' ); + header( 'Location: /finances/main_view/' ); + exit; + } + + $importRepo = self::importRepo(); + $importRepo -> ensureTables(); + $importRepo -> markDocumentPositionSkipped( $externalId, $documentType, $externalItemKey, $itemName ); + $importRepo -> removeOccurrenceFromItemQueue( $externalItemKey, $externalId ); + + \S::alert( 'Pozycja zostala pominieta tylko dla faktury ' . $externalId . '.' ); + header( 'Location: /finances/main_view/' ); + exit; + } } diff --git a/autoload/Domain/Finances/FakturowniaApiClient.php b/autoload/Domain/Finances/FakturowniaApiClient.php index 4d4365e..3440614 100644 --- a/autoload/Domain/Finances/FakturowniaApiClient.php +++ b/autoload/Domain/Finances/FakturowniaApiClient.php @@ -18,43 +18,109 @@ class FakturowniaApiClient public function fetchSalesDocuments( $startDate, $page = 1 ) { - $query = [ - 'page' => (int)$page, - 'per_page' => $this -> pageLimit - ]; - - if ( $this -> canUseCurrentMonthPeriod( $startDate ) ) - $query['period'] = 'this_month'; + // period=more + date_from: Fakturownia filtruje po stronie API po issue_date. + // Dzieki temu nie pobieramy historycznych faktur i paginacja konczy sie naturalnie. + $query = $this -> buildDateRangeQuery( $startDate, $page ); return $this -> requestList( '/invoices.json', $query ); } public function fetchCostDocuments( $startDate, $page = 1 ) { - $queries = [ - [ - 'page' => (int)$page, - 'per_page' => $this -> pageLimit, - 'income' => 'no' - ] + // W Fakturowni "faktura kosztowa" moze byc zapisana jako: + // a) zwykla faktura z income=false (widoczna w /invoices.json?income=no, URL /invoices/ID) — primary source + // b) koszt w oddzielnym module /costs.json + // c) wydatek w /expenses.json (starsze API) + // Odpytujemy wszystkie trzy i merge'ujemy po id, zeby zadna faktura nie przepadla. + // Pusta odpowiedz HTTP 200 z ktoregokolwiek endpointu NIE konczy pobierania — + // idziemy dalej do kolejnych sciezek (to byl bug przed 05-04). + // period=more + date_from filtruje po stronie API, wiec paginacja konczy sie naturalnie. + + $baseQuery = $this -> buildDateRangeQuery( $startDate, $page ); + + $attempts = [ + [ 'path' => '/invoices.json', 'query' => array_merge( $baseQuery, [ 'income' => 'no' ] ) ], + [ 'path' => '/costs.json', 'query' => $baseQuery ], + [ 'path' => '/expenses.json', 'query' => $baseQuery ] ]; - if ( $this -> canUseCurrentMonthPeriod( $startDate ) ) - $queries[0]['period'] = 'this_month'; + $merged = []; + $seenIds = []; + $hadAnySuccess = false; + $lastError = null; - $paths = [ '/costs.json', '/expenses.json', '/invoices.json' ]; - - foreach ( $paths as $path ) + foreach ( $attempts as $attempt ) { - foreach ( $queries as $query ) + try { - $response = $this -> requestList( $path, $query, true ); - if ( $response['ok'] ) - return $response['data']; + $response = $this -> requestList( $attempt['path'], $attempt['query'], true ); + } + catch ( \Throwable $e ) + { + $lastError = $e; + continue; + } + + if ( !$response['ok'] ) + { + $lastError = new \RuntimeException( 'Blad HTTP dla ' . $attempt['path'] ); + continue; + } + + $hadAnySuccess = true; + + foreach ( $response['data'] as $document ) + { + $id = $this -> extractDocumentId( $document ); + if ( $id === '' ) + { + $merged[] = $document; + continue; + } + + if ( isset( $seenIds[ $id ] ) ) + continue; + + $seenIds[ $id ] = true; + $merged[] = $document; } } - throw new \RuntimeException( 'Nie udalo sie pobrac faktur kosztowych z API Fakturowni.' ); + if ( !$hadAnySuccess && $lastError !== null ) + throw new \RuntimeException( 'Nie udalo sie pobrac faktur kosztowych z API Fakturowni: ' . $lastError -> getMessage() ); + + return $merged; + } + + private function buildDateRangeQuery( $startDate, $page ) + { + $query = [ + 'page' => (int)$page, + 'per_page' => $this -> pageLimit + ]; + + if ( is_string( $startDate ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $startDate ) ) + { + $query['period'] = 'more'; + $query['date_from'] = $startDate; + $query['date_to'] = date( 'Y-m-d' ); + } + + return $query; + } + + private function extractDocumentId( $document ) + { + if ( !is_array( $document ) ) + return ''; + + if ( isset( $document['invoice'] ) && is_array( $document['invoice'] ) ) + $document = $document['invoice']; + + if ( isset( $document['id'] ) && (string)$document['id'] !== '' ) + return (string)$document['id']; + + return ''; } public function fetchInvoiceDetails( $invoiceId ) @@ -96,7 +162,7 @@ class FakturowniaApiClient return $softFail ? [ 'ok' => true, 'data' => $list ] : $list; } - private function request( $path, $query ) + protected function request( $path, $query ) { $query['api_token'] = $this -> apiToken; $url = $this -> baseUrl . $path . '?' . http_build_query( $query ); @@ -152,11 +218,4 @@ class FakturowniaApiClient return array_keys( $value ) === range( 0, count( $value ) - 1 ); } - private function canUseCurrentMonthPeriod( $startDate ) - { - if ( !is_string( $startDate ) || !preg_match( '/^\d{4}-\d{2}-\d{2}$/', $startDate ) ) - return false; - - return $startDate === date( 'Y-m-01' ); - } } diff --git a/autoload/Domain/Finances/FakturowniaImportRepository.php b/autoload/Domain/Finances/FakturowniaImportRepository.php index 6553f26..7332c34 100644 --- a/autoload/Domain/Finances/FakturowniaImportRepository.php +++ b/autoload/Domain/Finances/FakturowniaImportRepository.php @@ -48,6 +48,19 @@ class FakturowniaImportRepository ) ENGINE=InnoDB DEFAULT CHARSET=utf8' ); + $this -> mdb -> query( + 'CREATE TABLE IF NOT EXISTS `fakturownia_skipped_positions` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `external_id` VARCHAR(64) NOT NULL, + `document_type` VARCHAR(32) NOT NULL, + `external_item_key` VARCHAR(191) NOT NULL, + `item_name` VARCHAR(255) NOT NULL, + `created_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_doc_item` (`external_id`, `document_type`, `external_item_key`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8' + ); + $this -> mdb -> query( 'CREATE TABLE IF NOT EXISTS `fakturownia_imported_documents` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, @@ -168,6 +181,89 @@ class FakturowniaImportRepository $this -> resolveQueueItem( 'item', $externalItemKey ); } + public function markDocumentPositionSkipped( $externalId, $documentType, $externalItemKey, $itemName ) + { + $this -> ensureTables(); + + $existing = $this -> mdb -> get( 'fakturownia_skipped_positions', 'id', [ + 'AND' => [ + 'external_id' => (string)$externalId, + 'document_type' => (string)$documentType, + 'external_item_key' => (string)$externalItemKey + ] + ] ); + if ( $existing ) + return; + + $this -> mdb -> insert( 'fakturownia_skipped_positions', [ + 'external_id' => (string)$externalId, + 'document_type' => (string)$documentType, + 'external_item_key' => (string)$externalItemKey, + 'item_name' => (string)$itemName, + 'created_at' => date( 'Y-m-d H:i:s' ) + ] ); + } + + public function isDocumentPositionSkipped( $externalId, $documentType, $externalItemKey ) + { + $this -> ensureTables(); + return (bool)$this -> mdb -> has( 'fakturownia_skipped_positions', [ + 'AND' => [ + 'external_id' => (string)$externalId, + 'document_type' => (string)$documentType, + 'external_item_key' => (string)$externalItemKey + ] + ] ); + } + + public function removeOccurrenceFromItemQueue( $externalItemKey, $externalId ) + { + $this -> ensureTables(); + + $row = $this -> mdb -> get( 'fakturownia_unmapped_queue', '*', [ + 'AND' => [ + 'queue_type' => 'item', + 'external_key' => (string)$externalItemKey + ] + ] ); + if ( !$row ) + return; + + $payload = []; + if ( isset( $row['payload_json'] ) && $row['payload_json'] ) + { + $decoded = json_decode( $row['payload_json'], true ); + if ( is_array( $decoded ) ) + $payload = $decoded; + } + + $occurrences = []; + if ( isset( $payload['occurrences'] ) && is_array( $payload['occurrences'] ) ) + { + foreach ( $payload['occurrences'] as $occ ) + { + if ( !is_array( $occ ) ) + continue; + if ( (string)( $occ['document_id'] ?? '' ) === (string)$externalId ) + continue; + $occurrences[] = $occ; + } + } + + if ( empty( $occurrences ) ) + { + $this -> mdb -> delete( 'fakturownia_unmapped_queue', [ 'id' => (int)$row['id'] ] ); + return; + } + + $payload['occurrences'] = $occurrences; + $this -> mdb -> update( 'fakturownia_unmapped_queue', [ + 'payload_json' => json_encode( $payload, JSON_UNESCAPED_UNICODE ), + 'hits' => count( $occurrences ), + 'last_seen_at' => date( 'Y-m-d H:i:s' ) + ], [ 'id' => (int)$row['id'] ] ); + } + public function isDocumentImported( $externalDocumentKey ) { $this -> ensureTables(); @@ -219,6 +315,64 @@ class FakturowniaImportRepository ] ); $now = date( 'Y-m-d H:i:s' ); + + // Dla queue_type='item' agregujemy occurrences per (document_id, item_key). + // Kazde wystapienie pozycji na innej fakturze to osobny wpis w payload.occurrences, + // zeby UI mogl pokazac liste i pozwolic na pomijanie per konkretna faktura. + if ( $queueType === 'item' ) + { + $occurrence = $payload; + $payloadToStore = [ 'occurrences' => [ $occurrence ] ]; + + if ( $existing ) + { + $existingPayload = []; + if ( isset( $existing['payload_json'] ) && $existing['payload_json'] ) + { + $decoded = json_decode( $existing['payload_json'], true ); + if ( is_array( $decoded ) ) + $existingPayload = $decoded; + } + + $occurrences = ( isset( $existingPayload['occurrences'] ) && is_array( $existingPayload['occurrences'] ) ) + ? $existingPayload['occurrences'] + : []; + + // Dedup po document_id — powtorny import nadpisuje istniejace wystapienie. + $occurrences = array_values( array_filter( $occurrences, function( $occ ) use ( $occurrence ) + { + if ( !is_array( $occ ) ) + return false; + return (string)( $occ['document_id'] ?? '' ) !== (string)( $occurrence['document_id'] ?? '' ); + } ) ); + + $occurrences[] = $occurrence; + $payloadToStore = [ 'occurrences' => $occurrences ]; + + $this -> mdb -> update( 'fakturownia_unmapped_queue', [ + 'external_name' => $externalName, + 'payload_json' => json_encode( $payloadToStore, JSON_UNESCAPED_UNICODE ), + 'hits' => count( $occurrences ), + 'resolved' => 0, + 'last_seen_at' => $now + ], [ 'id' => (int)$existing['id'] ] ); + return; + } + + $this -> mdb -> insert( 'fakturownia_unmapped_queue', [ + 'queue_type' => $queueType, + 'external_key' => $externalKey, + 'external_name' => $externalName, + 'payload_json' => json_encode( $payloadToStore, JSON_UNESCAPED_UNICODE ), + 'hits' => 1, + 'resolved' => 0, + 'first_seen_at' => $now, + 'last_seen_at' => $now + ] ); + return; + } + + // Pozostale queue_type (client) — zachowanie bez zmian. $payloadJson = json_encode( $payload, JSON_UNESCAPED_UNICODE ); if ( $existing ) diff --git a/autoload/Domain/Finances/FakturowniaInvoiceImporter.php b/autoload/Domain/Finances/FakturowniaInvoiceImporter.php index 8acceee..911a71e 100644 --- a/autoload/Domain/Finances/FakturowniaInvoiceImporter.php +++ b/autoload/Domain/Finances/FakturowniaInvoiceImporter.php @@ -8,6 +8,7 @@ class FakturowniaInvoiceImporter private $apiClient; private $startDate; private $pageLimit = 100; + private $ownTaxNo = ''; public function __construct( $mdb = null ) { @@ -37,6 +38,7 @@ class FakturowniaInvoiceImporter $config['page_limit'] ); $this -> pageLimit = (int)$config['page_limit']; + $this -> ownTaxNo = (string)$config['own_tax_no']; $summary = [ 'imported' => 0, @@ -85,13 +87,8 @@ class FakturowniaInvoiceImporter if ( !is_array( $documents ) || empty( $documents ) ) break; - $hasRelevantDateInPage = false; - foreach ( $documents as $document ) { - if ( $this -> isDateRelevantForImport( $document ) ) - $hasRelevantDateInPage = true; - $result = $this -> processSingleDocument( $document, $documentType ); $summary[ $result ]++; } @@ -99,11 +96,9 @@ class FakturowniaInvoiceImporter if ( count( $documents ) < $this -> pageLimit ) break; - // API zwraca dokumenty malejaco po czasie. Gdy cala strona jest starsza niz startDate, - // kolejne strony tez beda starsze i nie ma sensu pobierac dalej. - if ( !$hasRelevantDateInPage ) - break; - + // Nie przerywamy petli na podstawie dat w liscie — API moze sortowac po updated_at/created_at + // zamiast issue_date, co powodowalo gubienie faktur z wcześniejsza data wystawienia. + // Filtr "za stare" dziala per-dokument w processSingleDocument (strtotime(date) < startDate). $page++; if ( $page > 100 ) break; @@ -139,18 +134,39 @@ class FakturowniaInvoiceImporter if ( !$clientMap ) { + $positionNames = []; + foreach ( $document['positions'] as $pos ) + { + $name = trim( (string)( $pos['name'] ?? '' ) ); + if ( $name !== '' ) + $positionNames[] = $name; + } + $this -> repo -> queueUnmapped( 'client', $document['client_key'], $document['client_name'], [ 'document_id' => $document['external_id'], 'document_number' => $document['number'], 'document_type' => $documentType, - 'tax_no' => $document['client_tax_no'] + 'tax_no' => $document['client_tax_no'], + 'positions' => $positionNames ] ); return 'unmapped'; } $resolvedPositions = []; + $skippedPositions = []; foreach ( $document['positions'] as $position ) { + // Per-dokument skip: uzytkownik oznaczyl konkretna pozycje na konkretnej fakturze jako pomijana. + // Pomijanie dzieje sie ZANIM sprawdzamy mapowanie, zeby pominieta pozycja nie blokowala importu. + if ( $this -> repo -> isDocumentPositionSkipped( $document['external_id'], $documentType, $position['item_key'] ) ) + { + $skippedPositions[] = [ + 'item_key' => $position['item_key'], + 'name' => $position['name'] + ]; + continue; + } + $itemMap = $this -> repo -> getItemMapping( $position['item_key'] ); if ( !$itemMap ) { @@ -159,7 +175,8 @@ class FakturowniaInvoiceImporter 'document_number' => $document['number'], 'document_type' => $documentType, 'buyer_name' => $document['buyer_name'], - 'seller_name' => $document['seller_name'] + 'seller_name' => $document['seller_name'], + 'position_name' => $position['name'] ] ); return 'unmapped'; } @@ -175,7 +192,9 @@ class FakturowniaInvoiceImporter ]; } - if ( empty( $resolvedPositions ) ) + // Dokument bez zadnych pozycji do przetworzenia (ani resolved, ani skip) → nic nie zapisujemy. + // Nie oznaczamy jako imported, zeby nie zablokowal sie na zawsze. + if ( empty( $resolvedPositions ) && empty( $skippedPositions ) ) return 'skipped'; $operationIds = []; @@ -206,7 +225,8 @@ class FakturowniaInvoiceImporter $operationIds, [ 'number' => $document['number'], - 'client_name' => $document['client_name'] + 'client_name' => $document['client_name'], + 'skipped_positions' => $skippedPositions ] ); @@ -261,22 +281,6 @@ class FakturowniaInvoiceImporter return false; } - private function isDateRelevantForImport( $rawDocument ) - { - if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) ) - $rawDocument = $rawDocument['invoice']; - - if ( !is_array( $rawDocument ) ) - return false; - - $date = (string)( $rawDocument['issue_date'] ?? $rawDocument['sell_date'] ?? $rawDocument['created_at'] ?? '' ); - $date = substr( $date, 0, 10 ); - if ( !$date ) - return false; - - return strtotime( $date ) >= strtotime( $this -> startDate ); - } - private function normalizeDocument( $rawDocument, $documentType ) { if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) ) @@ -300,8 +304,22 @@ class FakturowniaInvoiceImporter } else { - $clientName = (string)( $rawDocument['seller_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' ); - $clientTaxNo = (string)( $rawDocument['seller_tax_no'] ?? $rawDocument['tax_no'] ?? '' ); + // Dla kosztow kontrahentem jest "druga strona" — ta, ktorej NIP != naszego. + // Fakturownia dla wydatkow pobranych z KSeF (approval_status='received') odwraca role: + // seller_* to dane wlasnej firmy, buyer_* to prawdziwy dostawca. Bez tej heurystyki + // koszty z KSeF trafialyby do kolejki unmapped z wlasnym NIP-em jako klucz klienta. + $sellerTaxNo = (string)( $rawDocument['seller_tax_no'] ?? '' ); + + if ( $this -> taxNoEqualsOwn( $sellerTaxNo ) ) + { + $clientName = (string)( $rawDocument['buyer_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' ); + $clientTaxNo = (string)( $rawDocument['buyer_tax_no'] ?? $rawDocument['tax_no'] ?? '' ); + } + else + { + $clientName = (string)( $rawDocument['seller_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' ); + $clientTaxNo = $sellerTaxNo !== '' ? $sellerTaxNo : (string)( $rawDocument['tax_no'] ?? '' ); + } } $positions = []; @@ -436,6 +454,18 @@ class FakturowniaInvoiceImporter return $normalizedTaxNo; } + private function taxNoEqualsOwn( $taxNo ) + { + if ( $this -> ownTaxNo === '' ) + return false; + + $normalized = $this -> normalizeTaxNo( $taxNo ); + if ( $normalized === '' ) + return false; + + return $normalized === $this -> normalizeTaxNo( $this -> ownTaxNo ); + } + private function buildItemKey( $rawPosition, $name ) { if ( isset( $rawPosition['product_id'] ) && (string)$rawPosition['product_id'] !== '' ) @@ -483,6 +513,7 @@ class FakturowniaInvoiceImporter $token = trim( (string)\Env::get( 'FAKTUROWNIA_API_TOKEN', '' ) ); $startDate = trim( (string)\Env::get( 'FAKTUROWNIA_START_DATE', '' ) ); $pageLimit = (int)\Env::get( 'FAKTUROWNIA_PAGE_LIMIT', 100 ); + $ownTaxNo = trim( (string)\Env::get( 'FAKTUROWNIA_OWN_TAX_NO', '' ) ); if ( $domain === '' || $token === '' || $startDate === '' ) return [ 'status' => 'error', 'ok' => false, 'msg' => 'Import Fakturownia: brak konfiguracji w .env.' ]; @@ -498,7 +529,8 @@ class FakturowniaInvoiceImporter 'api_url' => rtrim( $domain, '/' ), 'token' => $token, 'start_date' => $startDate, - 'page_limit' => $pageLimit > 0 ? $pageLimit : 100 + 'page_limit' => $pageLimit > 0 ? $pageLimit : 100, + 'own_tax_no' => $ownTaxNo ]; } diff --git a/templates/finances/fakturownia-import-panel.php b/templates/finances/fakturownia-import-panel.php index 8ae772b..31d5289 100644 --- a/templates/finances/fakturownia-import-panel.php +++ b/templates/finances/fakturownia-import-panel.php @@ -60,6 +60,24 @@ $esc = function( $value ) NIP:
+ + Ostatnia faktura:
+ + + + + + Pozycje z faktury: + + + Klucz: | wystapienia: @@ -90,47 +108,74 @@ $esc = function( $value ) Pozycja z faktury - Kategoria finansowa - Akcja + Kategoria finansowa (mapuje globalnie) + Akcja - fakturownia_pending_items as $row ): ?> - - -
- - - Faktura:
+ + fakturownia_pending_items as $itemIdx => $row ): ?> + + + + + + $occ ): ?> + + + +
+ + Faktura: (ID )
+ + + + Nabywca: | + Sprzedawca: +
+ + + Klucz: | wystapien w kolejce: + + + + + +
+ + + + +
+ 1 ): ?> +
Zapis mapowania kategorii rozwiazuje wszystkie wystapien tej pozycji. + + - - - Nabywca: | - Sprzedawca: -
- - Klucz: | wystapienia: - - -
- - - - - - - -
- - + +
+ + + + + + +
+ + + + + + (brak wystapien do rozwiazania) + diff --git a/tests/Domain/Finances/FakturowniaApiClientTest.php b/tests/Domain/Finances/FakturowniaApiClientTest.php new file mode 100644 index 0000000..9050d4a --- /dev/null +++ b/tests/Domain/Finances/FakturowniaApiClientTest.php @@ -0,0 +1,168 @@ + calledPaths[] = [ 'path' => $path, 'query' => $query ]; + + if ( !isset( $this -> responses[ $path ] ) ) + return [ 'http_code' => 200, 'body' => '[]' ]; + + return $this -> responses[ $path ]; + } +} + +function run_fakturownia_api_client_tests() +{ + // AC-1: /invoices.json?income=no jest pytane jako primary source, + // pusta odpowiedz /costs.json NIE konczy wczesniej i faktura 486639934 z /invoices.json trafia do wynikow. + $client = new FakeFakturowniaApiClient( 'https://example', 'token', 100 ); + $client -> responses = [ + '/invoices.json' => [ + 'http_code' => 200, + 'body' => json_encode( [ + 'invoices' => [ + [ + 'id' => 486639934, + 'number' => 'FV 100/04/2026', + 'kind' => 'vat', + 'income' => false, + 'issue_date' => '2026-04-05', + 'seller_name' => 'Dostawca Sp. z o.o.', + 'seller_tax_no' => '5252344567' + ] + ] + ] ) + ], + '/costs.json' => [ 'http_code' => 200, 'body' => '[]' ], + '/expenses.json' => [ 'http_code' => 404, 'body' => '' ] + ]; + + $result = $client -> fetchCostDocuments( '2026-04-01', 1 ); + + assert_true( + count( $result ) === 1, + 'Expected faktura 486639934 to be returned from /invoices.json?income=no even when /costs.json is empty.' + ); + assert_true( + (string)$result[0]['id'] === '486639934', + 'Expected invoice id 486639934 in merged cost documents.' + ); + + $paths = array_map( function( $c ) { return $c['path']; }, $client -> calledPaths ); + assert_true( + in_array( '/invoices.json', $paths, true ), + 'Expected /invoices.json to be queried for cost documents.' + ); + assert_true( + in_array( '/costs.json', $paths, true ), + 'Expected /costs.json to be queried as secondary source.' + ); + assert_true( + in_array( '/expenses.json', $paths, true ), + 'Expected /expenses.json to be queried as tertiary source (bug 05-04: empty /costs.json must not short-circuit).' + ); + + // AC-2: period=more + date_from + date_to sa wysylane, by API filtrowalo po issue_date. + // Bez tego importer pobieralby wszystkie historyczne faktury i cron by sie zawiesil. + $invoicesCall = null; + foreach ( $client -> calledPaths as $call ) + { + if ( $call['path'] === '/invoices.json' ) + { + $invoicesCall = $call; + break; + } + } + assert_true( $invoicesCall !== null, 'Expected at least one call to /invoices.json.' ); + assert_true( + ( $invoicesCall['query']['period'] ?? '' ) === 'more', + 'Expected period=more parameter so Fakturownia filters by date range server-side.' + ); + assert_true( + ( $invoicesCall['query']['date_from'] ?? '' ) === '2026-04-01', + 'Expected date_from to match startDate passed to fetchCostDocuments.' + ); + assert_true( + !empty( $invoicesCall['query']['date_to'] ?? '' ), + 'Expected date_to to be set (default: today) to complete the range filter.' + ); + assert_true( + ( $invoicesCall['query']['income'] ?? '' ) === 'no', + 'Expected income=no filter on /invoices.json for cost documents.' + ); + + // Dedup: ten sam id w /invoices.json i /costs.json trafia raz. + $dedupClient = new FakeFakturowniaApiClient( 'https://example', 'token', 100 ); + $dedupClient -> responses = [ + '/invoices.json' => [ + 'http_code' => 200, + 'body' => json_encode( [ + 'invoices' => [ + [ 'id' => 111, 'number' => 'A', 'income' => false ], + [ 'id' => 222, 'number' => 'B', 'income' => false ] + ] + ] ) + ], + '/costs.json' => [ + 'http_code' => 200, + 'body' => json_encode( [ + 'costs' => [ + [ 'id' => 222, 'number' => 'B' ], + [ 'id' => 333, 'number' => 'C' ] + ] + ] ) + ] + ]; + + $merged = $dedupClient -> fetchCostDocuments( '2026-04-01', 1 ); + $ids = array_map( function( $d ) { return (string)$d['id']; }, $merged ); + sort( $ids ); + assert_true( + $ids === [ '111', '222', '333' ], + 'Expected merged cost documents to be deduplicated by id across endpoints.' + ); + + // Gdy wszystkie 3 sciezki zwroca HTTP >= 400, fetchCostDocuments rzuca wyjatek. + $errorClient = new FakeFakturowniaApiClient( 'https://example', 'token', 100 ); + $errorClient -> responses = [ + '/invoices.json' => [ 'http_code' => 500, 'body' => '' ], + '/costs.json' => [ 'http_code' => 500, 'body' => '' ], + '/expenses.json' => [ 'http_code' => 500, 'body' => '' ] + ]; + + $threw = false; + try + { + $errorClient -> fetchCostDocuments( '2026-04-01', 1 ); + } + catch ( \Throwable $e ) + { + $threw = true; + } + assert_true( + $threw, + 'Expected fetchCostDocuments to throw when all endpoints fail with HTTP errors.' + ); + + // Gdy wszystkie 3 sciezki zwroca pusta liste z HTTP 200, zwracamy [] (nie wyjatek). + $emptyClient = new FakeFakturowniaApiClient( 'https://example', 'token', 100 ); + $emptyClient -> responses = [ + '/invoices.json' => [ 'http_code' => 200, 'body' => '[]' ], + '/costs.json' => [ 'http_code' => 200, 'body' => '[]' ], + '/expenses.json' => [ 'http_code' => 200, 'body' => '[]' ] + ]; + $emptyResult = $emptyClient -> fetchCostDocuments( '2026-04-01', 1 ); + assert_true( + is_array( $emptyResult ) && count( $emptyResult ) === 0, + 'Expected empty array when all endpoints return HTTP 200 with empty lists.' + ); +} diff --git a/tests/run.php b/tests/run.php index 2a08fa6..c07f9ef 100644 --- a/tests/run.php +++ b/tests/run.php @@ -5,6 +5,7 @@ require_once __DIR__ . '/Domain/Tasks/TaskAttachmentRepositoryTest.php'; require_once __DIR__ . '/Domain/Tasks/MailToTaskImporterTest.php'; require_once __DIR__ . '/Domain/Users/UserRepositoryTest.php'; require_once __DIR__ . '/Domain/Users/PermissionRepositoryTest.php'; +require_once __DIR__ . '/Domain/Finances/FakturowniaApiClientTest.php'; require_once __DIR__ . '/Controllers/TasksControllerTest.php'; require_once __DIR__ . '/Controllers/UsersControllerTest.php'; @@ -14,6 +15,7 @@ $tests = [ 'run_mail_to_task_importer_tests', 'run_user_repository_tests', 'run_permission_repository_tests', + 'run_fakturownia_api_client_tests', 'run_tasks_controller_tests', 'run_users_controller_tests' ];