169 lines
11 KiB
Markdown
169 lines
11 KiB
Markdown
---
|
|
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*
|