update
This commit is contained in:
@@ -16,7 +16,7 @@ Phases: 3 of 5 complete
|
|||||||
| 2 | Naprawa bledow krytycznych | TBD | Next | - |
|
| 2 | Naprawa bledow krytycznych | TBD | Next | - |
|
||||||
| 3 | Naprawa bledow glownych | TBD | Not started | - |
|
| 3 | Naprawa bledow glownych | TBD | Not started | - |
|
||||||
| 4 | Poprawa pokrycia testami | 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
|
## Phase Details
|
||||||
|
|
||||||
@@ -92,6 +92,8 @@ Phases: 3 of 5 complete
|
|||||||
- [x] 05-01: Integracja Fakturownia i automatyczny import do finansow
|
- [x] 05-01: Integracja Fakturownia i automatyczny import do finansow
|
||||||
- [x] 05-02: Mapowanie klienta po NIP + kompatybilnosc mapowan historycznych
|
- [x] 05-02: Mapowanie klienta po NIP + kompatybilnosc mapowan historycznych
|
||||||
- [x] 05-03: Filtr proforma — pomijanie faktur proforma (FP*) w imporcie
|
- [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*
|
*Roadmap created: 2026-03-15*
|
||||||
|
|||||||
@@ -10,29 +10,32 @@ See: .paul/PROJECT.md (updated 2026-04-11)
|
|||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v0.1 Stabilizacja i jakosc kodu
|
Milestone: v0.1 Stabilizacja i jakosc kodu
|
||||||
Phase: 5 of 5 (Import finansow z Fakturowni) - Complete
|
Phase: 5 of 5 (Import finansow z Fakturowni) — 05-04 zamkniete, 05-05 w APPLY
|
||||||
Plan: 05-03 unified
|
Plan: 05-05 (skip-list pozycji) — czeka na checkpoint decyzyjny
|
||||||
Status: Loop complete, Phase 5 complete. Next: Phase 2
|
Status: APPLY started, blocking on checkpoint:decision (new-column vs null-category)
|
||||||
Last activity: 2026-04-11 — Phase 5 transition complete
|
Last activity: 2026-04-14 — 05-04 UNIFY complete, 05-05 APPLY started
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone: [######----] 60%
|
- Milestone: [#######---] 70%
|
||||||
- Phase 5: [##########] 100%
|
- Phase 5: [########--] 80% (05-04 done, 05-05 in apply)
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN ──▶ APPLY ──▶ UNIFY
|
Plan 05-04: PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ✓ ✓ [Loop complete - ready for next PLAN]
|
✓ ✓ ✓ [Complete]
|
||||||
|
|
||||||
|
Plan 05-05: PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
|
✓ ◐ ○ [APPLY in progress, checkpoint pending]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-04-11
|
Last session: 2026-04-14
|
||||||
Stopped at: Phase 5 complete, ready to plan Phase 2
|
Stopped at: 05-05 checkpoint decyzyjny (model danych dla skip-flag)
|
||||||
Next action: /paul:plan for Phase 2 (Naprawa bledow krytycznych)
|
Next action: Uzytkownik wybiera: new-column (skip TINYINT w item_mappings) lub null-category (finance_category_id IS NULL)
|
||||||
Resume file: .paul/ROADMAP.md
|
Resume file: .paul/phases/05-finances-fakturownia-import/05-05-PLAN.md
|
||||||
|
|
||||||
---
|
---
|
||||||
*STATE.md - Updated after every significant action*
|
*STATE.md - Updated after every significant action*
|
||||||
|
|||||||
48
.paul/governance/governance_2026-04-14.jsonl
Normal file
48
.paul/governance/governance_2026-04-14.jsonl
Normal file
@@ -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"}
|
||||||
33
.paul/governance/governance_2026-04-15.jsonl
Normal file
33
.paul/governance/governance_2026-04-15.jsonl
Normal file
@@ -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"}
|
||||||
196
.paul/phases/05-finances-fakturownia-import/05-04-PLAN.md
Normal file
196
.paul/phases/05-finances-fakturownia-import/05-04-PLAN.md
Normal file
@@ -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
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## 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.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## 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
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Napraw fetchCostDocuments w FakturowniaApiClient</name>
|
||||||
|
<files>autoload/Domain/Finances/FakturowniaApiClient.php</files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
php -l autoload/Domain/Finances/FakturowniaApiClient.php
|
||||||
|
Manualne wywolanie z tokenem testowym: var_dump($client->fetchCostDocuments('2026-04-01', 1)) zawiera faktura 486639934.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1, AC-2 spelnione</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Popraw paginacje i filtr dat w FakturowniaInvoiceImporter</name>
|
||||||
|
<files>autoload/Domain/Finances/FakturowniaInvoiceImporter.php</files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php
|
||||||
|
php tests/run.php (istniejace testy przechodza).
|
||||||
|
</verify>
|
||||||
|
<done>AC-3 spelnione</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Test regresyjny dla faktury kosztowej typu invoice</name>
|
||||||
|
<files>tests/Domain/Finances/FakturowniaInvoiceImporterTest.php</files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>php tests/run.php — oba nowe testy zielone.</verify>
|
||||||
|
<done>AC-4 spelnione</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<what-built>Poprawiony import kosztow z Fakturowni + test regresyjny.</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
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).
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Napisz "approved" lub opisz problem do naprawy.</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- [ ] php -l dla wszystkich zmienionych plikow
|
||||||
|
- [ ] php tests/run.php — wszystkie testy zielone
|
||||||
|
- [ ] Manualny import pobieral faktura 486639934 (checkpoint human-verify)
|
||||||
|
- [ ] Wszystkie AC zaspokojone
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
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.
|
||||||
|
</output>
|
||||||
82
.paul/phases/05-finances-fakturownia-import/05-04-SUMMARY.md
Normal file
82
.paul/phases/05-finances-fakturownia-import/05-04-SUMMARY.md
Normal file
@@ -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.
|
||||||
233
.paul/phases/05-finances-fakturownia-import/05-05-PLAN.md
Normal file
233
.paul/phases/05-finances-fakturownia-import/05-05-PLAN.md
Normal file
@@ -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
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## 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).
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## 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
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="checkpoint:decision" gate="blocking">
|
||||||
|
<decision>Model danych dla skip-flag</decision>
|
||||||
|
<context>Jak reprezentowac skip w bazie — nowa kolumna czy konwencja pola juz istniejacego?</context>
|
||||||
|
<options>
|
||||||
|
<option id="new-column">
|
||||||
|
<name>Dodac kolumne `skip TINYINT(1) NOT NULL DEFAULT 0` do fakturownia_item_mappings</name>
|
||||||
|
<pros>Jawne, latwe do query, latwe do indeksowania, mozna miec `finance_category_id` + `skip=1` rownoczesnie (np. "kategoria domyslna ale aktualnie pomijana")</pros>
|
||||||
|
<cons>Wymaga migracji schematu, dodaje kolumne ktora w 99% przypadkow bedzie 0</cons>
|
||||||
|
</option>
|
||||||
|
<option id="null-category">
|
||||||
|
<name>Uzyc `finance_category_id IS NULL` jako sygnalu skip (bez migracji schematu)</name>
|
||||||
|
<pros>Zero migracji, semantyka "mapowanie istnieje ale bez kategorii = skip" jest naturalna, mniejsze ryzyko</pros>
|
||||||
|
<cons>Mniej jawne, trudniej odroznic "skip" od "mapowanie niedokonczone"; kod musi sprawdzac NULL specjalnie</cons>
|
||||||
|
</option>
|
||||||
|
</options>
|
||||||
|
<resume-signal>Select: new-column or null-category</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Rozszerzyc FakturowniaImportRepository o obsluge skip</name>
|
||||||
|
<files>autoload/Domain/Finances/FakturowniaImportRepository.php</files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>php -l autoload/Domain/Finances/FakturowniaImportRepository.php; php tests/run.php</verify>
|
||||||
|
<done>AC-1 spelnione w warstwie repozytorium</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Dodac obsluge skip w FakturowniaInvoiceImporter</name>
|
||||||
|
<files>autoload/Domain/Finances/FakturowniaInvoiceImporter.php</files>
|
||||||
|
<action>
|
||||||
|
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).
|
||||||
|
</action>
|
||||||
|
<verify>php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php; php tests/run.php</verify>
|
||||||
|
<done>AC-2, AC-3, AC-5 spelnione</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Przycisk "Pomijaj" i widok pominietych w panelu finances/main_view</name>
|
||||||
|
<files>autoload/controls/Finances.php, templates/finances/main_view.php</files>
|
||||||
|
<action>
|
||||||
|
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).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
php -l dla obu plikow.
|
||||||
|
Recznie: otworzyc /finances/main_view/, kliknac "Pomijaj" na pozycji — rekord pojawia sie w sekcji pomijanych.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1, AC-4 spelnione w warstwie UI</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 4: Test regresyjny skip-position</name>
|
||||||
|
<files>tests/Domain/Finances/FakturowniaApiClientTest.php (lub nowy test osobny dla importera, jesli wymaga DB-less mocka)</files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>php tests/run.php — nowy test zielony.</verify>
|
||||||
|
<done>AC-2, AC-3 pokryte testami</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<what-built>Mechanizm skip-list dla pozycji faktur Fakturownia.</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
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.
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Napisz "approved" lub opisz problem.</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- [ ] 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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
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.
|
||||||
|
</output>
|
||||||
4
.vscode/ftp-kr.sync.cache.json
vendored
4
.vscode/ftp-kr.sync.cache.json
vendored
@@ -168,8 +168,8 @@
|
|||||||
},
|
},
|
||||||
"FakturowniaInvoiceImporter.php": {
|
"FakturowniaInvoiceImporter.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 14907,
|
"size": 15552,
|
||||||
"lmtime": 1775219349455,
|
"lmtime": 1775895202500,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"FinanceRepository.php": {
|
"FinanceRepository.php": {
|
||||||
|
|||||||
@@ -414,4 +414,38 @@ class FinancesController
|
|||||||
header( 'Location: /finances/main_view/' );
|
header( 'Location: /finances/main_view/' );
|
||||||
exit;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,43 +18,109 @@ class FakturowniaApiClient
|
|||||||
|
|
||||||
public function fetchSalesDocuments( $startDate, $page = 1 )
|
public function fetchSalesDocuments( $startDate, $page = 1 )
|
||||||
{
|
{
|
||||||
$query = [
|
// period=more + date_from: Fakturownia filtruje po stronie API po issue_date.
|
||||||
'page' => (int)$page,
|
// Dzieki temu nie pobieramy historycznych faktur i paginacja konczy sie naturalnie.
|
||||||
'per_page' => $this -> pageLimit
|
$query = $this -> buildDateRangeQuery( $startDate, $page );
|
||||||
];
|
|
||||||
|
|
||||||
if ( $this -> canUseCurrentMonthPeriod( $startDate ) )
|
|
||||||
$query['period'] = 'this_month';
|
|
||||||
|
|
||||||
return $this -> requestList( '/invoices.json', $query );
|
return $this -> requestList( '/invoices.json', $query );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fetchCostDocuments( $startDate, $page = 1 )
|
public function fetchCostDocuments( $startDate, $page = 1 )
|
||||||
{
|
{
|
||||||
$queries = [
|
// 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
|
||||||
'page' => (int)$page,
|
// b) koszt w oddzielnym module /costs.json
|
||||||
'per_page' => $this -> pageLimit,
|
// c) wydatek w /expenses.json (starsze API)
|
||||||
'income' => 'no'
|
// 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 ) )
|
$merged = [];
|
||||||
$queries[0]['period'] = 'this_month';
|
$seenIds = [];
|
||||||
|
$hadAnySuccess = false;
|
||||||
|
$lastError = null;
|
||||||
|
|
||||||
$paths = [ '/costs.json', '/expenses.json', '/invoices.json' ];
|
foreach ( $attempts as $attempt )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$response = $this -> requestList( $attempt['path'], $attempt['query'], true );
|
||||||
|
}
|
||||||
|
catch ( \Throwable $e )
|
||||||
|
{
|
||||||
|
$lastError = $e;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ( $paths as $path )
|
if ( !$response['ok'] )
|
||||||
{
|
{
|
||||||
foreach ( $queries as $query )
|
$lastError = new \RuntimeException( 'Blad HTTP dla ' . $attempt['path'] );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hadAnySuccess = true;
|
||||||
|
|
||||||
|
foreach ( $response['data'] as $document )
|
||||||
{
|
{
|
||||||
$response = $this -> requestList( $path, $query, true );
|
$id = $this -> extractDocumentId( $document );
|
||||||
if ( $response['ok'] )
|
if ( $id === '' )
|
||||||
return $response['data'];
|
{
|
||||||
|
$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 )
|
public function fetchInvoiceDetails( $invoiceId )
|
||||||
@@ -96,7 +162,7 @@ class FakturowniaApiClient
|
|||||||
return $softFail ? [ 'ok' => true, 'data' => $list ] : $list;
|
return $softFail ? [ 'ok' => true, 'data' => $list ] : $list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function request( $path, $query )
|
protected function request( $path, $query )
|
||||||
{
|
{
|
||||||
$query['api_token'] = $this -> apiToken;
|
$query['api_token'] = $this -> apiToken;
|
||||||
$url = $this -> baseUrl . $path . '?' . http_build_query( $query );
|
$url = $this -> baseUrl . $path . '?' . http_build_query( $query );
|
||||||
@@ -152,11 +218,4 @@ class FakturowniaApiClient
|
|||||||
return array_keys( $value ) === range( 0, count( $value ) - 1 );
|
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' );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,19 @@ class FakturowniaImportRepository
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
) 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(
|
$this -> mdb -> query(
|
||||||
'CREATE TABLE IF NOT EXISTS `fakturownia_imported_documents` (
|
'CREATE TABLE IF NOT EXISTS `fakturownia_imported_documents` (
|
||||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
@@ -168,6 +181,89 @@ class FakturowniaImportRepository
|
|||||||
$this -> resolveQueueItem( 'item', $externalItemKey );
|
$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 )
|
public function isDocumentImported( $externalDocumentKey )
|
||||||
{
|
{
|
||||||
$this -> ensureTables();
|
$this -> ensureTables();
|
||||||
@@ -219,6 +315,64 @@ class FakturowniaImportRepository
|
|||||||
] );
|
] );
|
||||||
|
|
||||||
$now = date( 'Y-m-d H:i:s' );
|
$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 );
|
$payloadJson = json_encode( $payload, JSON_UNESCAPED_UNICODE );
|
||||||
|
|
||||||
if ( $existing )
|
if ( $existing )
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class FakturowniaInvoiceImporter
|
|||||||
private $apiClient;
|
private $apiClient;
|
||||||
private $startDate;
|
private $startDate;
|
||||||
private $pageLimit = 100;
|
private $pageLimit = 100;
|
||||||
|
private $ownTaxNo = '';
|
||||||
|
|
||||||
public function __construct( $mdb = null )
|
public function __construct( $mdb = null )
|
||||||
{
|
{
|
||||||
@@ -37,6 +38,7 @@ class FakturowniaInvoiceImporter
|
|||||||
$config['page_limit']
|
$config['page_limit']
|
||||||
);
|
);
|
||||||
$this -> pageLimit = (int)$config['page_limit'];
|
$this -> pageLimit = (int)$config['page_limit'];
|
||||||
|
$this -> ownTaxNo = (string)$config['own_tax_no'];
|
||||||
|
|
||||||
$summary = [
|
$summary = [
|
||||||
'imported' => 0,
|
'imported' => 0,
|
||||||
@@ -85,13 +87,8 @@ class FakturowniaInvoiceImporter
|
|||||||
if ( !is_array( $documents ) || empty( $documents ) )
|
if ( !is_array( $documents ) || empty( $documents ) )
|
||||||
break;
|
break;
|
||||||
|
|
||||||
$hasRelevantDateInPage = false;
|
|
||||||
|
|
||||||
foreach ( $documents as $document )
|
foreach ( $documents as $document )
|
||||||
{
|
{
|
||||||
if ( $this -> isDateRelevantForImport( $document ) )
|
|
||||||
$hasRelevantDateInPage = true;
|
|
||||||
|
|
||||||
$result = $this -> processSingleDocument( $document, $documentType );
|
$result = $this -> processSingleDocument( $document, $documentType );
|
||||||
$summary[ $result ]++;
|
$summary[ $result ]++;
|
||||||
}
|
}
|
||||||
@@ -99,11 +96,9 @@ class FakturowniaInvoiceImporter
|
|||||||
if ( count( $documents ) < $this -> pageLimit )
|
if ( count( $documents ) < $this -> pageLimit )
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// API zwraca dokumenty malejaco po czasie. Gdy cala strona jest starsza niz startDate,
|
// Nie przerywamy petli na podstawie dat w liscie — API moze sortowac po updated_at/created_at
|
||||||
// kolejne strony tez beda starsze i nie ma sensu pobierac dalej.
|
// zamiast issue_date, co powodowalo gubienie faktur z wcześniejsza data wystawienia.
|
||||||
if ( !$hasRelevantDateInPage )
|
// Filtr "za stare" dziala per-dokument w processSingleDocument (strtotime(date) < startDate).
|
||||||
break;
|
|
||||||
|
|
||||||
$page++;
|
$page++;
|
||||||
if ( $page > 100 )
|
if ( $page > 100 )
|
||||||
break;
|
break;
|
||||||
@@ -139,18 +134,39 @@ class FakturowniaInvoiceImporter
|
|||||||
|
|
||||||
if ( !$clientMap )
|
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'], [
|
$this -> repo -> queueUnmapped( 'client', $document['client_key'], $document['client_name'], [
|
||||||
'document_id' => $document['external_id'],
|
'document_id' => $document['external_id'],
|
||||||
'document_number' => $document['number'],
|
'document_number' => $document['number'],
|
||||||
'document_type' => $documentType,
|
'document_type' => $documentType,
|
||||||
'tax_no' => $document['client_tax_no']
|
'tax_no' => $document['client_tax_no'],
|
||||||
|
'positions' => $positionNames
|
||||||
] );
|
] );
|
||||||
return 'unmapped';
|
return 'unmapped';
|
||||||
}
|
}
|
||||||
|
|
||||||
$resolvedPositions = [];
|
$resolvedPositions = [];
|
||||||
|
$skippedPositions = [];
|
||||||
foreach ( $document['positions'] as $position )
|
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'] );
|
$itemMap = $this -> repo -> getItemMapping( $position['item_key'] );
|
||||||
if ( !$itemMap )
|
if ( !$itemMap )
|
||||||
{
|
{
|
||||||
@@ -159,7 +175,8 @@ class FakturowniaInvoiceImporter
|
|||||||
'document_number' => $document['number'],
|
'document_number' => $document['number'],
|
||||||
'document_type' => $documentType,
|
'document_type' => $documentType,
|
||||||
'buyer_name' => $document['buyer_name'],
|
'buyer_name' => $document['buyer_name'],
|
||||||
'seller_name' => $document['seller_name']
|
'seller_name' => $document['seller_name'],
|
||||||
|
'position_name' => $position['name']
|
||||||
] );
|
] );
|
||||||
return 'unmapped';
|
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';
|
return 'skipped';
|
||||||
|
|
||||||
$operationIds = [];
|
$operationIds = [];
|
||||||
@@ -206,7 +225,8 @@ class FakturowniaInvoiceImporter
|
|||||||
$operationIds,
|
$operationIds,
|
||||||
[
|
[
|
||||||
'number' => $document['number'],
|
'number' => $document['number'],
|
||||||
'client_name' => $document['client_name']
|
'client_name' => $document['client_name'],
|
||||||
|
'skipped_positions' => $skippedPositions
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -261,22 +281,6 @@ class FakturowniaInvoiceImporter
|
|||||||
return false;
|
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 )
|
private function normalizeDocument( $rawDocument, $documentType )
|
||||||
{
|
{
|
||||||
if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) )
|
if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) )
|
||||||
@@ -299,9 +303,23 @@ class FakturowniaInvoiceImporter
|
|||||||
$clientTaxNo = (string)( $rawDocument['buyer_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
|
$clientTaxNo = (string)( $rawDocument['buyer_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
// 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' );
|
$clientName = (string)( $rawDocument['seller_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' );
|
||||||
$clientTaxNo = (string)( $rawDocument['seller_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
|
$clientTaxNo = $sellerTaxNo !== '' ? $sellerTaxNo : (string)( $rawDocument['tax_no'] ?? '' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$positions = [];
|
$positions = [];
|
||||||
@@ -436,6 +454,18 @@ class FakturowniaInvoiceImporter
|
|||||||
return $normalizedTaxNo;
|
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 )
|
private function buildItemKey( $rawPosition, $name )
|
||||||
{
|
{
|
||||||
if ( isset( $rawPosition['product_id'] ) && (string)$rawPosition['product_id'] !== '' )
|
if ( isset( $rawPosition['product_id'] ) && (string)$rawPosition['product_id'] !== '' )
|
||||||
@@ -483,6 +513,7 @@ class FakturowniaInvoiceImporter
|
|||||||
$token = trim( (string)\Env::get( 'FAKTUROWNIA_API_TOKEN', '' ) );
|
$token = trim( (string)\Env::get( 'FAKTUROWNIA_API_TOKEN', '' ) );
|
||||||
$startDate = trim( (string)\Env::get( 'FAKTUROWNIA_START_DATE', '' ) );
|
$startDate = trim( (string)\Env::get( 'FAKTUROWNIA_START_DATE', '' ) );
|
||||||
$pageLimit = (int)\Env::get( 'FAKTUROWNIA_PAGE_LIMIT', 100 );
|
$pageLimit = (int)\Env::get( 'FAKTUROWNIA_PAGE_LIMIT', 100 );
|
||||||
|
$ownTaxNo = trim( (string)\Env::get( 'FAKTUROWNIA_OWN_TAX_NO', '' ) );
|
||||||
|
|
||||||
if ( $domain === '' || $token === '' || $startDate === '' )
|
if ( $domain === '' || $token === '' || $startDate === '' )
|
||||||
return [ 'status' => 'error', 'ok' => false, 'msg' => 'Import Fakturownia: brak konfiguracji w .env.' ];
|
return [ 'status' => 'error', 'ok' => false, 'msg' => 'Import Fakturownia: brak konfiguracji w .env.' ];
|
||||||
@@ -498,7 +529,8 @@ class FakturowniaInvoiceImporter
|
|||||||
'api_url' => rtrim( $domain, '/' ),
|
'api_url' => rtrim( $domain, '/' ),
|
||||||
'token' => $token,
|
'token' => $token,
|
||||||
'start_date' => $startDate,
|
'start_date' => $startDate,
|
||||||
'page_limit' => $pageLimit > 0 ? $pageLimit : 100
|
'page_limit' => $pageLimit > 0 ? $pageLimit : 100,
|
||||||
|
'own_tax_no' => $ownTaxNo
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,24 @@ $esc = function( $value )
|
|||||||
<? if ( !empty( $payload['tax_no'] ) ): ?>
|
<? if ( !empty( $payload['tax_no'] ) ): ?>
|
||||||
<small class="text-muted">NIP: <?= $esc( $payload['tax_no'] ); ?></small><br>
|
<small class="text-muted">NIP: <?= $esc( $payload['tax_no'] ); ?></small><br>
|
||||||
<? endif; ?>
|
<? endif; ?>
|
||||||
|
<? if ( !empty( $payload['document_number'] ) ): ?>
|
||||||
|
<small class="text-muted">Ostatnia faktura: <?= $esc( $payload['document_number'] ); ?></small><br>
|
||||||
|
<? endif; ?>
|
||||||
|
<? if ( !empty( $payload['positions'] ) && is_array( $payload['positions'] ) ): ?>
|
||||||
|
<? $positions = $payload['positions']; ?>
|
||||||
|
<? $shown = array_slice( $positions, 0, 10 ); ?>
|
||||||
|
<small class="text-muted">
|
||||||
|
Pozycje z faktury:
|
||||||
|
<ul style="margin:2px 0 4px 18px; padding:0;">
|
||||||
|
<? foreach ( $shown as $positionName ): ?>
|
||||||
|
<li><?= $esc( $positionName ); ?></li>
|
||||||
|
<? endforeach; ?>
|
||||||
|
<? if ( count( $positions ) > count( $shown ) ): ?>
|
||||||
|
<li>... i <?= count( $positions ) - count( $shown ); ?> wiecej</li>
|
||||||
|
<? endif; ?>
|
||||||
|
</ul>
|
||||||
|
</small>
|
||||||
|
<? endif; ?>
|
||||||
<small class="text-muted">Klucz: <?= $esc( $row['external_key'] ); ?> | wystapienia: <?= (int)$row['hits']; ?></small>
|
<small class="text-muted">Klucz: <?= $esc( $row['external_key'] ); ?> | wystapienia: <?= (int)$row['hits']; ?></small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -90,33 +108,39 @@ $esc = function( $value )
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Pozycja z faktury</th>
|
<th>Pozycja z faktury</th>
|
||||||
<th>Kategoria finansowa</th>
|
<th>Kategoria finansowa (mapuje globalnie)</th>
|
||||||
<th style="width: 130px;">Akcja</th>
|
<th style="width: 200px;">Akcja</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<? foreach ( $this -> fakturownia_pending_items as $row ): ?>
|
<? $renderedRows = 0; ?>
|
||||||
|
<? foreach ( $this -> fakturownia_pending_items as $itemIdx => $row ): ?>
|
||||||
|
<? $payload = is_array( $row['payload'] ?? null ) ? $row['payload'] : []; ?>
|
||||||
|
<? $occurrences = ( isset( $payload['occurrences'] ) && is_array( $payload['occurrences'] ) ) ? $payload['occurrences'] : []; ?>
|
||||||
|
<? if ( empty( $occurrences ) ) continue; ?>
|
||||||
|
<? $saveFormId = 'fi-save-' . $itemIdx; ?>
|
||||||
|
<? $occCount = count( $occurrences ); ?>
|
||||||
|
<? foreach ( $occurrences as $occIdx => $occ ): ?>
|
||||||
|
<? $isFirst = $occIdx === 0; ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<?= $esc( $row['external_name'] ); ?><br>
|
<?= $esc( $row['external_name'] ); ?><br>
|
||||||
<? $payload = is_array( $row['payload'] ?? null ) ? $row['payload'] : []; ?>
|
<? if ( !empty( $occ['document_number'] ) ): ?>
|
||||||
<? if ( !empty( $payload['document_number'] ) ): ?>
|
<small class="text-muted">Faktura: <?= $esc( $occ['document_number'] ); ?> (ID <?= $esc( $occ['document_id'] ?? '' ); ?>)</small><br>
|
||||||
<small class="text-muted">Faktura: <?= $esc( $payload['document_number'] ); ?></small><br>
|
|
||||||
<? endif; ?>
|
<? endif; ?>
|
||||||
<? if ( !empty( $payload['buyer_name'] ) || !empty( $payload['seller_name'] ) ): ?>
|
<? if ( !empty( $occ['buyer_name'] ) || !empty( $occ['seller_name'] ) ): ?>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
Nabywca: <?= $esc( $payload['buyer_name'] ?? '-' ); ?> |
|
Nabywca: <?= $esc( $occ['buyer_name'] ?? '-' ); ?> |
|
||||||
Sprzedawca: <?= $esc( $payload['seller_name'] ?? '-' ); ?>
|
Sprzedawca: <?= $esc( $occ['seller_name'] ?? '-' ); ?>
|
||||||
</small><br>
|
</small><br>
|
||||||
<? endif; ?>
|
<? endif; ?>
|
||||||
<small class="text-muted">Klucz: <?= $esc( $row['external_key'] ); ?> | wystapienia: <?= (int)$row['hits']; ?></small>
|
<? if ( $isFirst ): ?>
|
||||||
|
<small class="text-muted">Klucz: <?= $esc( $row['external_key'] ); ?> | wystapien w kolejce: <?= $occCount; ?></small>
|
||||||
|
<? endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<? if ( $isFirst ): ?>
|
||||||
<form method="post" action="/finances/fakturownia_item_mapping_save/" class="form-inline">
|
<td rowspan="<?= $occCount; ?>" style="vertical-align: top;">
|
||||||
<input type="hidden" name="csrf_token" value="<?= \S::csrf_token(); ?>">
|
<select name="finance_category_id" form="<?= $saveFormId; ?>" class="form-control input-sm" required>
|
||||||
<input type="hidden" name="external_key" value="<?= $esc( $row['external_key'] ); ?>">
|
|
||||||
<input type="hidden" name="external_name" value="<?= $esc( $row['external_name'] ); ?>">
|
|
||||||
<select name="finance_category_id" class="form-control input-sm" required>
|
|
||||||
<option value="">Wybierz kategorie</option>
|
<option value="">Wybierz kategorie</option>
|
||||||
<? foreach ( $this -> fakturownia_categories as $category ): ?>
|
<? foreach ( $this -> fakturownia_categories as $category ): ?>
|
||||||
<option value="<?= (int)$category['id']; ?>">
|
<option value="<?= (int)$category['id']; ?>">
|
||||||
@@ -124,13 +148,34 @@ $esc = function( $value )
|
|||||||
</option>
|
</option>
|
||||||
<? endforeach; ?>
|
<? endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
<form id="<?= $saveFormId; ?>" method="post" action="/finances/fakturownia_item_mapping_save/" style="display:inline;">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= \S::csrf_token(); ?>">
|
||||||
|
<input type="hidden" name="external_key" value="<?= $esc( $row['external_key'] ); ?>">
|
||||||
|
<input type="hidden" name="external_name" value="<?= $esc( $row['external_name'] ); ?>">
|
||||||
|
<button type="submit" class="btn btn-success btn-sm mt5">Zapisz mapowanie</button>
|
||||||
|
</form>
|
||||||
|
<? if ( $occCount > 1 ): ?>
|
||||||
|
<br><small class="text-muted">Zapis mapowania kategorii rozwiazuje wszystkie <?= $occCount; ?> wystapien tej pozycji.</small>
|
||||||
|
<? endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<? endif; ?>
|
||||||
<button type="submit" class="btn btn-success btn-sm">Zapisz</button>
|
<td style="white-space:nowrap;">
|
||||||
|
<form method="post" action="/finances/fakturownia_document_position_skip/" style="display:inline-block; margin:0;">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= \S::csrf_token(); ?>">
|
||||||
|
<input type="hidden" name="external_id" value="<?= $esc( $occ['document_id'] ?? '' ); ?>">
|
||||||
|
<input type="hidden" name="document_type" value="<?= $esc( $occ['document_type'] ?? '' ); ?>">
|
||||||
|
<input type="hidden" name="external_item_key" value="<?= $esc( $row['external_key'] ); ?>">
|
||||||
|
<input type="hidden" name="item_name" value="<?= $esc( $row['external_name'] ); ?>">
|
||||||
|
<button type="submit" class="btn btn-warning btn-sm" title="Pomijaj te pozycje tylko na tej fakturze">Pomijaj na tej fakturze</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<? $renderedRows++; ?>
|
||||||
<? endforeach; ?>
|
<? endforeach; ?>
|
||||||
|
<? endforeach; ?>
|
||||||
|
<? if ( $renderedRows === 0 ): ?>
|
||||||
|
<tr><td colspan="3"><em>(brak wystapien do rozwiazania)</em></td></tr>
|
||||||
|
<? endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<? endif; ?>
|
<? endif; ?>
|
||||||
|
|||||||
168
tests/Domain/Finances/FakturowniaApiClientTest.php
Normal file
168
tests/Domain/Finances/FakturowniaApiClientTest.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../autoload/Domain/Finances/FakturowniaApiClient.php';
|
||||||
|
|
||||||
|
use Domain\Finances\FakturowniaApiClient;
|
||||||
|
|
||||||
|
class FakeFakturowniaApiClient extends FakturowniaApiClient
|
||||||
|
{
|
||||||
|
public $calledPaths = [];
|
||||||
|
public $responses = [];
|
||||||
|
|
||||||
|
protected function request( $path, $query )
|
||||||
|
{
|
||||||
|
$this -> 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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ require_once __DIR__ . '/Domain/Tasks/TaskAttachmentRepositoryTest.php';
|
|||||||
require_once __DIR__ . '/Domain/Tasks/MailToTaskImporterTest.php';
|
require_once __DIR__ . '/Domain/Tasks/MailToTaskImporterTest.php';
|
||||||
require_once __DIR__ . '/Domain/Users/UserRepositoryTest.php';
|
require_once __DIR__ . '/Domain/Users/UserRepositoryTest.php';
|
||||||
require_once __DIR__ . '/Domain/Users/PermissionRepositoryTest.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/TasksControllerTest.php';
|
||||||
require_once __DIR__ . '/Controllers/UsersControllerTest.php';
|
require_once __DIR__ . '/Controllers/UsersControllerTest.php';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ $tests = [
|
|||||||
'run_mail_to_task_importer_tests',
|
'run_mail_to_task_importer_tests',
|
||||||
'run_user_repository_tests',
|
'run_user_repository_tests',
|
||||||
'run_permission_repository_tests',
|
'run_permission_repository_tests',
|
||||||
|
'run_fakturownia_api_client_tests',
|
||||||
'run_tasks_controller_tests',
|
'run_tasks_controller_tests',
|
||||||
'run_users_controller_tests'
|
'run_users_controller_tests'
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user