feat(125): invoice_requested import fix + drop legacy is_invoice column
- shopPRO: ShopproOrderMapper jako jedyne zrodlo heurystyki detekcji faktury; mapOrderAggregate() zwraca top-level invoice_detected (transient). - ShopproOrdersSyncService: usunieta wlasna shouldRequestInvoice(); propagacja aggregate['invoice_detected'] do setInvoiceRequested() tylko przy created=true. - Allegro: nowa shouldRequestInvoice(payload) z 4 wzorcami (invoice.required, naturalPerson=false, address.taxId, companyName/address.company.name). Wczesniej tylko invoice.required -> analogiczna luka jak shopPRO. - Migracja 20260513_000113: idempotentny backfill (UPDATE invoice_requested=1 WHERE is_invoice=1 AND invoice_requested=0) + DROP COLUMN orders.is_invoice. Guard przez information_schema.COLUMNS + PREPARE/EXECUTE z ALTER TABLE COMMENT no-op fallbackiem (portable MySQL/MariaDB). - Cleanup is_invoice z OrderImportRepository (INSERT cols/values/params, docstring Phase 112) i OrdersRepository (paginate SELECT, transformOrderRow hydrate). AllegroOrderImportService mapping w mapCheckoutFormPayload tez usuniety (wymuszone konsekwencja DROP COLUMN). - Bugfix #1089: zamowienie shopPRO z firm_nip (bez wants_invoice/invoice.required) ustawia teraz invoice_requested=1 -> UI w zakladce Platnosci zaznacza checkbox, przycisk "Wystaw fakture" widoczny. Pending operator: php bin/migrate.php (XAMPP MySQL online) -> backfill 7 zamowien. Smoke test: re-import shopPRO + nowe Allegro z NIP.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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:**
|
||||
|
||||
322
.paul/phases/125-invoice-requested-import-fix/125-01-PLAN.md
Normal file
322
.paul/phases/125-invoice-requested-import-fix/125-01-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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`).
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
<clarifications>
|
||||
- **[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.
|
||||
</clarifications>
|
||||
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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`
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Mapper exposes detection result, importers propagate it</name>
|
||||
<files>
|
||||
src/Modules/Settings/ShopproOrderMapper.php,
|
||||
src/Modules/Settings/ShopproOrdersSyncService.php,
|
||||
src/Modules/Settings/AllegroOrderImportService.php
|
||||
</files>
|
||||
<action>
|
||||
**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.
|
||||
</action>
|
||||
<verify>
|
||||
- `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ń.
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Migration — backfill invoice_requested + DROP COLUMN is_invoice</name>
|
||||
<files>
|
||||
database/migrations/20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql
|
||||
</files>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
- 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).
|
||||
</verify>
|
||||
<done>AC-4 satisfied: migracja idempotentna, backfill 7 zamówień przed DROP COLUMN, ponowne uruchomienie bezpieczne.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Remove is_invoice from PHP code (repository, hydrate)</name>
|
||||
<files>
|
||||
src/Modules/Orders/OrderImportRepository.php,
|
||||
src/Modules/Orders/OrdersRepository.php
|
||||
</files>
|
||||
<action>
|
||||
**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).
|
||||
</action>
|
||||
<verify>
|
||||
- `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).
|
||||
</verify>
|
||||
<done>AC-5 satisfied: żaden runtime SELECT/INSERT nie odwołuje się do `is_invoice`; aplikacja działa po DROP COLUMN.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Update docs (db_schema, architecture, tech_changelog)</name>
|
||||
<files>
|
||||
.paul/codebase/db_schema.md,
|
||||
.paul/codebase/architecture.md,
|
||||
.paul/codebase/tech_changelog.md
|
||||
</files>
|
||||
<action>
|
||||
**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.
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
- 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`.
|
||||
</verify>
|
||||
<done>Dokumentacja spójna z kodem; przyszli czytelnicy widzą że `invoice_requested` to jedyne źródło prawdy.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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).
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Po zakończeniu utworzyć `.paul/phases/125-invoice-requested-import-fix/125-01-SUMMARY.md`.
|
||||
</output>
|
||||
167
.paul/phases/125-invoice-requested-import-fix/125-01-SUMMARY.md
Normal file
167
.paul/phases/125-invoice-requested-import-fix/125-01-SUMMARY.md
Normal file
@@ -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*
|
||||
@@ -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;
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'] ?? ''),
|
||||
|
||||
@@ -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<string, mixed> $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<string, mixed> $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),
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, mixed> $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<int, true>
|
||||
|
||||
Reference in New Issue
Block a user