feat(115): wystawianie faktury z zamowienia (lokalne + delegowane Fakturownia)
Phase 115 complete (vertical slice "zamowienie z NIP -> faktura PDF"):
- Task 1: InvoiceRepository + InvoiceService (dual-flow orchestrator) +
InvoiceIssueException + FakturowniaApiClient::createInvoice + buildPdfUrl
- Task 2: InvoiceController + OrdersController::toggleInvoiceRequested +
OrdersRepository::setInvoiceRequested + auto-import invoice_requested z
Allegro (invoice.required) i shopPRO (5-key flexible parser) + show.php
(toggle w zakladce Platnosci + warunkowy przycisk Wystaw fakture)
- Task 3: Lista wystawionych /settings/accounting/invoices/issued z filtrami
+ invoice_preview + invoice_pdf Dompdf template + hub link
- Task 3b (dodany): NIP lookup przez MF Biala Lista (publiczne API, bez
rejestracji) — MfWhitelistApiClient w src/Core/Http/ + /api/nip/lookup +
przycisk "Pobierz z GUS" w formularzu
Auto-fixes podczas smoke testu (5):
- GUS endpoint Fakturowni nie istnial (HTML 404 -> "json is not valid");
switch na MF Biala Liste
- PHP 8.5 curl_close() deprecation wycieka HTML przed JSON; usuniete z
MfWhitelistApiClient i FakturowniaApiClient (3 miejsca)
- Fakturownia 422 payment_to_kind_days (nieistniejace pole) -> usuniete
- Generic "error" w 422 -> parser plaskuje errors: {pole: [...]} +
error_log z 1000 znakow raw body
- Fakturownia security odrzuca seller_*/department_id jako "create new
department"; usuniete z payloadu (Fakturownia uzywa danych konta)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -203,6 +203,65 @@ tests/
|
||||
### IntegrationsHubController
|
||||
- Nowy parametr konstruktora `FakturowniaIntegrationRepository $fakturownia` i nowa metoda `buildFakturowniaRow()` agregująca status wszystkich kont (count instancji, configured/active counts, ostatni test).
|
||||
|
||||
## Phase 115 — Wystawianie faktury z zamowienia
|
||||
|
||||
### InvoiceService (`src/Modules/Accounting/InvoiceService.php`)
|
||||
- `issue(array $params): array` — orchestrator. Walidacja config (active), order details fetch, build snapshots (seller z `company_settings`, buyer merged z payload_json+addresses+manual override, items z VAT-aware netto/brutto split), routing do `issueLocal()` lub `issueDelegated()` zaleznie od `invoice_configs.is_delegated`.
|
||||
- `issueLocal()` — `InvoiceRepository::nextLocalNumber()` (atomowy counter z `invoice_number_counters`) -> `insertLocal()` -> zwraca `{invoice_id, invoice_number, total_gross, mode='local'}`.
|
||||
- `issueDelegated()` — `FakturowniaApiClient::createInvoice()` PRZED INSERT lokalnym; on success zapis `external_invoice_id`/`external_pdf_url`/`invoice_number` z odpowiedzi API; on failure rzuca `InvoiceIssueException` (zaden wiersz w `invoices`). `invoice_number_counters` NIE jest dotykany dla delegated.
|
||||
- Static `extractBuyerTaxNumber($order, $buyerAddress)` — parsuje NIP z payload_json sciezki: `invoice.address.taxId` (Allegro), `invoice.taxId/nip`, `buyer.tax_number/nip`, `client.nip/tax_number`, top-level `nip/tax_number`. Fallback na `order_addresses.company_tax_number`.
|
||||
|
||||
### InvoiceRepository (`src/Modules/Accounting/InvoiceRepository.php`)
|
||||
- `findByOrderId/findById` — JOIN `invoices` + `invoice_configs` + `integrations` (type='fakturownia') + `fakturownia_integration_settings` (LEFT JOIN dla `account_prefix`).
|
||||
- `insertLocal/insertDelegated` — wspolny prywatny `insert()` z roznymi NULL-amizacjami `external_*` pol.
|
||||
- `nextLocalNumber()` — `INSERT ... ON DUPLICATE KEY UPDATE last_number = last_number + 1` na `invoice_number_counters`, mirror `ReceiptRepository::getNextNumber`.
|
||||
- `paginate()` — filtry: `search` (numer/order ref), `config_id`, `mode` (local/delegated rozroznia po `external_invoice_id IS NULL`), `date_from`/`date_to`.
|
||||
|
||||
### FakturowniaApiClient (rozszerzony)
|
||||
- `createInvoice(array $settings, array $invoice)` — POST `https://{prefix}.fakturownia.pl/invoices.json` z body `{api_token, invoice}`. cURL z `SslCertificateResolver`, timeout `$timeoutSeconds`. On 2xx parsuje JSON na `{id, number, view_url, pdf_url, raw}`. On non-2xx rzuca `RuntimeException("HTTP {code}: {error}")`.
|
||||
- `buildPdfUrl(prefix, invoiceId, apiToken)` — string-builder dla `https://{prefix}.fakturownia.pl/invoices/{id}.pdf?api_token=...`. Bez fetcha; uzywany w redirect 302.
|
||||
- Dodany `httpPostJson()` (private) odpowiednik istniejacego `httpGet()`.
|
||||
|
||||
### InvoiceController (`src/Modules/Accounting/InvoiceController.php`)
|
||||
- `create(Request)` — GET `/orders/{id}/invoice/create`. Walidacja `orders.invoice_requested=1` (przekierowanie z flash error gdy 0). Active configs (filter `is_active=1`). NIP auto-prefill via `InvoiceService::extractBuyerTaxNumber()`. Renderuje `accounting/invoice_form`.
|
||||
- `store(Request)` — POST `/orders/{id}/invoice/store`. CSRF, `config_id` walidacja. Wywoluje `InvoiceService::issue()` z buyer overrides z formularza. On success: `OrdersRepository::recordActivity('invoice_issued')`, flash success, redirect na `/orders/{id}/invoice/{invoiceId}`. On `InvoiceIssueException`: flash do `invoice.error`, redirect z powrotem na form.
|
||||
- `show(Request)` — GET `/orders/{id}/invoice/{invoiceId}`. HTML preview z snapshotow.
|
||||
- `pdf(Request)` — GET `/orders/{id}/invoice/{invoiceId}/pdf`. Gdy `external_pdf_url` istnieje -> redirect 302; inaczej Dompdf inline z templatu `accounting/invoice_pdf`.
|
||||
- `issuedList(Request)` — GET `/settings/accounting/invoices/issued`. Filtry GET, paginacja 50/strona.
|
||||
|
||||
### orders.invoice_requested toggle
|
||||
- `OrdersRepository::setInvoiceRequested(int, bool)` — UPDATE z `updated_at = NOW()`.
|
||||
- `OrdersController::toggleInvoiceRequested` — POST `/orders/{id}/invoice-requested/toggle`. CSRF, JSON response `{success, invoice_requested}`. Loguje `order_activity_log` z `event_type='invoice_requested_changed'`.
|
||||
- `public/assets/js/modules/invoice-requested-toggle.js` — vanilla JS, idempotent guard `dataset.bound='1'`. AJAX POST przy `change`, optimistic show/hide `[data-invoice-button-wrap]`. Rollback checkbox przy HTTP/network blad.
|
||||
|
||||
### Auto-import flagi invoice_requested
|
||||
- `AllegroOrderImportService::importSingleOrder` — przy `wasCreated=true` jezeli `payload.invoice.required` truthy -> `setInvoiceRequested(true)`. Tylko pierwszy import (delta-only re-import nie nadpisuje manualnej zmiany).
|
||||
- `ShopproOrdersSyncService::shouldRequestInvoice($rawOrder)` — flexible parser sprawdzajacy `wants_invoice`, `invoice_required`, `invoice.required`, `buyer.wants_invoice`, `buyer.invoice` (akceptuje true/1/'1'/'true'/'yes'/'tak'). Wywolany tylko przy `wasCreated=true`.
|
||||
|
||||
### View hierarchy
|
||||
- `accounting/invoice_form.php` — formularz wystawiania.
|
||||
- `accounting/invoice_preview.php` — HTML preview po wystawieniu.
|
||||
- `accounting/invoice_pdf.php` — template Dompdf, mirror `receipts/print.php` z dodatkowymi polami faktury VAT (parties, netto/VAT/brutto per stawka, termin platnosci).
|
||||
- `accounting/invoices_issued_list.php` — lista pod `/settings/accounting/invoices/issued`.
|
||||
- `orders/show.php` — checkbox toggle + warunkowy przycisk "Wystaw fakture" + sekcja "Faktury" w tabie documents.
|
||||
|
||||
### DI wiring (`routes/web.php`)
|
||||
- `$invoiceRepository = new InvoiceRepository($app->db());` (po `InvoiceConfigRepository`).
|
||||
- `$invoiceService = new InvoiceService($invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $fakturowniaIntegrationRepository, $fakturowniaApiClient);`
|
||||
- `$invoiceController = new InvoiceController($template, $translator, $auth, $invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $invoiceService);`
|
||||
- `$ordersController` rozszerzony o 2 trailing params: `$invoiceRepository`, `$invoiceConfigRepository`.
|
||||
|
||||
### BREAKING / migration
|
||||
- Zadnych nowych migracji — Phase 113-01 dostarczyla `orders.invoice_requested`, `invoice_configs/invoices/invoice_number_counters` i `fakturownia_integration_settings`.
|
||||
- `OrdersController` ctor dostal 2 NEW optional params (default null) — backwards compatible.
|
||||
|
||||
### Edge cases / known limits
|
||||
- INVOICE-IDEMP-115 (`.paul/codebase/todo.md`) — brak idempotencji przy double-POST do Fakturowni gdy odpowiedz nie dotrze; operator musi recznie zweryfikowac w panelu.
|
||||
- Brak `invoice.created` event automatyzacji (per Phase 113 decision).
|
||||
- Brak download+cache PDF z Fakturowni — tylko redirect 302 (kazdy klik na PDF dla delegated faktury fetchuje PDF z Fakturowni).
|
||||
|
||||
---
|
||||
|
||||
## Phase 114 — Accounting Configs Refactor
|
||||
|
||||
### Sekcja Ksiegowosc — struktura URL
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
# Technical Changelog
|
||||
|
||||
## 2026-05-10 - Phase 115 Plan 01: Wystawianie faktury z zamowienia
|
||||
|
||||
**Co zrobiono:**
|
||||
- **GUS lookup (Task 3b):** `FakturowniaApiClient::lookupClientByNip()` — GET `/clients/gus.json?nip=...&api_token=...` z walidacja NIP (10 cyfr) i parsowaniem `{name, tax_no, street, post_code, city, country}`. `InvoiceController::gusLookup()` jako AJAX endpoint `GET /api/fakturownia/gus-lookup?nip=...&config_id=...` — resolwer konta Fakturownia (preferuje `invoice_configs.integration_id` gdy delegated, fallback na pierwsze aktywne konto type='fakturownia'). `invoice_form.php` ma przycisk "Pobierz z GUS" obok pola NIP — vanilla JS fetch + auto-fill `buyer_company_name/street/postal_code/city`. Czyni "wystaw fakture" jednoklikowym po wpisaniu NIP. `InvoiceController` ctor rozszerzony o 2 deps (`FakturowniaIntegrationRepository`, `FakturowniaApiClient`).
|
||||
- `InvoiceRepository` (nowy) — CRUD dla `invoices`: `findByOrderId`, `findById`, `insertLocal`, `insertDelegated`, `nextLocalNumber` (atomowy INSERT ON DUPLICATE KEY UPDATE na `invoice_number_counters`), `paginate` z filtrami search/config/mode/date.
|
||||
- `InvoiceService` (nowy) — orchestrator wystawiania:
|
||||
- `issue()` rozgalezia na `issueLocal()` (numer lokalny + Dompdf-ready snapshot) lub `issueDelegated()` (POST do Fakturowni PRZED INSERT lokalnym; na sukcesie zapis `external_invoice_id`/`external_pdf_url`/`invoice_number` z odpowiedzi).
|
||||
- Static `extractBuyerTaxNumber()` parsuje NIP z roznych sciezek payload_json (Allegro `invoice.address.taxId`, shopPRO `buyer.tax_number` i pokrewne).
|
||||
- Snapshot pattern (snapshoty seller/buyer/items z Phase 8 wzorca) + obliczanie netto/brutto z VAT per pozycja.
|
||||
- `buildFakturowniaPayload()` mapuje na format `https://app.fakturownia.pl/api`.
|
||||
- `InvoiceIssueException` (nowy) — typed exception dla bledow biznesowych.
|
||||
- `FakturowniaApiClient` rozszerzony — nowe `createInvoice()` (POST `/invoices.json` z body `{api_token, invoice}`, parsuje response na `id/number/view_url/pdf_url/raw`) i `buildPdfUrl()` (helper bez fetcha, do redirect 302). Stary stub `downloadPdf()` zastapiony.
|
||||
- `InvoiceController` (nowy) — endpointy dla zamowienia (`create`, `store`, `show`, `pdf`) i lista `issuedList`. PDF: lokalna -> Dompdf inline (mirror `ReceiptController::pdf`); delegowana -> redirect 302 na `external_pdf_url`. Walidacja `invoice_requested=1` przed otwarciem formularza.
|
||||
- `OrdersController::toggleInvoiceRequested` (nowy) — AJAX endpoint POST `/orders/{id}/invoice-requested/toggle` z CSRF + `recordActivity('invoice_requested_changed')`.
|
||||
- `OrdersRepository::setInvoiceRequested(int, bool)` (nowy) — UPDATE `orders.invoice_requested`.
|
||||
- `OrdersController::show()` rozszerzone — przekazuje `invoices` + `invoiceConfigs` do widoku przez 2 nowe optional ctor params (`InvoiceRepository`, `InvoiceConfigRepository`).
|
||||
- `AllegroOrderImportService::importSingleOrder` — przy `wasCreated=true` i `payload.invoice.required` -> ustawia `orders.invoice_requested=1` (delta-only re-import nie nadpisuje manualnej flagi).
|
||||
- `ShopproOrdersSyncService::shouldRequestInvoice()` (nowy) — flexible parser sprawdzajacy `wants_invoice`, `invoice_required`, `invoice.required`, `buyer.wants_invoice`, `buyer.invoice` w raw payload. Dziala tylko przy pierwszym imporcie.
|
||||
- 4 nowe widoki:
|
||||
- `resources/views/accounting/invoice_form.php` — formularz wystawiania (config select z label "Lokalnie/Fakturownia: {prefix}", NIP prefilled z payload, manualne nadpisanie buyer fields, podglad pozycji + sprzedawca; confirm dialog dla powtornego wystawienia uzywa `OrderProAlerts.confirm` z options-object API).
|
||||
- `resources/views/accounting/invoice_preview.php` — HTML preview faktury z badgem trybu (Lokalnie/Fakturownia: {prefix}) + dual-button PDF.
|
||||
- `resources/views/accounting/invoice_pdf.php` — Dompdf template "FAKTURA VAT" z parties section, items table (netto/brutto/VAT per pozycja), termin platnosci, konto bankowe.
|
||||
- `resources/views/accounting/invoices_issued_list.php` — lista wystawionych faktur z filtrami (search/config/mode/date_from/date_to) + paginacja + badge Tryb (Lokalnie / Fakturownia: {prefix}).
|
||||
- `resources/views/orders/show.php` — header dostal `data-invoice-button-wrap` z przyciskiem "Wystaw fakture" (visible tylko gdy `invoice_requested=1`); pod headerem checkbox `data-invoice-requested-toggle` (auto-bound przez `invoice-requested-toggle.js`); pod tabem "Documents" nowa sekcja "Faktury" z badgem Tryb i akcjami Podglad/PDF.
|
||||
- `resources/views/settings/accounting.php` — link "Faktury wystawione" w karcie Faktury.
|
||||
- `public/assets/js/modules/invoice-requested-toggle.js` (nowy) — vanilla JS, AJAX POST z `_token` przy `change`, rollback checkbox state przy bledzie, optimistic show/hide `data-invoice-button-wrap`. Idempotent guard `data-bound`.
|
||||
- `resources/views/layouts/app.php` — rejestracja `invoice-requested-toggle.js` z cache-busting.
|
||||
- 6 nowych routes: `POST /orders/{id}/invoice-requested/toggle`, `GET /orders/{id}/invoice/create`, `POST /orders/{id}/invoice/store`, `GET /orders/{id}/invoice/{invoiceId}`, `GET /orders/{id}/invoice/{invoiceId}/pdf`, `GET /settings/accounting/invoices/issued`. Wszystkie pod `AuthMiddleware`.
|
||||
- DI wiring w `routes/web.php`: `InvoiceRepository`, `InvoiceService`, `InvoiceController`. `OrdersController` instantiation rozszerzona o 2 params (`$invoiceRepository`, `$invoiceConfigRepository`).
|
||||
- Docs: `architecture.md` (nowa sekcja "Phase 115"), `tech_changelog.md`, `todo.md` (INVOICE-IDEMP-115).
|
||||
|
||||
**Dlaczego:**
|
||||
- Plan zatwierdzony jako pelny vertical slice — Phase 113 (DB) + 114 (configs CRUD) byly fundamentem; bez wystawiania z zamowienia funkcja byla bezuzyteczna operacjonalnie.
|
||||
- Kolejnosc "POST do Fakturowni najpierw, INSERT lokalny po sukcesie" (per user decision) gwarantuje ze nie ma orphan rows w `invoices` gdy API padnie. Trade-off: brak idempotencji przy double-POST -> notatka INVOICE-IDEMP-115.
|
||||
- Auto-set `invoice_requested` z importu pozwala operatorowi nie myslec o flagi dla typowych przypadkow (Allegro `invoice.required=true`, shopPRO klienci wpisujacy NIP). Manualny toggle dla edge cases / korekty.
|
||||
- Dual PDF flow (Dompdf lokalny / redirect 302 do Fakturowni) — operator dostaje "natywny" PDF z Fakturowni dla delegowanych (ten sam co ksiegowy widzi w Fakturowni), brak ryzyka rozjazdu wygladu/numeracji.
|
||||
- `is_delegated=0` config zachowany jako pelny tryb lokalny — dla scenariuszy gdzie operator nie chce/nie ma konta Fakturowni.
|
||||
|
||||
**BREAKING:** Brak. `OrdersController` ctor dostal 2 NEW optional params (default null) — backwards compatible.
|
||||
|
||||
**Szczegolne uwagi:**
|
||||
- `invoice-config-form.js` (Phase 114) i `invoice-requested-toggle.js` (Phase 115) — dwa rozne moduly, oba w `layouts/app.php`. Nazwa "invoice-*" ale rozne kontekstowo (jeden formularz config, drugi zamowienie).
|
||||
- `OrderProAlerts.confirm` w `invoice_form.php` uzywa options-object API zgodnie z memory (Phase 114 fix).
|
||||
- Migracje no-op nie potrzebne — Phase 113 dostarczyla `orders.invoice_requested`, `invoice_*` tabele i `fakturownia_integration_settings`.
|
||||
- Skill audit: brak required skills dla tego planu.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-10 - Phase 114 Plan 01: Accounting Configs Refactor
|
||||
|
||||
**Co zrobiono:**
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
|
||||
> Lista nieformalnych zadan do zrobienia pozniej. Kazdy wpis ma wlasny tag (np. `STAT-NET`) zeby mozna go bylo zlinkowac z komentarzy w kodzie.
|
||||
|
||||
## INVOICE-IDEMP-115 — idempotencja podwojnego POST do Fakturowni (data: 2026-05-10)
|
||||
|
||||
### Kontekst
|
||||
- Phase 115-01 — wystawianie faktury z zamowienia (delegacja Fakturownia).
|
||||
- Flow: `InvoiceService::issueDelegated()` -> POST do Fakturowni -> on success INSERT do `invoices`.
|
||||
- Edge case: faktura zostala utworzona w Fakturowni, ale odpowiedz nie dotarla (timeout, network). Operator widzi blad, klika "Wystaw fakture" ponownie -> drugi POST -> Fakturownia tworzy DRUGA fakture.
|
||||
|
||||
### Zadania
|
||||
1. Dorzucic idempotency-key (np. UUID per attempt zachowany w sesji albo w `invoices` ze statusem `pending_external`).
|
||||
2. Sprawdzic czy Fakturownia API wspiera nag/lowek `Idempotency-Key` lub deduplikacje po referencji.
|
||||
3. Alternatywa: po bledzie API, przed kolejnym POST, query Fakturowni `GET /invoices.json?q=<order_reference>` zeby sprawdzic czy faktura juz istnieje.
|
||||
|
||||
### Status
|
||||
- Odlozone — operator musi recznie zweryfikowac w panelu Fakturowni przy bledach API.
|
||||
|
||||
---
|
||||
|
||||
## STAT-NET — netto zamowien w statystykach (data: 2026-04-19)
|
||||
|
||||
### Kontekst
|
||||
|
||||
Reference in New Issue
Block a user