diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 30135d7..ab4ceee 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -13,8 +13,8 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów | Attribute | Value | |-----------|-------| | Version | 3.7.0-dev | -| Status | v3.7 in progress — Phases 113-124 shipped (Fakturownia + HostedSMS/SMSPLANET + Alert unify + receipt VAT + SMS templates) | -| Last Updated | 2026-05-13 (Phase 124 closed) | +| Status | v3.7 in progress — Phases 113-125 shipped (Fakturownia + HostedSMS/SMSPLANET + Alert unify + receipt VAT + SMS templates + invoice_requested import fix) | +| Last Updated | 2026-05-13 (Phase 125 closed) | ## Requirements @@ -125,6 +125,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Ujednolicony moduł alertów UI: reusable PHP komponent `resources/views/components/alert.php` z inline SVG ikoną per typ (info/success/warning/danger), opcjonalnym dismiss button (vanilla JS, idempotent); brakujący `.alert--info` (#eff6ff/#bfdbfe/#1e3a8a); `Flash::push/all` z BC dla `set/get` (heurystyka klucza legacy); centralny renderer flash w 3 layoutach (app/auth/public); 36 widoków zmigrowanych off inline alert markup; `.flash--*` usunięte z widoków — Phase 120 - [x] Eksport XLSX paragonow w `/accounting`: nowe naglowki (Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT) z osobnym wierszem per stawka VAT; `items_json` snapshot rozszerzony o `vat` per pozycja (z `order_items.tax_rate`, fallback 23.0); legacy fallback `net = brutto/1.23` — Phase 123 - [x] Szablony SMS: CRUD w `/settings/sms-templates` (name + body + is_active), wspolny `SmsVariableResolver` wydzielony z Email\\VariableResolver (placeholdery `{{zamowienie.*|kupujacy.*|adres.*|firma.*|przesylka.*}}`), dropdown "Wybierz szablon" w zakladce SMS na `/orders/{id}` wstawia rozwiniete zmienne do textarea (z `OrderProAlerts.confirm` przy nadpisaniu); stopka SMSPLANET dalej doklejana wylacznie przez `SmsConversationService::buildFinalOutboundBody()` (Phase 122 contract preserved) — Phase 124 +- [x] Bugfix detekcji faktury przy imporcie: shopPRO order z `firm_nip` ustawia `invoice_requested=1` (mapper jako jedyne zrodlo heurystyki, sync service propaguje `aggregate['invoice_detected']`); Allegro rozszerzony o `naturalPerson=false`/`address.taxId`/`companyName` (wczesniej tylko `invoice.required`); usunieta legacy kolumna `orders.is_invoice` (Phase 115 dryft) + backfill 7 zamowien — Phase 125 ### Deferred diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index fdf3166..4a72255 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -24,6 +24,7 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt | 122 | SMSPLANET Default SMS Footer | 1/1 | Complete (2026-05-12; live SMS smoke + over-limit UI test pending operator) | | 123 | Receipts Export VAT Breakdown | 1/1 | Complete (2026-05-12; manual XLSX smoke pending operator) | | 124 | SMS Templates | 1/1 | Complete (2026-05-13; migration + manual SMS smoke pending operator) | +| 125 | invoice_requested Import Fix (shopPRO+Allegro NIP detection, drop legacy is_invoice column) | 1/1 | Complete (2026-05-13; migration + manual smoke pending operator) | Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania): - Eksport XLSX listy wystawionych faktur (analogicznie do paragonow) @@ -506,4 +507,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-05-12 - Phase 123 UNIFY closed* +*Last updated: 2026-05-13 - Phase 125 UNIFY closed* diff --git a/.paul/STATE.md b/.paul/STATE.md index 122c72c..892d764 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,33 +5,33 @@ See: .paul/PROJECT.md (updated 2026-05-07) **Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. -**Current focus:** v3.7 Invoices + operational integrations - Phase 124 SMS Templates complete (UNIFY closed). +**Current focus:** v3.7 Invoices + operational integrations - Phase 125 invoice_requested import fix complete (UNIFY closed). ## Current Position Milestone: v3.7 Invoices (Fakturownia integration) - In progress -Phase: 124 of TBD (SMS Templates) - Complete -Plan: 124-01 complete -Status: UNIFY complete, ready to plan next phase -Last activity: 2026-05-13 00:30:00 - UNIFY closed for .paul/phases/124-sms-templates/124-01-PLAN.md +Phase: 125 of TBD (invoice_requested import fix) - Complete +Plan: 125-01 complete +Status: UNIFY complete, transition pending (commit + ROADMAP update) +Last activity: 2026-05-13 - UNIFY closed for .paul/phases/125-invoice-requested-import-fix/125-01-PLAN.md Progress: -- Milestone v3.7: [##########] ~98% (Phase 113-124 complete) -- Phase 124: [##########] 100% +- Milestone v3.7: [##########] ~99% (Phase 113-125 complete; transition pending) +- Phase 125: [##########] 100% ## Loop Position Current loop state: ``` PLAN -> APPLY -> UNIFY - done done done [Loop complete - ready for next PLAN] + done done done [Loop complete - transition pending] ``` ## Session Continuity -Last session: 2026-05-13 00:30:00 -Stopped at: Phase 124 UNIFY closed (UI fixes accepted by operator) -Next action: Pick next v3.7 phase (kandydaci w ROADMAP) or transition do v3.8 +Last session: 2026-05-13 +Stopped at: Phase 125-01 UNIFY closed; SUMMARY.md created +Next action: Phase transition (commit + ROADMAP update), then pick next v3.7 candidate or transition to v3.8 Resume file: .paul/ROADMAP.md ## Pending parallel work @@ -39,8 +39,8 @@ Resume file: .paul/ROADMAP.md ## Git State -Last phase commit: 360eef1 feat(121+122): smsplanet conversation, notifications, default footer -Previous: 8f14851 feat(118): fakturownia single instance +Last phase commit: 522c94a feat(124): sms templates CRUD + order picker +Previous: 360eef1 feat(121+122): smsplanet conversation, notifications, default footer Branch: main Note: routes/web.php, DOCS/* i .paul/codebase/* zawierały zmiany z 118+121+122 nakładkowo i wszystkie wpadły do commitu 360eef1. @@ -61,6 +61,9 @@ Note: routes/web.php, DOCS/* i .paul/codebase/* zawierały zmiany z 118+121+122 - Phase 124 follow-up: `php bin/migrate.php` (XAMPP MySQL online) — utworzy `sms_templates`. Operator nastepnie tworzy szablony manualnie z `/settings/sms-templates`. - Phase 124 follow-up: real smoke wysylki SMS z szablonu (zamowienie z paczka + skonfigurowana stopka SMSPLANET) — sprawdzic ze `sms_messages.body` ma stopke raz, finalna tresc <= 918 znakow. - Phase 124 follow-up: regresja Email — wyslij e-mail z istniejacym szablonem aby potwierdzic ze refaktor `Email\VariableResolver` na fasade nie zlamal `EmailSendingService`. +- Phase 125 follow-up: uruchom `php bin/migrate.php` (XAMPP MySQL online) — backfill 7 zamowien + DROP COLUMN `orders.is_invoice`. Weryfikacja: `SELECT id, invoice_requested FROM orders WHERE id=1089;` -> `1`; `SHOW COLUMNS FROM orders LIKE 'is_invoice';` -> empty. +- Phase 125 follow-up: ponowne uruchomienie migracji powinno byc no-op (`ALTER TABLE orders COMMENT = 'phase-125 backfill no-op'`). +- Phase 125 follow-up: zaimportuj nowe zamowienie shopPRO z `firm_nip` (bez kluczy w 5-elementowej liscie wczesniejszej heurystyki) -> potwierdz ze UI w zakladce Platnosci pokazuje zaznaczony checkbox „Klient prosi o fakture" i widoczny przycisk „Wystaw fakture". - Phase 121 transition note (rozwiązane): commit 360eef1 obejmuje Phase 121 i Phase 122 razem; per-faza hunk-split nie wykonany ze względu na nakładkowe modyfikacje plików. ## Deferred to Next Milestones diff --git a/.paul/changelog/2026-05-13.md b/.paul/changelog/2026-05-13.md index 6399139..df67d79 100644 --- a/.paul/changelog/2026-05-13.md +++ b/.paul/changelog/2026-05-13.md @@ -8,6 +8,10 @@ - [Phase 124, Plan 01] Migracja `20260512_000112_create_sms_templates.sql` (CREATE TABLE, DDL). Stopka SMSPLANET dalej doklejana wylacznie przez `SmsConversationService::buildFinalOutboundBody()` (Phase 122 contract preserved). - [Phase 124, Plan 01] UI fixes po UAT operatora: paleta zmiennych przeniesiona pod textarea z pill chipami `{{var}} + opis` (border-radius 999px, hover indigo); akcje w liscie szablonow uzywaja `display: flex; flex-wrap: nowrap` zamiast `white-space: nowrap` (form-children byly blokowe). - [Phase 124, Plan 01] UNIFY zamkniety; `php bin/migrate.php` i real smoke wysylki SMS z szablonu zalezne od XAMPP MySQL online. +- [Phase 125, Plan 01] Bugfix #1089: shopPRO order z `firm_nip` ustawia teraz `invoice_requested=1` przy imporcie (mapper jako jedyne zrodlo heurystyki, sync service propaguje `aggregate['invoice_detected']`). +- [Phase 125, Plan 01] Allegro rozszerzenie `shouldRequestInvoice()`: detekcja `invoice.naturalPerson=false`, `invoice.address.taxId`, `invoice.companyName` (wczesniej tylko `invoice.required`). +- [Phase 125, Plan 01] Migracja `20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql` — idempotentna (information_schema guard); backfill 7 zamowien + DROP COLUMN `orders.is_invoice` (legacy z Phase 115). +- [Phase 125, Plan 01] Cleanup `is_invoice` z `OrderImportRepository` (INSERT/params/docstring) i `OrdersRepository` (SELECT/hydrate); usuniete `shouldRequestInvoice()` z `ShopproOrdersSyncService` (zastapione heurystyka mappera). ## Zmienione pliki @@ -35,3 +39,11 @@ - `.paul/STATE.md` - `.paul/phases/124-sms-templates/124-01-PLAN.md` - `.paul/phases/124-sms-templates/124-01-SUMMARY.md` +- `src/Modules/Settings/ShopproOrderMapper.php` +- `src/Modules/Settings/ShopproOrdersSyncService.php` +- `src/Modules/Settings/AllegroOrderImportService.php` +- `src/Modules/Orders/OrderImportRepository.php` +- `src/Modules/Orders/OrdersRepository.php` +- `database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql` +- `.paul/phases/125-invoice-requested-import-fix/125-01-PLAN.md` +- `.paul/phases/125-invoice-requested-import-fix/125-01-SUMMARY.md` diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index e61f934..34a0c22 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -258,9 +258,11 @@ tests/ - `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`. +### Auto-import flagi invoice_requested (zaktualizowane Phase 125-01) +- **shopPRO:** `ShopproOrderMapper::resolveInvoiceRequested($payload)` jest jedynym zrodlem heurystyki — sprawdza top-level klucze payloadu: `is_invoice`, `invoice.required`, `invoice` (bool), oraz obecnosc danych firmowych (`firm_name`/`firm_nip`/`invoice.company_name`/`invoice.tax_id`/`invoice.nip`/`company_name`/`tax_id`/`nip` etc.). Wynik eksponowany w `mapOrderAggregate()` jako top-level klucz `invoice_detected` (transient, nie pisany do DB). `ShopproOrdersSyncService::importOne` propaguje `!empty($aggregate['invoice_detected'])` do `setInvoiceRequested(true)` tylko przy `wasCreated=true`. Stara metoda `shouldRequestInvoice` w sync service usunieta (zastapiona heurystyka mappera). +- **Allegro:** `AllegroOrderImportService::shouldRequestInvoice($payload)` sprawdza w kolejnosci: `invoice.required` (truthy), `invoice.naturalPerson === false` (klient firmowy), `invoice.address.taxId` (NIP w adresie faktury), `invoice.companyName`/`invoice.address.company.name`. Wywolane tylko przy `wasCreated=true` w `importSingleOrder`. Rozszerzenie heurystyki naprawia luke gdy klient Allegro podaje NIP bez ustawiania `invoice.required=true`. +- **Kontrakt Phase 115/112 zachowany:** auto-set TYLKO przy `wasCreated=true`. Delta-only re-import nie nadpisuje manualnej flagi operatora (manualny toggle przez `/orders/{id}/invoice-requested/toggle` przezywa kolejne synchronizacje). +- **Legacy:** kolumna `orders.is_invoice` (Phase 115) usunieta migracja `20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql`. Backfill: 7 zamowien gdzie `is_invoice=1 AND invoice_requested=0` dostalo `invoice_requested=1` przed DROP COLUMN. ### View hierarchy - `accounting/invoice_form.php` — formularz wystawiania. diff --git a/.paul/codebase/db_schema.md b/.paul/codebase/db_schema.md index dc71f61..6f26f18 100644 --- a/.paul/codebase/db_schema.md +++ b/.paul/codebase/db_schema.md @@ -1,6 +1,6 @@ # Database Schema -**Updated:** 2026-05-12 | **Total tables:** 61 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci +**Updated:** 2026-05-13 | **Total tables:** 61 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci --- @@ -275,6 +275,8 @@ UNIQUE: `(integration_id, external_product_id, external_variant_id)` UNIQUE: `(integration_id, external_order_id)` > Note: Order notes are stored in the separate `order_notes` table (no `notes` column on `orders`). +> +> Note (Phase 125-01, 2026-05-13): `invoice_requested` (Phase 113-01) jest jedynym znacznikiem zadania faktury. Legacy kolumna `is_invoice` zostala usunieta w Phase 125-01 (migracja `20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql`), bo dryfowala wzgledem `invoice_requested` (mapper pisal do `is_invoice`, UI czytalo `invoice_requested` — bug #1089). **order_items** — Line items within orders | Column | Type | Nullable | Notes | diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md index 8ce23a5..6579b1e 100644 --- a/.paul/codebase/tech_changelog.md +++ b/.paul/codebase/tech_changelog.md @@ -1,5 +1,28 @@ # Technical Changelog +## 2026-05-13 - Phase 125 Plan 01: invoice_requested Import Fix + +**Co zrobiono:** +- Bugfix #1089: shopPRO order z `firm_nip` (bez kluczy z 5-elementowej listy `shouldRequestInvoice`) nie ustawial `invoice_requested=1` przy imporcie. Mapper wykrywal poprawnie (`is_invoice` przez heurystyke NIP), ale `ShopproOrdersSyncService::shouldRequestInvoice` mial wezsza liste kluczy -> UI w zakladce Platnosci wyswietlal odznaczony checkbox. +- `ShopproOrderMapper::mapOrderAggregate()` zwraca teraz top-level klucz `invoice_detected` (wynik `resolveInvoiceRequested($payload)`). Klucz `is_invoice` usuniety z tablicy `order` (nie odpowiada juz zadnej kolumnie DB). +- `ShopproOrdersSyncService::importOne()` propaguje `!empty($aggregate['invoice_detected'])` do `setInvoiceRequested(true)` zamiast wlasnej heurystyki. Stara metoda `shouldRequestInvoice` usunieta (zastapiona heurystyka mappera — zero duplikacji). +- `AllegroOrderImportService::shouldRequestInvoice($payload)` (nowa prywatna metoda) — rozszerza detekcje o `invoice.naturalPerson === false`, `invoice.address.taxId`, `invoice.companyName`/`invoice.address.company.name`. Wczesniej tylko `invoice.required` -> analogiczna luka jak shopPRO dla klientow Allegro z NIP bez `required=true`. +- Migracja `20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql`: + - Idempotentny guard przez `information_schema.COLUMNS` + prepared statements (DDL no-op gdy kolumna juz nie istnieje, pattern z Key Decision 2026-05-10). + - Backfill: `UPDATE orders SET invoice_requested=1 WHERE is_invoice=1 AND invoice_requested=0` (7 zamowien na produkcji, w tym #1089). + - DROP COLUMN `orders.is_invoice` (legacy z Phase 115, dryft wzgledem `invoice_requested`). +- `OrderImportRepository::insertOrder()` SQL — usuniety `is_invoice` z kolumn INSERT i `:is_invoice` z VALUES. `orderParams()` — usunieta linia mapowania. Docstring `updateOrderDelta()` (Phase 112) — usunieta wzmianka. +- `OrdersRepository` — usuniety `o.is_invoice` z SELECT (`paginate` query) i `transformOrderRow()` hydrate (klucz `is_invoice` nie wystepuje juz w zwracanych row'ach). + +**Dlaczego:** +- Bug #1089: zamowienie shopPRO z fakturowymi danymi firmowymi mialo `is_invoice=1` (mapper) ale `invoice_requested=0` (sync service). UI pokazywal odznaczony checkbox -> przycisk "Wystaw fakture" niedostepny -> operator musial recznie klikac toggle. +- Dryft: Phase 115 zostawila dwie kolumny dla tej samej semantyki (`is_invoice` legacy + `invoice_requested` nowy). Dwie sciezki detekcji (mapper vs. sync service) mialy rozna szerokosc heurystyki -> systematyczny rozjazd dla shopPRO orders z `firm_name`/`firm_nip`. +- Fix architekturalny: jedno zrodlo prawdy (`invoice_requested`), jedna heurystyka per zrodlo (mapper dla shopPRO, prywatna metoda dla Allegro). + +**BREAKING:** +- Kolumna `orders.is_invoice` przestaje istniec po migracji `20260513_000113_*`. Wewnetrzny kontrakt — nie wystepuje w API ani odpowiedziach JSON. Jezeli ktokolwiek (skrypt operatora, raport custom) czytal `is_invoice` z DB -> przelaczyc na `invoice_requested`. +- `ShopproOrderMapper::mapOrderAggregate()` zwraca teraz dodatkowy top-level klucz `invoice_detected` (boolean). Klucz `is_invoice` znika z podtablicy `order` (nie odpowiada juz kolumnie DB). + ## 2026-05-12 - Phase 124 Plan 01: SMS Templates **Co zrobiono:** diff --git a/.paul/phases/125-invoice-requested-import-fix/125-01-PLAN.md b/.paul/phases/125-invoice-requested-import-fix/125-01-PLAN.md new file mode 100644 index 0000000..b231a2a --- /dev/null +++ b/.paul/phases/125-invoice-requested-import-fix/125-01-PLAN.md @@ -0,0 +1,322 @@ +--- +phase: 125-invoice-requested-import-fix +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Modules/Settings/ShopproOrdersSyncService.php + - src/Modules/Settings/ShopproOrderMapper.php + - src/Modules/Settings/AllegroOrderImportService.php + - src/Modules/Orders/OrderImportRepository.php + - src/Modules/Orders/OrdersRepository.php + - database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql + - .paul/codebase/db_schema.md + - .paul/codebase/architecture.md + - .paul/codebase/tech_changelog.md +autonomous: true +delegation: off +--- + + +## Goal +Naprawić rozjazd między mapperem shopPRO a auto-set `orders.invoice_requested` przy imporcie, tak aby zamówienie z fakturą poprawnie pokazywało zaznaczony checkbox w zakładce Płatności i odblokowywało przycisk „Wystaw fakturę". Przy okazji usunąć duplikującą się kolumnę `orders.is_invoice` (legacy z Phase 115), bo to ona była źródłem dryftu między mapperem a sync service'em. + +## Purpose +- Bugfix #1089: shopPRO order z `firm_nip` nie ustawia `invoice_requested=1` (operator musi ręcznie klikać toggle). +- Analogiczna luka w Allegro: import nie wykrywa `invoice.address.taxId` bez `invoice.required=true`. +- Eliminacja struktury źródłowej buga: dwie kolumny dla tej samej semantyki (`is_invoice` ustawiany przez importer, `invoice_requested` czytany przez UI). Jedyne źródło prawdy → `invoice_requested`. + +## Output +- ShopproOrdersSyncService i AllegroOrderImportService używają tej samej heurystyki co mapper, propagując wynik z `$aggregate['order']['_invoice_detected']` (lub bezpośrednio z mappera) do `setInvoiceRequested()`. +- Migracja idempotentna: backfill `invoice_requested=1` dla zamówień gdzie poprzednio detekcja zadziałała na poziomie mappera, potem DROP COLUMN `orders.is_invoice`. +- Kod oczyszczony z odniesień do `is_invoice` (mapper, OrderImportRepository INSERT, OrdersRepository SELECT/hydrate). +- Dokumentacja zaktualizowana (`db_schema.md`, `architecture.md`, `tech_changelog.md`). + + + + +- **[Detekcja shopPRO]** — Jak szeroko rozszerzyć auto-detekcję faktury przy imporcie shopPRO? + → Odpowiedź: Propaguj wynik mappera (is_invoice → invoice_requested) — najprościej, zero duplikacji, mapper już sprawdza firm_name/firm_nip/invoice.required/is_invoice/invoice (top-level). +- **[Allegro fix]** — Czy przy okazji naprawić analogiczną lukę dla Allegro? + → Odpowiedź: Tak — rozszerz Allegro o detekcję NIP/firmy (`invoice.address.taxId`, `invoice.naturalPerson=false`, `invoice.companyName`). +- **[Backfill]** — Backfill istniejących zamówień: jak wykonać? + → Odpowiedź: Migracja SQL idempotentna (UPDATE … WHERE is_invoice=1 AND invoice_requested=0) — pipeline `php bin/migrate.php`, idempotentne (po backfillu DROP COLUMN i tak czyni operację jednorazową). +- **[is_invoice fate]** — Co zrobić z kolumną orders.is_invoice? + → Odpowiedź: Usunąć kolumnę w tej fazie — migracja DROP COLUMN po backfillu, usunięcie z `OrderImportRepository` (INSERT, params, docstring Phase 112), z `OrdersRepository` (SELECT, hydrate), z `ShopproOrderMapper` (zwraca flagę jako transient w aggregate'cie zamiast pisać do DB), z `AllegroOrderImportService` (analogicznie). + +**Stan w bazie produkcyjnej (2026-05-13):** +- 7 zamówień `is_invoice=1 AND invoice_requested=0` (w tym #1089, source=shoppro). +- Kolumna `orders.is_invoice TINYINT(1) NOT NULL DEFAULT 0` istnieje, nie ma indexu. +- `db_schema.md` (linia 244+) NIE wymienia kolumny `is_invoice` — dokumentacja była już niespójna z DB. + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/115-invoice-from-order/115-01-SUMMARY.md +# Phase 115 wprowadziła dwutorową detekcję: mapper → is_invoice, sync service → invoice_requested. +# Ta faza koryguje rozjazd przez propagację z mappera i eliminację is_invoice. + +## Source Files +@src/Modules/Settings/ShopproOrderMapper.php +@src/Modules/Settings/ShopproOrdersSyncService.php +@src/Modules/Settings/AllegroOrderImportService.php +@src/Modules/Orders/OrderImportRepository.php +@src/Modules/Orders/OrdersRepository.php +@.paul/codebase/db_schema.md +@.paul/codebase/architecture.md + + + + +## AC-1: Import shopPRO z firm_nip ustawia invoice_requested +```gherkin +Given zamówienie shopPRO z payloadem zawierającym `firm_name`+`firm_nip` (bez kluczy `wants_invoice`/`invoice_required`/`invoice.required`/`buyer.wants_invoice`/`buyer.invoice`) +When ShopproOrdersSyncService importuje to zamówienie jako nowe (wasCreated=true) +Then `orders.invoice_requested = 1` +And UI w zakładce Płatności (`/orders/{id}`) ma zaznaczony checkbox „Klient prosi o fakturę" +And przycisk „Wystaw fakturę" jest widoczny +``` + +## AC-2: Import Allegro z NIP ustawia invoice_requested +```gherkin +Given zamówienie Allegro z payloadem `invoice.address.taxId` lub `invoice.naturalPerson=false`, ale BEZ `invoice.required=true` +When AllegroOrderImportService importuje to zamówienie jako nowe (wasCreated=true) +Then `orders.invoice_requested = 1` +``` + +## AC-3: Re-import nie nadpisuje manualnego toggla +```gherkin +Given zamówienie z payload zawierającym firm_nip oraz manualnie ustawionym `invoice_requested=0` (operator odznaczył) +When importer re-importuje to zamówienie (wasCreated=false, delta-only) +Then `orders.invoice_requested` pozostaje `0` +And kontrakt Phase 115 (auto-set tylko przy `created=true`) jest zachowany +``` + +## AC-4: Backfill istniejących zamówień +```gherkin +Given migracja `20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql` zostaje uruchomiona +When `php bin/migrate.php` wykonuje ją na bazie gdzie 7 zamówień ma `is_invoice=1 AND invoice_requested=0` +Then `UPDATE orders SET invoice_requested=1 WHERE is_invoice=1 AND invoice_requested=0` wykonuje się PRZED `DROP COLUMN` +And po migracji `orders.is_invoice` nie istnieje +And dla każdego z 7 dotkniętych zamówień `invoice_requested=1` +And ponowne uruchomienie migracji jest no-op (kolumna już nie istnieje — `DROP COLUMN IF EXISTS` lub idempotentny SQL przez `information_schema` check) +``` + +## AC-5: Kolumna is_invoice nie istnieje w runtime +```gherkin +Given migracja została wykonana +When aplikacja importuje nowe zamówienie lub wyświetla szczegóły zamówienia +Then żaden SELECT/INSERT/UPDATE nie referencuje `orders.is_invoice` +And testy PHPUnit (`OrderImportRepositoryTest`) przechodzą bez modyfikacji asercji powiązanych z is_invoice (albo asercje zostały usunięte wraz z fixture) +And ręczny `SELECT id, invoice_requested FROM orders WHERE id=1089` zwraca `invoice_requested=1` +``` + + + + + + + Task 1: Mapper exposes detection result, importers propagate it + + src/Modules/Settings/ShopproOrderMapper.php, + src/Modules/Settings/ShopproOrdersSyncService.php, + src/Modules/Settings/AllegroOrderImportService.php + + + **ShopproOrderMapper.php:** + - Usuń klucz `'is_invoice'` z tablicy `$order` (linia 148). + - Zachowaj metodę `resolveInvoiceRequested(array $payload): bool` jako jedyne źródło heurystyki. + - W `mapOrderAggregate()` dodaj do zwracanego array klucz top-level `'invoice_detected' => $this->resolveInvoiceRequested($payload)` (poza `order`, na poziomie aggregate'u — analogicznie do `addresses`/`items`/`payments`). Powód: nie zaśmiecać kontraktu `order` (który mapuje 1:1 na kolumny tabeli) flagą transient'ową. + + **ShopproOrdersSyncService.php:** + - W bloku po `upsertOrderAggregate` (okolice linii 273-277) zamień warunek `if ($this->shouldRequestInvoice($rawOrder))` na `if (!empty($aggregate['invoice_detected']))`. + - USUŃ prywatną metodę `shouldRequestInvoice(array $rawOrder): bool` (linie 316-338) — zastąpiona heurystyką mappera. + - Zachowaj guard `wasCreated=true` (kontrakt Phase 115 z AC-3). + + **AllegroOrderImportService.php:** + - W bloku auto-set (linie 99-103) rozszerz warunek: `if (!empty($invoiceFlag['required']))` → wydziel do nowej prywatnej metody `private function shouldRequestInvoice(array $payload): bool` która sprawdza: + 1. `!empty($payload['invoice']['required'])` (istniejące zachowanie) + 2. `!empty($payload['invoice']['naturalPerson']) === false && isset($payload['invoice'])` → klient firmowy + 3. `!empty($payload['invoice']['address']['taxId'])` — NIP w adresie faktury + 4. `!empty($payload['invoice']['companyName'])` + - Zwracaj `true` gdy któryś z warunków spełniony. + - Wywołanie: `if ($wasCreated && $this->shouldRequestInvoice($payload)) { $this->ordersRepository->setInvoiceRequested($savedOrderId, true); }` + + Avoid: + - NIE zmieniać sygnatury `OrdersRepository::setInvoiceRequested()` ani `recordActivity()`. + - NIE dodawać kolumny `invoice_detected` do DB — to wartość transient w pamięci między mapperem a syncem. + + + - `php -l src/Modules/Settings/ShopproOrderMapper.php` (lint pass). + - `php -l src/Modules/Settings/ShopproOrdersSyncService.php`. + - `php -l src/Modules/Settings/AllegroOrderImportService.php`. + - Grep: `grep -n "is_invoice" src/Modules/Settings/` zwraca tylko pozostałości w komentarzach (jeśli są) — żadnych aktywnych odwołań. + - Grep: `grep -n "shouldRequestInvoice" src/Modules/Settings/ShopproOrdersSyncService.php` → 0 trafień. + + AC-1, AC-2, AC-3 satisfied: mapper jest jedynym źródłem detekcji shopPRO, Allegro ma rozszerzoną detekcję NIP/firmy, oba importery propagują flagę tylko przy created=true. + + + + Task 2: Migration — backfill invoice_requested + DROP COLUMN is_invoice + + database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql + + + Utwórz migrację z idempotentnym DDL+DML: + + ```sql + -- Phase 125-01: backfill invoice_requested z is_invoice + DROP is_invoice + -- Idempotentna: DML w bloku IF (kolumna istnieje), DROP w bloku IF (kolumna istnieje). + -- Pattern z `.paul` Key Decision (2026-05-10): migracje no-op zawsze jako DDL. + + SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'orders' + AND COLUMN_NAME = 'is_invoice' + ); + + SET @sql_backfill := IF(@col_exists > 0, + 'UPDATE orders SET invoice_requested = 1 WHERE is_invoice = 1 AND invoice_requested = 0', + 'ALTER TABLE orders COMMENT = ''phase-125 backfill no-op''' + ); + PREPARE stmt_backfill FROM @sql_backfill; + EXECUTE stmt_backfill; + DEALLOCATE PREPARE stmt_backfill; + + SET @sql_drop := IF(@col_exists > 0, + 'ALTER TABLE orders DROP COLUMN is_invoice', + 'ALTER TABLE orders COMMENT = ''phase-125 drop no-op''' + ); + PREPARE stmt_drop FROM @sql_drop; + EXECUTE stmt_drop; + DEALLOCATE PREPARE stmt_drop; + ``` + + Avoid: + - NIE używać `SELECT 1;` jako no-op (Key Decision 2026-05-10: powoduje SQLSTATE 2014 z PDO unbuffered). + - NIE używać `DROP COLUMN IF EXISTS` (MariaDB only — produkcja może być na czystym MySQL). + + + - Plik istnieje w `database/migrations/`. + - Składnia SQL waliduje: `mysql --help` test dry-run niemożliwy bez DB; alternatywa: `mysql -h … -e "$(cat migration.sql)" --dry-run` — XAMPP nie wspiera; zostawiamy weryfikację na operatora przy `php bin/migrate.php`. + - Po wykonaniu na bazie: `SELECT COUNT(*) FROM orders WHERE invoice_requested=1;` rośnie o 7 vs. pre-migration baseline. + - `SHOW COLUMNS FROM orders LIKE 'is_invoice';` → empty set. + - Re-run migracji → no-op (ALTER TABLE COMMENT). + + AC-4 satisfied: migracja idempotentna, backfill 7 zamówień przed DROP COLUMN, ponowne uruchomienie bezpieczne. + + + + Task 3: Remove is_invoice from PHP code (repository, hydrate) + + src/Modules/Orders/OrderImportRepository.php, + src/Modules/Orders/OrdersRepository.php + + + **OrderImportRepository.php:** + - W `insertOrder()` SQL (linie 161-175) usuń `is_invoice` z listy kolumn INSERT oraz `:is_invoice` z VALUES. + - W `orderParams()` (linia 258) usuń linię `'is_invoice' => !empty($orderData['is_invoice']) ? 1 : 0,`. + - W docstring `updateOrderDelta()` (linia 190) usuń wzmiankę o `is_invoice` z listy pól nie nadpisywanych przy delta-only (kolumna już nie istnieje, wzmianka jest dezinformująca). + + **OrdersRepository.php:** + - Linia 172: usuń `o.is_invoice,` z SELECT (`findById()` / pokrewne — zweryfikować, że to ten sam zapytanie). + - Linia 235: usuń `'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1,` z hydrate / mapowania zwracanego row'a. + - Jeśli kontroler/view referencjuje `$order['is_invoice']` — sprawdź `grep -rn "\\['is_invoice'\\]" src/ resources/views/` i usuń odwołania (lub zamień na `$order['invoice_requested']` jeśli kontekst tego wymaga). + + Avoid: + - NIE zmieniać kontraktu `OrderImportRepository::upsertOrderAggregate()` poza usunięciem klucza is_invoice (sygnatura, return value bez zmian). + - NIE dotykać `payment_status` ani `total_paid` (chronione przez Phase 119). + + + - `php -l src/Modules/Orders/OrderImportRepository.php`. + - `php -l src/Modules/Orders/OrdersRepository.php`. + - `grep -rn "is_invoice" src/ resources/views/` → 0 trafień (poza ewentualnymi historycznymi komentarzami w `.paul/`). + - Manual: po wdrożeniu importu zamówienie #1089 ma `invoice_requested=1` po re-imporcie (delta-only nie nadpisze, ale przed migracją można też ręcznie ustawić toggle aby zweryfikować że runtime nie próbuje pisać do is_invoice). + + AC-5 satisfied: żaden runtime SELECT/INSERT nie odwołuje się do `is_invoice`; aplikacja działa po DROP COLUMN. + + + + Task 4: Update docs (db_schema, architecture, tech_changelog) + + .paul/codebase/db_schema.md, + .paul/codebase/architecture.md, + .paul/codebase/tech_changelog.md + + + **db_schema.md:** + - Sekcja `orders` (linia 244+) — pozostaje bez `is_invoice` (i tak już nieobecna, tylko potwierdzenie spójności). + - W komentarzu pod tabelą `orders` dodaj wzmiankę: „`invoice_requested` (Phase 113-01) jest jedynym znacznikiem żądania faktury. Legacy `is_invoice` usunięte w Phase 125-01." + - Phase footer: zaktualizuj `Updated: 2026-05-13` i `Total tables: 61` (bez zmian; kolumna usunięta to nie tabela). + + **architecture.md:** + - W sekcji „### Auto-import flagi invoice_requested" (linia 261+) zaktualizuj opis: + - shopPRO: `ShopproOrderMapper::resolveInvoiceRequested()` jest jedynym źródłem heurystyki; sprawdza: `is_invoice` w payload (top-level z legacy źródeł zewnętrznych — payload, nie kolumna), `invoice.required`, top-level `invoice` jako bool, `firm_name`/`firm_nip`/`invoice.company_name`/`invoice.tax_id`. Sync service propaguje wynik z `$aggregate['invoice_detected']`. + - Allegro: `AllegroOrderImportService::shouldRequestInvoice($payload)` sprawdza `invoice.required`, `invoice.naturalPerson=false`, `invoice.address.taxId`, `invoice.companyName`. Tylko przy `wasCreated=true`. + + **tech_changelog.md:** + - Dodaj wpis dla Phase 125-01: + ``` + ## Phase 125-01 — invoice_requested import fix (2026-05-13) + - Bug: zamówienia shopPRO z `firm_nip` (bez `invoice.required`) nie ustawiały `invoice_requested=1` (mapper wykrywał, sync service nie). + - Fix: ShopproOrderMapper eksponuje `invoice_detected` w aggregate. ShopproOrdersSyncService propaguje zamiast duplikować heurystykę. Usunięta metoda `shouldRequestInvoice` ze sync service. + - Allegro: rozszerzenie detekcji o `invoice.naturalPerson=false`, `invoice.address.taxId`, `invoice.companyName`. + - Migracja 20260513_000113 — backfill 7 zamówień + DROP COLUMN `orders.is_invoice` (legacy z Phase 115, dryft względem `invoice_requested`). + - Files: ShopproOrderMapper.php, ShopproOrdersSyncService.php, AllegroOrderImportService.php, OrderImportRepository.php, OrdersRepository.php. + ``` + + + - Grep `grep -n "is_invoice" .paul/codebase/db_schema.md .paul/codebase/architecture.md` — tylko historyczne wzmianki w kontekście Phase 125. + - Sekcja Phase 125 istnieje w `tech_changelog.md`. + + Dokumentacja spójna z kodem; przyszli czytelnicy widzą że `invoice_requested` to jedyne źródło prawdy. + + + + + + +## DO NOT CHANGE +- `orders.invoice_requested` schema (Phase 113-01 zostaje, index `idx_orders_invoice_requested` zostaje). +- `OrdersRepository::setInvoiceRequested()` sygnatura i `recordActivity('invoice_requested_changed')` event type. +- Kontrakt Phase 115: auto-set tylko przy `wasCreated=true`, delta-only re-import nie nadpisuje manualnej flagi. +- Kontrakt Phase 119: `updateOrderDelta()` zachowuje ochronę `total_paid` i `is_canceled_by_buyer` — ta faza nie dotyka logiki delta. +- Kontrakt Phase 112: `replaceAddresses/replaceItems/replaceNotes` wywoływane tylko przy `created=true`. +- `payload_json` — nie filtrujemy, surowy payload zostaje (mapper-tylko detekcja). +- Allegro `invoice.required` jako warunek wystarczający — pozostaje (rozszerzenie, nie zamiana). + +## SCOPE LIMITS +- NIE robimy nowego eventu automatyzacji `invoice.created` (zostaje na future plan zgodnie z Key Decision 2026-05-10). +- NIE dotykamy INVOICE-IDEMP-115 (double-POST Fakturownia) — to osobny todo. +- NIE refaktorujemy `OrderImportRepository::orderParams()` poza usunięciem klucza is_invoice (tylko minimalna delta). +- NIE migrujemy stałych Allegro do wspólnego helpera — `shouldRequestInvoice` zostaje prywatną metodą per importer (shopPRO ma to w mapperze, Allegro nie ma analogicznego mappera klasowego; ujednolicenie poza zakresem). +- NIE dodajemy testów PHPUnit dla detekcji shopPRO/Allegro (możliwe w follow-up; weryfikacja przez ręczny smoke test operatora na zamówieniu #1089). + + + + +Przed zamknięciem planu: +- [ ] `php -l` na 5 zmodyfikowanych plikach PHP — bez błędów składni. +- [ ] `grep -rn "is_invoice" src/` zwraca 0 trafień w aktywnym kodzie. +- [ ] Migracja przygotowana, idempotentna (`@col_exists` guard + ALTER TABLE COMMENT no-op). +- [ ] Dokumentacja zaktualizowana (db_schema, architecture, tech_changelog). +- [ ] STATE.md pokazuje pending action: operator uruchamia `php bin/migrate.php` + ręczna weryfikacja na #1089. + + + +- 5 plików PHP zmodyfikowanych zgodnie z zadaniami 1+3. +- Nowa migracja idempotentna. +- 3 pliki dokumentacji zaktualizowane. +- AC-1..AC-5 spełnione (manualna weryfikacja AC-1/AC-4 przez operatora na żywej bazie po uruchomieniu migracji). +- Order #1089 po backfillu ma `invoice_requested=1` i checkbox w UI zaznaczony. + + + +Po zakończeniu utworzyć `.paul/phases/125-invoice-requested-import-fix/125-01-SUMMARY.md`. + diff --git a/.paul/phases/125-invoice-requested-import-fix/125-01-SUMMARY.md b/.paul/phases/125-invoice-requested-import-fix/125-01-SUMMARY.md new file mode 100644 index 0000000..00a1479 --- /dev/null +++ b/.paul/phases/125-invoice-requested-import-fix/125-01-SUMMARY.md @@ -0,0 +1,167 @@ +--- +phase: 125-invoice-requested-import-fix +plan: 01 +subsystem: orders-import +tags: [shoppro, allegro, invoices, import, migration, fakturownia] + +requires: + - phase: 115-invoice-from-order + provides: orders.invoice_requested column + setInvoiceRequested API + UI toggle + - phase: 113-fakturownia-integration-foundation + provides: invoice_requested schema baseline + +provides: + - Bugfix shopPRO/Allegro: invoice_requested auto-set ujednolicony z heurystyka mappera + - DROP COLUMN orders.is_invoice (legacy z Phase 115) + backfill 7 zamowien + - Wspolny kontrakt detekcji: ShopproOrderMapper.invoice_detected (aggregate top-level) + AllegroOrderImportService::shouldRequestInvoice() + +affects: [future invoice automation, invoice.created event, fakturownia idempotency] + +tech-stack: + added: [] + patterns: + - "Mapper jako jedyne zrodlo heurystyki detekcji (sync service propaguje, nie duplikuje)" + - "Migracje DROP COLUMN idempotentne przez information_schema.COLUMNS guard + prepared statement" + +key-files: + created: + - database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql + modified: + - src/Modules/Settings/ShopproOrderMapper.php + - src/Modules/Settings/ShopproOrdersSyncService.php + - src/Modules/Settings/AllegroOrderImportService.php + - src/Modules/Orders/OrderImportRepository.php + - src/Modules/Orders/OrdersRepository.php + - .paul/codebase/db_schema.md + - .paul/codebase/architecture.md + - .paul/codebase/tech_changelog.md + +key-decisions: + - "Mapper jako jedyne zrodlo heurystyki shopPRO — sync service propaguje aggregate['invoice_detected'] zamiast wlasnej listy kluczy" + - "Allegro shouldRequestInvoice rozszerzony o invoice.naturalPerson=false / invoice.address.taxId / invoice.companyName" + - "DROP COLUMN orders.is_invoice w tej samej migracji co backfill (eliminacja dryftu architekturalnego, nie tylko fixu objawowego)" + - "Migracja idempotentna przez information_schema guard zamiast DROP COLUMN IF EXISTS (portable MySQL/MariaDB)" + +patterns-established: + - "Top-level klucz 'invoice_detected' w mapOrderAggregate() — pattern dla transient flag detection (nie pisanej do DB ale konsumowanej przez sync service)" + - "Idempotentny DROP COLUMN: SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS...) + PREPARE/EXECUTE z fallbackiem ALTER TABLE COMMENT no-op" + +duration: ~30min +started: 2026-05-13T00:45:00Z +completed: 2026-05-13T01:15:00Z +--- + +# Phase 125 Plan 01: invoice_requested Import Fix Summary + +**shopPRO order z `firm_nip` (bez explicit invoice.required flagi) ustawia teraz `invoice_requested=1` przy imporcie; Allegro rozszerzony o detekcje NIP/naturalPerson/companyName; legacy `orders.is_invoice` usunieta z DB i kodu (eliminacja dryftu architekturalnego z Phase 115).** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~30 min | +| Started | 2026-05-13T00:45:00Z | +| Completed | 2026-05-13T01:15:00Z | +| Tasks | 4/4 completed | +| Files modified | 8 (5 PHP + 1 SQL + 3 docs) | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Import shopPRO z firm_nip ustawia invoice_requested | Pass (kod) / Pending UAT | Kod: `ShopproOrdersSyncService::importOne` propaguje `aggregate['invoice_detected']`; mapper heurystyka obejmuje firm_name/firm_nip. UAT: smoke test po migracji. | +| AC-2: Import Allegro z NIP ustawia invoice_requested | Pass (kod) / Pending UAT | `AllegroOrderImportService::shouldRequestInvoice` sprawdza required/naturalPerson=false/taxId/companyName. UAT: brak aktywnego zamowienia Allegro z samym NIP do potwierdzenia w tej sesji. | +| AC-3: Re-import nie nadpisuje manualnego toggla | Pass (kod) | Guard `wasCreated=true` zachowany w obu importerach (kontrakt Phase 115/112). | +| AC-4: Backfill istniejących zamówień | Pass (migracja gotowa) / Pending operator | Migracja idempotentna; 7 zamowien czeka na `php bin/migrate.php` (operator + XAMPP MySQL online). | +| AC-5: Kolumna is_invoice nie istnieje w runtime | Pass (kod, czeka migracji) | Grep `is_invoice` w `src/`: tylko 1 trafienie (ShopproOrderMapper:388 — klucz payloadu, nie kolumna DB). Aplikacja nie referencuje juz kolumny `orders.is_invoice` — gotowa na DROP COLUMN. | + +## Accomplishments + +- **Bugfix #1089**: shopPRO order z `firm_nip` (klient firmowy bez `wants_invoice`/`invoice_required`/`invoice.required` flag) ustawia teraz `invoice_requested=1` przy pierwszym imporcie. UI w zakladce Platnosci pokazuje zaznaczony checkbox, przycisk "Wystaw fakture" jest dostepny. +- **Fix architekturalny**: dwie kolumny dla tej samej semantyki (`is_invoice` + `invoice_requested`) i dwie sciezki detekcji (mapper + sync service) ujednolicone do jednego zrodla prawdy. Mapper jest jedyna heurystyka, sync service propaguje wynik. +- **Allegro symmetric fix**: detekcja faktury rozszerzona o `invoice.naturalPerson=false`, `invoice.address.taxId`, `invoice.companyName`/`invoice.address.company.name` (wczesniej tylko `invoice.required`). +- **Idempotentna migracja**: pattern `information_schema.COLUMNS` + `PREPARE/EXECUTE` z fallbackiem `ALTER TABLE COMMENT` no-op — portable na czysty MySQL (nie wymaga `DROP COLUMN IF EXISTS` MariaDB-only). + +## Task Commits + +Atomic commits zaplanowane na transition (delegation: off, inline execution; brak commitow w trakcie APPLY). Wszystkie zmiany w roboczym tree, gotowe do `feat(125): invoice_requested import fix` commit. + +| Task | Status | Description | +|------|--------|-------------| +| Task 1: Mapper exposes detection, importers propagate | Done | ShopproOrderMapper.invoice_detected aggregate key + propagacja w sync service + Allegro shouldRequestInvoice | +| Task 2: Migration backfill + DROP COLUMN | Done | 20260513_000113 idempotent SQL | +| Task 3: Remove is_invoice from PHP | Done | OrderImportRepository INSERT + OrdersRepository SELECT/hydrate | +| Task 4: Update docs | Done | db_schema.md note + architecture.md sekcja invoice_requested + tech_changelog.md wpis | + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `src/Modules/Settings/ShopproOrderMapper.php` | Modified | Usuniety klucz `is_invoice` z `order`; dodany top-level `invoice_detected` w `mapOrderAggregate()` | +| `src/Modules/Settings/ShopproOrdersSyncService.php` | Modified | `shouldRequestInvoice()` usunieta; propagacja `!empty($aggregate['invoice_detected'])` w `importOne()` | +| `src/Modules/Settings/AllegroOrderImportService.php` | Modified | `shouldRequestInvoice($payload)` (nowa) z 4 wzorcami heurystyki; mapping `is_invoice` w `mapCheckoutFormPayload()` usuniety | +| `src/Modules/Orders/OrderImportRepository.php` | Modified | Usuniety `is_invoice` z INSERT (kolumny + values + orderParams); docstring `updateOrderDelta()` (Phase 112) zaktualizowany | +| `src/Modules/Orders/OrdersRepository.php` | Modified | Usuniety `o.is_invoice` z SELECT (`paginate` query); usuniety `is_invoice` klucz z `transformOrderRow()` hydrate | +| `database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql` | Created | Idempotentny backfill + DROP COLUMN przez information_schema guard | +| `.paul/codebase/db_schema.md` | Modified | Updated date + note pod tabela `orders` o usunieciu `is_invoice` | +| `.paul/codebase/architecture.md` | Modified | Sekcja "Auto-import flagi invoice_requested" rozszerzona o Phase 125 zmiany (shopPRO mapper-driven + Allegro heurystyka NIP) | +| `.paul/codebase/tech_changelog.md` | Modified | Prepend wpis Phase 125-01 z Co/Dlaczego/BREAKING | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Mapper jako jedyne zrodlo heurystyki (top-level `invoice_detected` w aggregate) | Eliminuje dryft sync service vs. mapper. Sync service propaguje, nie duplikuje listy kluczy | Pattern dla przyszlych detection-flag (gdy mapper juz robi heurystyka, sync service czyta wynik) | +| DROP COLUMN `is_invoice` razem z fixem detekcji | Korzen buga to dwie kolumny dla tej samej semantyki. Naprawa objawu (UI sync) bez usuwania struktury zostawia dryftowy potential dla kolejnych fix'ow | Cleaner architecture, +1 BREAKING (legacy column gone) | +| Migracja idempotentna przez `information_schema.COLUMNS` guard | `DROP COLUMN IF EXISTS` to MariaDB only; portable MySQL pattern + idempotencja dla re-runow | Pattern dla przyszlych migracji DROP COLUMN | +| Allegro shouldRequestInvoice z 4 wzorcami (required / naturalPerson / taxId / companyName) | Allegro miala analogiczna luke: klient firmowy z NIP ale bez `required=true` (np. checkout-form gdzie NIP zapisany w invoice.address ale invoice.required niewymaganie zaznaczony) | Symmetric fix shopPRO + Allegro w jednej fazie zamiast osobnego ticketu | +| AllegroOrderImportService: usuniecie `is_invoice` z `mapCheckoutFormPayload()` poza scope Task 1 | Bez tego INSERT by sie wywalil po migracji (kolumna znika) — wymuszone przez DROP COLUMN, zalogowane w deviations | Scope addition wymuszony konsekwencja DROP COLUMN; bez ryzyka | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 1 | Niezbedny side-effect DROP COLUMN | +| Scope additions | 0 | — | +| Deferred | 0 | — | + +**Total impact:** Niezbedna korekta w AllegroOrderImportService nieuwzgledniona explicite w PLAN.md Task 1; logiczny side-effect DROP COLUMN. + +### Auto-fixed Issues + +**1. AllegroOrderImportService::mapCheckoutFormPayload: usuniecie `'is_invoice' => !empty($invoice['required'])` (linia 235)** +- **Found during:** Task 1 (inspekcja AllegroOrderImportService przy dodawaniu `shouldRequestInvoice`) +- **Issue:** PLAN Task 1 opisuje tylko zmiany w `importSingleOrder` (linie 99-103) dla Allegro. PLAN Task 3 dotyczy `OrderImportRepository`/`OrdersRepository`. Mapping `is_invoice` w Allegro pozostalby pisany do `$mapped['order']`, ktore potem `OrderImportRepository::insertOrder` wstawia do DB — po DROP COLUMN to powodowaloby SQL error. +- **Fix:** Usuniety klucz `is_invoice` z mapowania w `mapCheckoutFormPayload()` (linia 235). +- **Files:** `src/Modules/Settings/AllegroOrderImportService.php` +- **Verification:** `grep is_invoice src/Modules/Settings/AllegroOrderImportService.php` → 0 trafien po edit; lint pass. +- **Commit:** w zakresie wspolnego commitu phase-125. + +### Deferred Items + +Brak — plan wykonany pelnie. + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Allegro `is_invoice` mapping w mapCheckoutFormPayload poza explicite scope PLAN | Self-fix w Task 1 (logiczny side-effect DROP COLUMN); zalogowane w Deviations.Auto-fixed | + +## Next Phase Readiness + +**Ready:** +- Bugfix #1089 wdrozony w kodzie; gotowe do smoke testu po migracji. +- Allegro symmetric fix — eliminacja analogicznej luki bez czekania na osobny ticket. +- Migracja idempotentna — pattern do reuse w przyszlych DROP COLUMN. + +**Concerns:** +- AC-1/AC-2 wymagaja UAT na zywej bazie po `php bin/migrate.php` — operator musi potwierdzic na shopPRO 1089 i nowym Allegro zamowieniu z NIP. +- Brak PHPUnit testow dla `shouldRequestInvoice` (Allegro) i `invoice_detected` (shopPRO mapper). Boundary explicit; mozliwe follow-up plan. + +**Blockers:** None. + +--- +*Phase: 125-invoice-requested-import-fix, Plan: 01* +*Completed: 2026-05-13* diff --git a/database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql b/database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql new file mode 100644 index 0000000..17fca50 --- /dev/null +++ b/database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql @@ -0,0 +1,27 @@ +-- Phase 125-01: backfill invoice_requested z is_invoice + DROP COLUMN is_invoice +-- Idempotentna: guard przez information_schema.COLUMNS; no-op po pierwszym uruchomieniu. +-- Pattern z Key Decision 2026-05-10: migracje no-op zawsze jako DDL (ALTER TABLE COMMENT), +-- nigdy SELECT 1; (PDO unbuffered + result set -> SQLSTATE 2014). + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'orders' + AND COLUMN_NAME = 'is_invoice' +); + +SET @sql_backfill := IF(@col_exists > 0, + 'UPDATE orders SET invoice_requested = 1 WHERE is_invoice = 1 AND invoice_requested = 0', + 'ALTER TABLE orders COMMENT = ''phase-125 backfill no-op''' +); +PREPARE stmt_backfill FROM @sql_backfill; +EXECUTE stmt_backfill; +DEALLOCATE PREPARE stmt_backfill; + +SET @sql_drop := IF(@col_exists > 0, + 'ALTER TABLE orders DROP COLUMN is_invoice', + 'ALTER TABLE orders COMMENT = ''phase-125 drop no-op''' +); +PREPARE stmt_drop FROM @sql_drop; +EXECUTE stmt_drop; +DEALLOCATE PREPARE stmt_drop; diff --git a/src/Modules/Orders/OrderImportRepository.php b/src/Modules/Orders/OrderImportRepository.php index 219297a..32e14b8 100644 --- a/src/Modules/Orders/OrderImportRepository.php +++ b/src/Modules/Orders/OrderImportRepository.php @@ -162,13 +162,13 @@ final class OrderImportRepository 'INSERT INTO orders ( integration_id, source, source_order_id, external_order_id, external_platform_id, external_platform_account_id, status_code, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id, - customer_login, is_invoice, is_encrypted, is_canceled_by_buyer, currency, + customer_login, is_encrypted, is_canceled_by_buyer, currency, total_without_tax, total_with_tax, total_paid, delivery_price, send_date_min, send_date_max, ordered_at, source_created_at, source_updated_at, preferences_json, payload_json, fetched_at ) VALUES ( :integration_id, :source, :source_order_id, :external_order_id, :external_platform_id, :external_platform_account_id, :status_code, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id, - :customer_login, :is_invoice, :is_encrypted, :is_canceled_by_buyer, :currency, + :customer_login, :is_encrypted, :is_canceled_by_buyer, :currency, :total_without_tax, :total_with_tax, :total_paid, :delivery_price, :send_date_min, :send_date_max, :ordered_at, :source_created_at, :source_updated_at, :preferences_json, :payload_json, :fetched_at )' @@ -187,7 +187,7 @@ final class OrderImportRepository * Phase 112-01: Delta-only update for re-import. Touches only fields that legitimately * change at the source between syncs. All other order columns (integration_id, source, * external_*, customer_login, currency, totals other than total_paid, delivery_price, - * send_date_*, ordered_at, source_created_at, preferences_json, is_invoice, is_encrypted, + * send_date_*, ordered_at, source_created_at, preferences_json, is_encrypted, * external_carrier_*, external_payment_type_id) are NOT overwritten on re-import. * * Phase 119-01: When `payment_status` is unchanged between DB and source payload, @@ -255,7 +255,6 @@ final class OrderImportRepository 'external_carrier_id' => $orderData['external_carrier_id'] ?? null, 'external_carrier_account_id' => $orderData['external_carrier_account_id'] ?? null, 'customer_login' => $orderData['customer_login'] ?? null, - 'is_invoice' => !empty($orderData['is_invoice']) ? 1 : 0, 'is_encrypted' => !empty($orderData['is_encrypted']) ? 1 : 0, 'is_canceled_by_buyer' => !empty($orderData['is_canceled_by_buyer']) ? 1 : 0, 'currency' => (string) ($orderData['currency'] ?? 'PLN'), diff --git a/src/Modules/Orders/OrdersRepository.php b/src/Modules/Orders/OrdersRepository.php index b35f9a6..28aafe2 100644 --- a/src/Modules/Orders/OrdersRepository.php +++ b/src/Modules/Orders/OrdersRepository.php @@ -169,7 +169,6 @@ final class OrdersRepository o.source_updated_at, o.fetched_at, ' . $effectiveOrderedAtSql . ' AS effective_ordered_at, - o.is_invoice, o.is_canceled_by_buyer, a.name AS buyer_name, a.email AS buyer_email, @@ -232,7 +231,6 @@ final class OrdersRepository 'source_created_at' => (string) ($row['source_created_at'] ?? ''), 'source_updated_at' => (string) ($row['source_updated_at'] ?? ''), 'fetched_at' => (string) ($row['fetched_at'] ?? ''), - 'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1, 'is_canceled_by_buyer' => (int) ($row['is_canceled_by_buyer'] ?? 0) === 1, 'external_carrier_id' => (string) ($row['external_carrier_id'] ?? ''), 'external_payment_type_id' => (string) ($row['external_payment_type_id'] ?? ''), diff --git a/src/Modules/Settings/AllegroOrderImportService.php b/src/Modules/Settings/AllegroOrderImportService.php index 5419d28..89aed39 100644 --- a/src/Modules/Settings/AllegroOrderImportService.php +++ b/src/Modules/Settings/AllegroOrderImportService.php @@ -96,11 +96,8 @@ final class AllegroOrderImportService ); } - if ($wasCreated) { - $invoiceFlag = is_array($payload['invoice'] ?? null) ? $payload['invoice'] : []; - if (!empty($invoiceFlag['required'])) { - $this->ordersRepository->setInvoiceRequested($savedOrderId, true); - } + if ($wasCreated && $this->shouldRequestInvoice($payload)) { + $this->ordersRepository->setInvoiceRequested($savedOrderId, true); } if ($wasCreated && $this->automationService !== null) { @@ -141,6 +138,37 @@ final class AllegroOrderImportService return $value; } + /** + * Detect "klient prosi o fakture" flag from Allegro checkout-form payload. + * Triggers on explicit `invoice.required`, business buyer (`naturalPerson=false`), + * NIP in invoice address, or explicit company name. + * + * @param array $payload + */ + private function shouldRequestInvoice(array $payload): bool + { + $invoice = is_array($payload['invoice'] ?? null) ? $payload['invoice'] : null; + if ($invoice === null) { + return false; + } + + if (!empty($invoice['required'])) { + return true; + } + + if (array_key_exists('naturalPerson', $invoice) && $invoice['naturalPerson'] === false) { + return true; + } + + $address = is_array($invoice['address'] ?? null) ? $invoice['address'] : []; + if (!empty($address['taxId'])) { + return true; + } + + $companyName = trim((string) ($invoice['companyName'] ?? $address['company']['name'] ?? '')); + return $companyName !== ''; + } + /** * @param array $payload * @return array{ @@ -232,7 +260,6 @@ final class AllegroOrderImportService 'external_carrier_id' => $deliveryForm !== '' ? $deliveryForm : null, 'external_carrier_account_id' => $deliveryMethodId !== '' ? $deliveryMethodId : null, 'customer_login' => trim((string) ($buyer['login'] ?? '')), - 'is_invoice' => !empty($invoice['required']), 'is_encrypted' => false, 'is_canceled_by_buyer' => in_array($externalStatus, ['cancelled', 'canceled'], true), 'currency' => strtoupper($currency), diff --git a/src/Modules/Settings/ShopproOrderMapper.php b/src/Modules/Settings/ShopproOrderMapper.php index e8547a6..179d824 100644 --- a/src/Modules/Settings/ShopproOrderMapper.php +++ b/src/Modules/Settings/ShopproOrderMapper.php @@ -145,7 +145,6 @@ final class ShopproOrderMapper 'customer_login' => StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login', ])), - 'is_invoice' => $this->resolveInvoiceRequested($payload), 'is_encrypted' => false, 'is_canceled_by_buyer' => false, 'currency' => $currency, @@ -185,6 +184,7 @@ final class ShopproOrderMapper 'shipments' => $shipments, 'notes' => $notes, 'status_history' => $statusHistory, + 'invoice_detected' => $this->resolveInvoiceRequested($payload), ]; } diff --git a/src/Modules/Settings/ShopproOrdersSyncService.php b/src/Modules/Settings/ShopproOrdersSyncService.php index edd1b97..c36de2f 100644 --- a/src/Modules/Settings/ShopproOrdersSyncService.php +++ b/src/Modules/Settings/ShopproOrdersSyncService.php @@ -270,10 +270,8 @@ final class ShopproOrdersSyncService ); } - if ($savedOrderId > 0 && $wasCreated) { - if ($this->shouldRequestInvoice($rawOrder)) { - $this->orders->setInvoiceRequested($savedOrderId, true); - } + if ($savedOrderId > 0 && $wasCreated && !empty($aggregate['invoice_detected'])) { + $this->orders->setInvoiceRequested($savedOrderId, true); } if ($savedOrderId > 0 && $wasCreated && !$wasPaymentTransition && $this->automationService !== null) { @@ -307,36 +305,6 @@ final class ShopproOrdersSyncService } } - /** - * Detect "klient prosi o fakture" flag from shopPRO raw payload. - * Tries common keys; returns false when none present (manual toggle still possible). - * - * @param array $rawOrder - */ - private function shouldRequestInvoice(array $rawOrder): bool - { - foreach ([['wants_invoice'], ['invoice_required'], ['invoice', 'required'], ['buyer', 'wants_invoice'], ['buyer', 'invoice']] as $path) { - $value = $rawOrder; - $found = true; - foreach ($path as $key) { - if (!is_array($value) || !array_key_exists($key, $value)) { - $found = false; - break; - } - $value = $value[$key]; - } - if ($found && ( - $value === true - || $value === 1 - || $value === '1' - || (is_string($value) && in_array(strtolower($value), ['true', 'yes', 'tak'], true)) - )) { - return true; - } - } - return false; - } - /** * @param mixed $rawIds * @return array