--- phase: 06-xml-feed-import plan: 01 subsystem: products tags: [xml-feed, gmc, products, schema-rename, cron, supplemental-feed] requires: - phase: 02-supplemental-feed-cl1 provides: SupplementalFeed::generate_for_client (TSV out -> GMC) - phase: 04-products-aggregate provides: products_aggregate scope, p.title/p.name display fallback provides: - clients.xml_feed_url + xml_feed_last_sync_at - products schema split: title/description (zrodlo) vs title_gmc/description_gmc (edycja) - products.price (DECIMAL z feedu) - \services\XmlFeedImporter (XMLReader streaming + batched manual upsert) - cron_universal hook na import feedu affects: [products UI, AI suggestions modal, supplemental feed generation] tech-stack: added: [XMLReader streaming, manual SELECT-then-UPDATE/INSERT upsert] patterns: - Idempotentne migracje rename via PREPARE/EXECUTE z guardem INFORMATION_SCHEMA - Source/edit field separation (X / X_gmc) z SupplementalFeed czytajacym _gmc key-files: created: - migrations/029_products_rename_columns_and_xml_feed.sql - autoload/services/class.XmlFeedImporter.php modified: - autoload/factory/class.Products.php - autoload/controls/class.Products.php - autoload/controls/class.Cron.php - autoload/controls/class.Clients.php - autoload/services/class.SupplementalFeed.php - templates/clients/main_view.php - api.php key-decisions: - "title/description = ZRODLO (feed XML lub pierwszy fetch GA), title_gmc/description_gmc = EDYTOWALNE (output do GMC supplemental)" - "INDEX (client_id, offer_id) bez UNIQUE - istniejace duplikaty legacy uniemozliwiaja UNIQUE" - "Manual upsert (SELECT IN + UPDATE/INSERT) zamiast ON DUPLICATE KEY UPDATE - zgodne z brakiem UNIQUE" - "Aliasy AS name w SELECTach zachowane - kontrakt JS/DataTables nieruszony" patterns-established: - "Source vs Edit field naming: X dla zrodla, X_gmc dla edycji wysylanej do Merchant Center" - "Importery zewnetrznych feedow: XMLReader + batche w transakcjach + tempfile, set_time_limit(600), memory_limit 512M, gc co batch" duration: ~50min started: 2026-04-30T00:00:00Z completed: 2026-04-30T01:00:00Z --- # Phase 6 Plan 01: XML Feed Import Summary **Klient moze podac URL feedu XML w panelu edycji; cron_universal pobiera go strumieniowo (XMLReader, batche 200 w transakcjach), wzbogaca tabele products o title/description/price/custom_label_1, a schemat products zostal rozdzielony na pola zrodlowe (title/description) i edytowalne dla supplemental feed (title_gmc/description_gmc).** ## Performance | Metric | Value | |--------|-------| | Duration | ~50 min | | Started | 2026-04-30T00:00:00Z | | Completed | 2026-04-30T01:00:00Z | | Tasks | 4/4 auto + 1 checkpoint approved | | Files modified | 9 | ## Acceptance Criteria Results | Criterion | Status | Notes | |-----------|--------|-------| | AC-1: Schemat bazy danych | Pass | Migracja 029 idempotentna; kolumny title/title_gmc/description_gmc/price + xml_feed_url/xml_feed_last_sync_at; INDEX zamiast UNIQUE z powodu legacy duplikatow | | AC-2: Edycja klienta — pole feed XML | Pass | Pole "XML Feed URL" w dialogu edycji + walidacja FILTER_VALIDATE_URL + prefill z `data.xml_feed_url` | | AC-3: Cron pobiera feed XML i wzbogaca products | Pass | XmlFeedImporter::import_for_client wywolywany w cron_universal; manual upsert (SELECT IN + UPDATE/INSERT); transakcje per batch; ON ERROR pojedynczego itemu - skip+log | | AC-4: Refaktor odwolan do name/title | Pass | Wszystkie p.name -> p.title; p.title -> p.title_gmc; aliasy AS name zachowane; is_product_core_field ma title_gmc/description_gmc; lint PHP czysty | ## Accomplishments - **Schemat products przemodelowany** na czysty source/edit split: `title`+`description` to zrodlowe pola wypelniane przez feed XML lub pierwszy fetch GA; `title_gmc`+`description_gmc` to pola edytowane przez UI/AI i wysylane do GMC przez SupplementalFeed. - **Odporny importer feedu XML** — XMLReader streaming bez ladowania calego pliku do pamieci, batche po 200 pozycji w transakcjach, manual upsert kompatybilny z legacy duplikatami `(client_id, offer_id)`, set_time_limit(600), memory_limit 512M, gc_collect_cycles co batch. - **UI edycji klienta** uzupelnione o pole `XML Feed URL` (typ `url`, walidacja serwerowa) bez nowych zaleznosci frontendowych. - **Cron_universal** automatycznie pobiera feed po sync produktow GA dla kazdego klienta z ustawionym `xml_feed_url`; raport `xml_feed: { fetched, updated, inserted, skipped, peak_memory_mb, duration_ms }` dolaczony do response cron-a. ## Files Created/Modified | File | Change | Purpose | |------|--------|---------| | `migrations/029_products_rename_columns_and_xml_feed.sql` | Created | Idempotentny rename name->title, title->title_gmc + description_gmc + price + xml_feed_url/xml_feed_last_sync_at + INDEX (client_id, offer_id) | | `autoload/services/class.XmlFeedImporter.php` | Created | XMLReader streaming parser + manual batched upsert (SELECT IN -> UPDATE/INSERT) + cURL streaming download | | `autoload/factory/class.Products.php` | Modified | SQL rename p.name->p.title, p.title->p.title_gmc; is_product_core_field z title_gmc/description_gmc; aliasy AS name zachowane | | `autoload/controls/class.Products.php` | Modified | get_product_data dla edycji ('title_gmc', 'description_gmc'); set_product_data zapisuje do _gmc; mapowanie $row['title']/$row['title_gmc'] | | `autoload/controls/class.Cron.php` | Modified | Hook XmlFeedImporter w cron_universal; SELECTy/INSERTy products zaktualizowane; raport xml_feed w response | | `autoload/controls/class.Clients.php` | Modified | save() przyjmuje xml_feed_url + walidacja FILTER_VALIDATE_URL | | `autoload/services/class.SupplementalFeed.php` | Modified | TSV czyta z title_gmc AS title, description_gmc AS description | | `templates/clients/main_view.php` | Modified | Pole "XML Feed URL" w dialogu edycji + prefill JS | | `api.php` | Modified | Wszystkie SQL na `products` zaktualizowane (p.name->p.title, p.title->p.title_gmc); aliasy `AS name`/`AS title` zachowane jako kontrakt API; `set_product_data('title', ...)` -> `'title_gmc'` (post-fix po uwadze uzytkownika) | ## Decisions Made | Decision | Rationale | Impact | |----------|-----------|--------| | title/description = zrodlo, title_gmc/description_gmc = edycja | Korekta semantyki w trakcie checkpointa - pierwotna interpretacja byla odwrotna. Edytor pisze do _gmc, supplemental feed czyta z _gmc i pcha do GMC | Cala logika edycyjna i sync GMC zostaly zaktualizowane konsekwentnie; XmlFeedImporter pisze do title/description | | INDEX zamiast UNIQUE na (client_id, offer_id) | Istniejace dane mialy duplikaty (np. '2-1625' x N), UNIQUE rzucal 1062 IntegrityViolation | Manual upsert w PHP zamiast ON DUPLICATE KEY UPDATE; legacy duplikaty sa wszystkie aktualizowane (lista id z SELECT IN) | | Manual SELECT IN + UPDATE/INSERT per batch | Konsekwencja braku UNIQUE; daje pelna kontrole nad obsluga duplikatow | Jeden SELECT + N UPDATE/INSERT per batch w transakcji - wciaz wydajne dla 5000+ pozycji | | Aliasy `... AS name` w SELECTach zachowane | Kontrakt z DataTables/JS w templates/products | Brak zmian frontendu; minimalny blast radius | ## Deviations from Plan ### Summary | Type | Count | Impact | |------|-------|--------| | Auto-fixed | 3 | Konieczne korekty - migracja UNIQUE -> INDEX, semantyka pol, brakujacy api.php | | Scope additions | 0 | Brak | | Deferred | 1 | Backfill description -> description_gmc nieobligatoryjny | **Total impact:** Korekty kierunkowe (semantyka pol + UNIQUE -> INDEX), bez scope creep. ### Auto-fixed Issues **3. [Coverage] api.php nie byl uwzgledniony w pierwotnym refaktorze** - **Found during:** post-checkpoint pytanie uzytkownika ("Czy w api.php rowniez poprawiles nazwy kolumn?") - **Issue:** Plan boundaries i grep nie obejmowaly `api.php` - 4 zapytania SQL na `products` z `p.name`/`p.title`, oraz `set_product_data($product['id'], 'title', ...)` w endpointcie product_title_set - **Fix:** Wszystkie zapytania zaktualizowane analogicznie do reszty kodu: `p.name` -> `p.title`, `p.title` -> `p.title_gmc`, aliasy `AS name`/`AS title` dodane gdzie publiczny kontrakt API (offer_id/get_default_titles endpointy); `set_product_data` zapisuje teraz do `title_gmc` - **Files:** `api.php` - **Verification:** lint PHP czysty; grep `p\.name\|p\.title\b` zwraca tylko legalne aliasy **1. [Schema] UNIQUE INDEX (client_id, offer_id) blokowal migracje przez legacy duplikaty** - **Found during:** human-verify checkpoint (php install.php zwrocil SQLSTATE 23000 / 1062 'Duplicate entry 2-1625') - **Issue:** Istniejace dane w `products` mialy wiele wierszy z tym samym `(client_id, offer_id)` - **Fix:** Zamiana na non-unique INDEX `idx_products_client_offer` + przepisanie XmlFeedImporter::flush_batch z `ON DUPLICATE KEY UPDATE` na manual SELECT-then-UPDATE/INSERT (aktualizuje WSZYSTKIE legacy duplikaty per offer_id) - **Files:** `migrations/029_products_rename_columns_and_xml_feed.sql`, `autoload/services/class.XmlFeedImporter.php` - **Verification:** `php install.php` przechodzi; importer testowo dziala na klientach z xml_feed_url **2. [Semantics] Inwersja zrodlo vs edycja w polach title/description** - **Found during:** uwaga uzytkownika podczas Task 4 ("title/description = zrodlo, title_gmc/description_gmc = edycja") - **Issue:** Pierwotny plan zaklada title=glowny display, title_gmc=zrodlo z feedu - co odwrocilo by przeplyw - **Fix:** Flip: XmlFeedImporter pisze do title/description; is_product_core_field = title_gmc/description_gmc; SupplementalFeed czyta z _gmc; controls/Products.php: edycja zapisuje do _gmc; AI prompt context czyta z _gmc - **Files:** `autoload/factory/class.Products.php`, `autoload/controls/class.Products.php`, `autoload/services/class.SupplementalFeed.php`, `autoload/services/class.XmlFeedImporter.php` - **Verification:** lint PHP czysty wszedzie; flow source -> edit -> GMC zgodny z modelem mentalnym ### Deferred Items - **Backfill istniejacych edycji do _gmc:** stare wartosci `description` (mieszane: GMC source + uzytkowe edycje) pozostaja w `description`. Jesli historyczne edycje powinny migrowac do `description_gmc`, wymagany jest dodatkowy `UPDATE products SET description_gmc = description WHERE description_gmc IS NULL`. Pozostawione decyzji uzytkownika - brak autoryzacji w planie. ## Issues Encountered | Issue | Resolution | |-------|------------| | `php install.php` -> SQLSTATE 23000 (1062) na uk_products_client_offer | Zamiana UNIQUE -> non-unique INDEX + manual upsert w PHP | | Niejednoznacznosc semantyki title vs title_gmc po pierwszym renamie | Korekta po uwadze uzytkownika - flip: title=zrodlo, _gmc=edycja, sciagniete na cale stos | ## Next Phase Readiness **Ready:** - Schemat `products` rozdziela source/edit - przyszle integracje feedow (np. inne formaty, multi-feed) maja czytelny target - `\services\XmlFeedImporter` jako referencyjny wzorzec importera feedow (XMLReader + batched transactions) - Cron pipeline ma jednolity hook point dla wzbogacania danych produktow **Concerns:** - Legacy duplikaty `(client_id, offer_id)` w `products` nie sa rozwiazywane przez ten plan - importer aktualizuje wszystkie wiersze z danym offer_id, ale uklad bazy pozostaje "smieciowy". Przyszle zadanie deduplikacji powinno przeniesc historie/agregaty na pojedynczy id przed wymuszeniem UNIQUE. - Pole `description` istnialo wczesniej i moglo zawierac edycje uzytkownika - po nowej semantyce te dane reprezentuja "zrodlo", co moze byc mylace dla starych klientow. Backfill -> description_gmc jest deferred. **Blockers:** None. --- *Phase: 06-xml-feed-import, Plan: 01* *Completed: 2026-04-30*