update
This commit is contained in:
@@ -1,30 +1,30 @@
|
||||
# STATE
|
||||
# STATE
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: (ad-hoc) Products - aggregate + breakdown
|
||||
Phase: 4 of 4 (Products Aggregate Breakdown) - Completed
|
||||
Plan: 04-01 unified (loop closed)
|
||||
Milestone: (ad-hoc) Products - XML feed import
|
||||
Phase: 6 of 6 (XML Feed Import) - Completed
|
||||
Plan: 06-01 unified (loop closed)
|
||||
Status: UNIFY complete
|
||||
Last activity: 2026-04-25T17:28:08+02:00 - Zamknieto petle planu 04-01
|
||||
Last activity: 2026-04-30T01:15:00Z - Zamknieto petle planu 06-01
|
||||
|
||||
Progress:
|
||||
- Milestone: [██████████] 100%
|
||||
- Phase 4: [██████████] 100%
|
||||
- Phase 6: [██████████] 100%
|
||||
|
||||
## Loop Position
|
||||
|
||||
```
|
||||
PLAN --> APPLY --> UNIFY
|
||||
✓ ✓ ✓ [Petla zamknieta]
|
||||
✓ ✓ ✓ [Petla zamknieta]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-25
|
||||
Last session: 2026-04-30
|
||||
Stopped at: Loop closed, gotowe do nowego /paul:plan
|
||||
Next action: Jesli chcesz kontynuowac - uruchom /paul:plan z kolejnym zadaniem
|
||||
Resume file: .paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md
|
||||
Resume file: .paul/phases/06-xml-feed-import/06-01-SUMMARY.md
|
||||
|
||||
## Historia zrealizowanych planow
|
||||
|
||||
@@ -32,6 +32,8 @@ Resume file: .paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md
|
||||
- `02-01-PLAN.md` - custom_label_1 w supplemental feed TSV (completed 2026-04-22)
|
||||
- `03-01-PLAN.md` - Powrot do widoku "wszystkie kampanie" w /products (completed 2026-04-24)
|
||||
- `04-01-PLAN.md` - Agregat produktu + rozwijane podwiersze kampania/grupa w /products (completed 2026-04-25)
|
||||
- `05-01-PLAN.md` - Usuwanie wpisow historii produktu per kampania+grupa z UI breakdown (completed 2026-04-29)
|
||||
- `06-01-PLAN.md` - XML feed import + rename name/title + xml_feed_url w clients (completed 2026-04-30)
|
||||
|
||||
## Decisions
|
||||
|
||||
@@ -40,8 +42,15 @@ Resume file: .paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md
|
||||
| 2026-04-24 | Usuniecie placeholdera zamiast sentinel `0/all` | 3 | Minimalny blast radius, bez zmian w kontrolerze/factory |
|
||||
| 2026-04-25 | Bez wybranej grupy: glowny agregat per produkt + rozwijane podwiersze per kampania/grupa | 4 | Czytelniejsza analiza produktu i szybki drill-down |
|
||||
| 2026-04-25 | Podwiersze tylko readonly, edycja tylko w parent row | 4 | Spojnosc UX i brak konfliktu akcji edycyjnych |
|
||||
| 2026-04-29 | Usuwanie scope-level: products_aggregate + products_history + products_history_30 (transakcja), bez ruszania `products` ani Google Ads API | 5 | Hard delete lokalny, ograniczony do trojki product+campaign+ad_group |
|
||||
| 2026-04-29 | UI: nowa kolumna "Akcje" w breakdown z czerwona ikona kosza + dialog $.confirm + ajax.reload(null,false) | 5 | Brak utraty stanu paginacji/filtrow przy odswiezaniu agregatu |
|
||||
| 2026-04-30 | XML feed: jeden URL w clients.xml_feed_url, mapping po g:id=offer_id, parser XMLReader (streaming), batched manual upsert (200/batch) w transakcjach | 6 | Odporno na feedy z kilkoma tysiacami pozycji, niezalezne pola _gmc dla danych edytowalnych |
|
||||
| 2026-04-30 | Refaktor schematu products: name->title, title->title_gmc, dodanie description_gmc i price | 6 | Rozdzial danych zrodlowych od edytowalnych |
|
||||
| 2026-04-30 | Korekta semantyki: title/description = ZRODLOWE (z feedu lub pierwszy fetch GA), title_gmc/description_gmc = EDYTOWALNE (wysylane jako supplemental feed do GMC) | 6 | XmlFeedImporter pisze do title/description, edycja UI/AI pisze do _gmc, SupplementalFeed czyta z _gmc |
|
||||
| 2026-04-30 | INDEX zamiast UNIQUE na (client_id, offer_id) | 6 | Istniejace dane mialy duplikaty - manual upsert (SELECT IN + UPDATE/INSERT) zamiast ON DUPLICATE KEY UPDATE |
|
||||
|
||||
## Notes
|
||||
|
||||
- PAUL framework dziala tutaj w trybie ad-hoc (bez ROADMAP.md i PROJECT.md).
|
||||
- Human-verify checkpoint dla planu 04-01 zatwierdzony (`approved`).
|
||||
- Plan 06-01 zostal zamkniety mimo legacy duplikatow w products - przyszle zadanie deduplikacji moze wymusic UNIQUE.
|
||||
- Backfill historycznych edycji `description` -> `description_gmc` jest deferred (czeka na decyzje uzytkownika).
|
||||
|
||||
19
.paul/changelog/2026-04-29.md
Normal file
19
.paul/changelog/2026-04-29.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 2026-04-29
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 5, Plan 05-01] Usuwanie wpisow historii produktu per kampania+grupa z UI breakdown w /products
|
||||
- Factory: nowa metoda `\factory\Products::delete_product_scope_history($pid,$cid,$agid)` z transakcja PDO; atomowy DELETE z `products_aggregate`, `products_history`, `products_history_30`
|
||||
- Controller: nowa akcja AJAX `/products/delete_product_scope_history/` z walidacja `product_id > 0`
|
||||
- Controller: rozszerzono `breakdown_for_view` o `product_id`, `campaign_id`, `ad_group_id` (auto-fix - bez tego UI handler dostawal undefined)
|
||||
- UI: nowa kolumna "Akcje" w tabeli rozbicia, ikona kosza z dialogiem `$.confirm`, optymistyczne usuwanie wiersza + `ajax.reload(null,false)`
|
||||
- Bonus: przebudowany styl ikony rozwijania breakdown (CSS rotate, hover, fioletowy stan open) - usunieto JS swap klas FA
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `autoload/factory/class.Products.php`
|
||||
- `autoload/controls/class.Products.php`
|
||||
- `templates/products/main_view.php`
|
||||
- `.paul/phases/05-products-scope-history-delete/05-01-PLAN.md`
|
||||
- `.paul/phases/05-products-scope-history-delete/05-01-SUMMARY.md`
|
||||
- `.paul/STATE.md`
|
||||
29
.paul/changelog/2026-04-30.md
Normal file
29
.paul/changelog/2026-04-30.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 2026-04-30
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 6, Plan 06-01] XML feed import per klient + refaktor schematu products (source/edit split) + integracja z cron_universal
|
||||
- Dodano pole `xml_feed_url` w edycji klienta (templates/clients/main_view.php) z walidacja URL po stronie serwera
|
||||
- Migracja 029: rename `products.name` -> `title`, `products.title` -> `title_gmc`, dodanie `description_gmc`, `price`, `clients.xml_feed_url`, `clients.xml_feed_last_sync_at`, INDEX `(client_id, offer_id)` (non-unique - legacy duplikaty)
|
||||
- Nowy serwis `\services\XmlFeedImporter` - XMLReader streaming + batche 200 w transakcjach + manual upsert (SELECT IN + UPDATE/INSERT); odporny na feedy 5000+ pozycji
|
||||
- Hook w `cron_universal` wywoluje XmlFeedImporter po sync produktow GA dla klientow z ustawionym feedem; raport `xml_feed` w response
|
||||
- Refaktor wszystkich odwolan do kolumn `products.name`/`products.title` w factory, controls, services, templates, api.php; aliasy `AS name` zachowane jako kontrakt JS/API
|
||||
- Korekta semantyki: `title`/`description` = ZRODLO (z feedu lub pierwszy fetch GA), `title_gmc`/`description_gmc` = EDYTOWALNE (wysylane do GMC supplemental feed)
|
||||
- `is_product_core_field` zaktualizowane do `title_gmc`/`description_gmc` (edytowalne)
|
||||
- `SupplementalFeed::generate_for_client` czyta `title_gmc AS title, description_gmc AS description` przy generowaniu TSV
|
||||
- api.php (cztery endpointy products): SQL zaktualizowane, set_product_data zapisuje do title_gmc
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `migrations/029_products_rename_columns_and_xml_feed.sql`
|
||||
- `autoload/services/class.XmlFeedImporter.php`
|
||||
- `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`
|
||||
- `.paul/phases/06-xml-feed-import/06-01-PLAN.md`
|
||||
- `.paul/phases/06-xml-feed-import/06-01-SUMMARY.md`
|
||||
- `.paul/STATE.md`
|
||||
23
.paul/governance/governance_2026-04-29.jsonl
Normal file
23
.paul/governance/governance_2026-04-29.jsonl
Normal file
@@ -0,0 +1,23 @@
|
||||
{"ts":"2026-04-29T18:56:54Z","tool":"Bash","cmd":"mkdir -p \".paul/phases/05-products-scope-history-delete\" && ls .paul/phases/\",\"description\":\"create phase directory\"},\"tool_response\":{\"stdout\":\"01-products-cl1-column\\n02-supplemental-fe","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T18:58:47Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\05-products-scope-history-delete\\\\05-01-PLAN.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T18:59:10Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T18:59:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T18:59:54Z","tool":"Bash","cmd":"php -l autoload/factory/class.Products.php\",\"description\":\"syntax check factory\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/factory/class.Products.php\",\"stderr\":\"\","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:00:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:00:07Z","tool":"Bash","cmd":"php -l autoload/controls/class.Products.php\",\"description\":\"syntax check controller\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/controls/class.Products.php\",\"stderr\","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:00:22Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:00:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:00:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:00:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:00:58Z","tool":"Bash","cmd":"php -l templates/products/main_view.php && git diff --stat\",\"description\":\"syntax + diff stat\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in templates/products/main_view.php\\nwar","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:02:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:03:02Z","tool":"Bash","cmd":"php -l autoload/controls/class.Products.php\",\"description\":\"syntax check controller\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/controls/class.Products.php\",\"stderr\","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:04:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:04:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:04:50Z","tool":"Bash","cmd":"php -l templates/products/main_view.php\",\"description\":\"syntax check template\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in templates/products/main_view.php\",\"stderr\":\"\",\"i","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:05:06Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:05:59Z","tool":"Bash","cmd":"date -u +\"%Y-%m-%dT%H:%M:%SZ\"\",\"description\":\"get current UTC time\"},\"tool_response\":{\"stdout\":\"2026-04-29T19:05:59Z\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"noOutputExp","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:07:06Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\05-products-scope-history-delete\\\\05-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:07:23Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\changelog\\\\2026-04-29.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:07:35Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T19:07:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
69
.paul/governance/governance_2026-04-30.jsonl
Normal file
69
.paul/governance/governance_2026-04-30.jsonl
Normal file
@@ -0,0 +1,69 @@
|
||||
{"ts":"2026-04-29T22:33:54Z","tool":"Bash","cmd":"sed -n '1052,1110p' autoload/controls/class.Cron.php\",\"description\":\"Read sync_products_fetch\"},\"tool_response\":{\"stdout\":\" static private function sync_products_fetch_for_client( $client, ","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:39:44Z","tool":"Bash","cmd":"mkdir -p \".paul/phases/06-xml-feed-import\" && grep -n \"price\\\\|sku\\\\|brand\\\\|gtin\\\\|mpn\\\\|product_type\" migrations/016_products_model_unification.sql migrations/006_products_scope_dimens","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:42:03Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\06-xml-feed-import\\\\06-01-PLAN.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:42:32Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:44:31Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\migrations\\\\029_products_rename_columns_and_xml_feed.sql","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:45:09Z","tool":"Bash","cmd":"sed -n '1,30p' autoload/factory/class.Products.php\",\"description\":\"Read Products factory header\"},\"tool_response\":{\"stdout\":\"<?\\nnamespace factory;\\nclass Products\\n{\\n static private f","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:45:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:18Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:46:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\services\\\\class.SupplementalFeed.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:47:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Cron.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:47:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Cron.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:47:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Cron.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:47:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Cron.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:47:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Cron.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:48:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:49:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:49:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:49:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:49:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:49:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:50:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\clients\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:50:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\clients\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:50:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Clients.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:51:21Z","tool":"Bash","cmd":"sed -n '100,135p' autoload/controls/class.Cron.php\",\"description\":\"Read cron_universal hook area\"},\"tool_response\":{\"stdout\":\" if ( empty( $campaign_errors ) && $last_day_in_window && $","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:51:25Z","tool":"Bash","cmd":"sed -n '135,160p' autoload/controls/class.Cron.php\",\"description\":\"Read further cron_universal\"},\"tool_response\":{\"stdout\":\" $errors = array_merge( $campaign_errors, $products_errors );","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:51:30Z","tool":"Bash","cmd":"sed -n '40,80p' autoload/controls/class.Cron.php\",\"description\":\"Read cron_universal start\"},\"tool_response\":{\"stdout\":\" if ( $client_id > 0 )\\n {\\n $client = $mdb -> query(\\n ","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:52:15Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\services\\\\class.XmlFeedImporter.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:52:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\migrations\\\\029_products_rename_columns_and_xml_feed.sql","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:52:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\services\\\\class.XmlFeedImporter.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:53:06Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Cron.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:53:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Cron.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:53:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:53:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:53:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:53:56Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:54:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\services\\\\class.SupplementalFeed.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:54:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\services\\\\class.XmlFeedImporter.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:54:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\services\\\\class.XmlFeedImporter.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:54:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\services\\\\class.XmlFeedImporter.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:54:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:55:08Z","tool":"Bash","cmd":"sed -n '1108,1115p' autoload/controls/class.Cron.php; echo ---; grep -n \"insert.*products\\\\|UPDATE products\\\\|->update.*products\\\\|VALUES.*offer_id\\\\|VALUES.*name\" autoload/controls/class.Cr","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:55:20Z","tool":"Bash","cmd":"php -l autoload/services/class.XmlFeedImporter.php 2>&1; echo ---; php -l autoload/controls/class.Cron.php 2>&1; echo ---; php -l autoload/factory/class.Products.php 2>&1; echo ---; php -l autoload/co","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:55:26Z","tool":"Bash","cmd":"php -l autoload/services/class.SupplementalFeed.php 2>&1; echo ---; php -r \"if(\\\\$xml=simplexml_load_string('<?xml version=\\\\\"1.0\\\\\"?><rss xmlns:g=\\\\\"http://base.google.com/ns/1.0\\\\\"><c","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:55:41Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:56:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\migrations\\\\029_products_rename_columns_and_xml_feed.sql","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:57:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\services\\\\class.XmlFeedImporter.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T22:57:27Z","tool":"Bash","cmd":"php -l autoload/services/class.XmlFeedImporter.php\",\"description\":\"Lint importer\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in autoload/services/class.XmlFeedImporter.php\",\"std","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:00:49Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\06-xml-feed-import\\\\06-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:01:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\api.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:01:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\api.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:01:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\api.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:01:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\api.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:01:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\api.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:01:57Z","tool":"Bash","cmd":"php -l api.php\",\"description\":\"Lint api.php\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in api.php\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"noOutputExpected\":f","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:02:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\06-xml-feed-import\\\\06-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:02:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\06-xml-feed-import\\\\06-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:02:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\06-xml-feed-import\\\\06-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:02:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\06-xml-feed-import\\\\06-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:02:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\06-xml-feed-import\\\\06-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:03:02Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
{"ts":"2026-04-29T23:03:22Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\changelog\\\\2026-04-30.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||
293
.paul/phases/05-products-scope-history-delete/05-01-PLAN.md
Normal file
293
.paul/phases/05-products-scope-history-delete/05-01-PLAN.md
Normal file
@@ -0,0 +1,293 @@
|
||||
---
|
||||
phase: 05-products-scope-history-delete
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- autoload/factory/class.Products.php
|
||||
- autoload/controls/class.Products.php
|
||||
- templates/products/main_view.php
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodac w widoku /products w rozwinietym podwierszu breakdown (per kampania+grupa reklam) przycisk usuwania z potwierdzeniem, ktory wycina lokalne wpisy statystyczno-historyczne dla konkretnej trojki product_id + campaign_id + ad_group_id.
|
||||
|
||||
## Purpose
|
||||
Pozwala uzytkownikowi rzetelnie wyczyscic statystyki produktu w obrebie jednej grupy reklam (np. po podmianie produktu w kampanii/grupie albo gdy historia jest zasmiecona), bez wplywu na pozostale grupy/kampanie i bez ruszania wpisu w tabeli `products`.
|
||||
|
||||
## Output
|
||||
- Nowy endpoint AJAX `/products/delete_product_scope_history/`
|
||||
- Nowa metoda factory `\factory\Products::delete_product_scope_history($product_id, $campaign_id, $ad_group_id)`
|
||||
- UI: kolumna "Akcje" w tabeli breakdown z czerwona ikona kosza, dialog potwierdzenia, optymistyczne usuniecie wiersza i odswiezenie wiersza nadrzednego (agregatu) bez resetu paginacji.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
<clarifications>
|
||||
- **Zakres usuwania** — Co dokladnie ma usuwac przycisk z podwiersza breakdown?
|
||||
→ Odpowiedz: `products_aggregate` (statystyki) + `products_history` (dzienne) + `products_history_30` (30d), wszystkie dla danej trojki product_id+campaign_id+ad_group_id.
|
||||
- **Miejsce UI** — Gdzie umiescic akcje usuwania?
|
||||
→ Odpowiedz: Nowa kolumna "Akcje" na koncu tabeli breakdown z czerwona ikona kosza per wiersz.
|
||||
- **Scope API** — Czy usuwanie lokalne czy z Google Ads API?
|
||||
→ Odpowiedz: Tylko lokalnie (DB). Bez wywolan do Google Ads API. Cron synchronizacji moze i tak ponownie zaciagnac dane jesli produkt nadal aktywny w MC.
|
||||
- **UI po usun.** — Co po udanym usunieciu?
|
||||
→ Odpowiedz: Usun wiersz breakdown + odswiez agregat parent (`ajax.reload(null, false)`), zachowujac paginacje.
|
||||
</clarifications>
|
||||
|
||||
## Project Context
|
||||
@.paul/STATE.md
|
||||
@.paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md
|
||||
|
||||
## Source Files
|
||||
@autoload/factory/class.Products.php
|
||||
@autoload/controls/class.Products.php
|
||||
@templates/products/main_view.php
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Endpoint usuwa lokalne wpisy dla trojki product+campaign+ad_group
|
||||
```gherkin
|
||||
Given uzytkownik wysyla POST do /products/delete_product_scope_history/ z parametrami product_id, campaign_id, ad_group_id (wszystkie >= 0, product_id > 0)
|
||||
When zadanie zostaje przetworzone
|
||||
Then z products_aggregate znika dokladnie jeden wiersz pasujacy do (product_id, campaign_id, ad_group_id)
|
||||
And z products_history znikaja wszystkie wpisy pasujace do tej samej trojki
|
||||
And z products_history_30 znikaja wszystkie wpisy pasujace do tej samej trojki
|
||||
And zaden wiersz w `products`, `products_aggregate`, `products_history`, `products_history_30` dla *innych* trojek (inne campaign_id lub ad_group_id, ten sam product_id) nie zostaje naruszony
|
||||
And odpowiedz to JSON `{"status":"ok"}`
|
||||
```
|
||||
|
||||
## AC-2: Walidacja parametrow i bledow
|
||||
```gherkin
|
||||
Given POST do /products/delete_product_scope_history/ bez product_id (lub product_id <= 0)
|
||||
When zadanie zostaje przetworzone
|
||||
Then odpowiedz to JSON `{"status":"error","message":"<komunikat PL>"}`
|
||||
And zaden wiersz w bazie nie zostaje usuniety
|
||||
```
|
||||
Uwaga: `campaign_id` lub `ad_group_id` rowne 0 sa dozwolone (PMax-y maja `ad_group_id = 0`, breakdown query renderuje takie wiersze).
|
||||
|
||||
## AC-3: UI kolumny Akcje + potwierdzenie
|
||||
```gherkin
|
||||
Given uzytkownik rozwinal breakdown produktu w /products
|
||||
When patrzy na tabele rozbicia
|
||||
Then na koncu kazdego wiersza widzi nowa kolumne "Akcje" z czerwona ikona kosza (`fa-solid fa-trash`, `btn btn-sm btn-danger`)
|
||||
And po klinieciu w ikone otwiera sie dialog $.confirm z tekstem zawierajacym nazwe kampanii i grupy reklam danego wiersza oraz pytaniem o potwierdzenie
|
||||
And dialog ma przyciski "Usun" (akcja) i "Anuluj"
|
||||
And dopoki uzytkownik nie potwierdzi, zaden request nie jest wysylany
|
||||
```
|
||||
|
||||
## AC-4: UI po sukcesie aktualizuje sie bez utraty stanu
|
||||
```gherkin
|
||||
Given uzytkownik potwierdzil usuniecie i serwer zwrocil status ok
|
||||
When odpowiedz dotrze do przegladarki
|
||||
Then wiersz breakdown znika z aktualnie otwartej tabeli rozbicia (bez przeladowania calej strony)
|
||||
And glowna tabela `#products` jest odswiezona przez `ajax.reload(null, false)` (zachowana paginacja, sortowanie, filtry)
|
||||
And uzytkownik widzi toast/komunikat sukcesu (PL, krotki)
|
||||
And przy bledzie z serwera wyswietla sie czerwony dialog z `message` z odpowiedzi (lub fallback PL) i wiersz NIE jest usuwany z UI
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Factory - delete_product_scope_history</name>
|
||||
<files>autoload/factory/class.Products.php</files>
|
||||
<action>
|
||||
Dodaj statyczna metode `delete_product_scope_history( $product_id, $campaign_id, $ad_group_id )` w klasie `\factory\Products`.
|
||||
- Rzutuj wszystkie 3 argumenty na (int).
|
||||
- Walidacja: `$product_id <= 0` -> return false.
|
||||
- Uzyj globalnego `$mdb` (Medoo) zgodnie z konwencja pliku.
|
||||
- Wykonaj 3 osobne DELETE w jednej transakcji `$mdb->pdo->beginTransaction()` / `commit()` / w catch `rollBack()` + `return false`:
|
||||
1. `$mdb->delete('products_aggregate', ['product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id])`
|
||||
2. `$mdb->delete('products_history', ['product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id])`
|
||||
3. `$mdb->delete('products_history_30',['product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id])`
|
||||
- Zwroc `true` po commit.
|
||||
- NIE dotykaj tabeli `products` ani `campaigns`/`campaign_ad_groups` (chronione - viz. boundaries).
|
||||
- Avoid: jakichkolwiek warunkow `LIKE`, surowego SQL z konkatenacja stringow (Medoo dela tu wszystko bezpiecznie). Avoid: usuwania bez `campaign_id`/`ad_group_id` w klauzuli (skasowaloby calego produkta).
|
||||
</action>
|
||||
<verify>
|
||||
`php -l autoload/factory/class.Products.php` zwraca "No syntax errors".
|
||||
Manualny test SQL (na kopii lub z dry-runem): przed zmiana zliczyc `SELECT COUNT(*) FROM products_aggregate WHERE product_id=X AND campaign_id=Y AND ad_group_id=Z` (oczekiwane 1), wywolac metode, ponownie zliczyc (oczekiwane 0). Powtorzyc dla `products_history` i `products_history_30`. Sprawdzic, ze inne trojki dla tego samego product_id pozostaly.
|
||||
</verify>
|
||||
<done>AC-1 spelnione: usuwanie ograniczone do trojki, AC-2 spelnione w czesci serwerowej (zwrot false dla zlych parametrow).</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Controller - akcja AJAX delete_product_scope_history</name>
|
||||
<files>autoload/controls/class.Products.php</files>
|
||||
<action>
|
||||
Dodaj public static akcje `delete_product_scope_history()` w klasie `\controls\Products` (obok istniejacych `delete_product()` / `delete_products()`).
|
||||
- Czytaj parametry przez `\S::get`:
|
||||
$product_id = (int) \S::get('product_id');
|
||||
$campaign_id = (int) \S::get('campaign_id');
|
||||
$ad_group_id = (int) \S::get('ad_group_id');
|
||||
- Walidacja: `$product_id <= 0` -> `echo json_encode(['status'=>'error','message'=>'Brak identyfikatora produktu.']); exit;`
|
||||
- Wywolaj `\factory\Products::delete_product_scope_history($product_id, $campaign_id, $ad_group_id)`.
|
||||
- Sukces: `echo json_encode(['status'=>'ok']); exit;`
|
||||
- Porazka: `echo json_encode(['status'=>'error','message'=>'Nie udalo sie usunac wpisow historii dla tego zakresu.']); exit;`
|
||||
- Naladuj wzor istniejacych akcji `delete_product()` (linia ~1163) co do stylu (brak naglowkow, `echo json_encode(...)`, `exit`).
|
||||
- Avoid: dodawania nagłowkow `Content-Type: application/json` (reszta kontrolera tego nie robi - jQuery sobie radzi).
|
||||
</action>
|
||||
<verify>
|
||||
`php -l autoload/controls/class.Products.php` zwraca "No syntax errors".
|
||||
Recznie z konsoli przegladarki na zalogowanej sesji:
|
||||
`$.post('/products/delete_product_scope_history/', {product_id: 0}, console.log)` -> `{status:'error', message: ...}`
|
||||
`$.post('/products/delete_product_scope_history/', {product_id: <istniejacy>, campaign_id: <ist>, ad_group_id: <ist>}, console.log)` -> `{status:'ok'}` i wpisy znikaja.
|
||||
</verify>
|
||||
<done>AC-1, AC-2 spelnione end-to-end od strony serwera.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: UI - kolumna Akcje w breakdown + handler</name>
|
||||
<files>templates/products/main_view.php</files>
|
||||
<action>
|
||||
Modyfikacje wylacznie w obrebie breakdown:
|
||||
|
||||
a) `products_build_breakdown_html(row_meta)` (~linia 604):
|
||||
- Dodaj `<th>Akcje</th>` na koncu naglowka tabeli (po `<th>CL4</th>`).
|
||||
- W petli `rows.forEach` dodaj na koncu `<tr>` jeszcze jedna komorke:
|
||||
`'<td class="text-center"><button type="button" class="btn btn-sm btn-danger js-products-breakdown-delete" title="Usun wpisy historii dla tej kampanii+grupy" '
|
||||
+ 'data-product-id="' + entry.product_id + '" '
|
||||
+ 'data-campaign-id="' + (entry.campaign_id || 0) + '" '
|
||||
+ 'data-ad-group-id="' + (entry.ad_group_id || 0) + '" '
|
||||
+ 'data-campaign-name="' + escape_html(entry.campaign_name || '') + '" '
|
||||
+ 'data-ad-group-name="' + escape_html(entry.ad_group_name || '') + '">'
|
||||
+ '<i class="fa-solid fa-trash"></i></button></td>'`
|
||||
- WYMAGANE: w tasku 4 (factory/breakdown payload) `entry` musi zawierac `product_id`, `campaign_id`, `ad_group_id`. Sprawdzic istniejacy SELECT (`get_products_scope_breakdown`) - juz je zwraca (linie 707-709). Nie trzeba zmian backendu.
|
||||
|
||||
b) Dopisz delegowany handler kliku (np. tuz przy istniejacym handlerze `js-products-breakdown-toggle`, ~linia 743):
|
||||
|
||||
```js
|
||||
$( '#products tbody' ).on( 'click', '.js-products-breakdown-delete', function( e ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var $btn = $( this );
|
||||
var productId = parseInt( $btn.data( 'product-id' ), 10 ) || 0;
|
||||
var campaignId = parseInt( $btn.data( 'campaign-id' ), 10 ) || 0;
|
||||
var adGroupId = parseInt( $btn.data( 'ad-group-id' ), 10 ) || 0;
|
||||
var campaignName= String( $btn.data( 'campaign-name' ) || '' );
|
||||
var adGroupName = String( $btn.data( 'ad-group-name' ) || '' );
|
||||
if ( productId <= 0 ) { return; }
|
||||
|
||||
$.confirm({
|
||||
title: 'Usun wpisy historii',
|
||||
content: 'Czy na pewno chcesz usunac wpisy statystyk i historii tego produktu w kampanii <strong>'
|
||||
+ escape_html(campaignName) + '</strong> / grupie <strong>' + escape_html(adGroupName) + '</strong>?'
|
||||
+ '<br><br><small>Operacja usuwa wpisy z products_aggregate, products_history oraz products_history_30 dla tej kombinacji. Nie usuwa produktu z tabeli products ani z Google Ads.</small>',
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Usun',
|
||||
btnClass: 'btn-danger',
|
||||
action: function() {
|
||||
var $tr = $btn.closest( 'tr' );
|
||||
$btn.prop( 'disabled', true );
|
||||
$.ajax({
|
||||
url: '/products/delete_product_scope_history/',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: { product_id: productId, campaign_id: campaignId, ad_group_id: adGroupId },
|
||||
success: function( res ) {
|
||||
if ( res && res.status === 'ok' ) {
|
||||
$tr.remove();
|
||||
if ( typeof products_table !== 'undefined' && products_table ) {
|
||||
products_table.ajax.reload( null, false );
|
||||
}
|
||||
show_toast( 'Usunieto wpisy historii dla wybranego zakresu.', 'success' );
|
||||
} else {
|
||||
$btn.prop( 'disabled', false );
|
||||
$.alert({ title: 'Blad', content: ( res && res.message ) || 'Nie udalo sie usunac wpisow.' });
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$btn.prop( 'disabled', false );
|
||||
$.alert({ title: 'Blad', content: 'Blad polaczenia z serwerem.' });
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: { text: 'Anuluj' }
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Uwaga: `show_toast` i `escape_html` istnieja juz w pliku - uzyj ich. Jezeli `products_table` nie jest w scope tego closure (sprawdzic - jest zdefiniowana w glownym `$( function() { ... })`), uzyj `$( '#products' ).DataTable().ajax.reload( null, false )`.
|
||||
|
||||
c) CSS: nie wymagany dodatkowy CSS - kolumna "Akcje" przejmuje style domyslne `.products-breakdown-table td`. Ewentualnie w sekcji styli (~linia 217-237) dopisac `.products-breakdown-table td:last-child { width: 56px; text-align: center; }` jezeli kolumna sie rozjedzie.
|
||||
|
||||
d) Zaktualizuj komentarz/uwage na gorze tabeli breakdown jezeli istnieje (sprawdzic - prawdopodobnie nie ma).
|
||||
|
||||
Avoid: zmian w naglowkach pozostalych kolumn. Avoid: globalnego refresh `location.reload()`. Avoid: ruszania w `products_breakdown_meta` ani `get_products_scope_breakdown` (payload juz ma wymagane pola).
|
||||
</action>
|
||||
<verify>
|
||||
Manual w przegladarce na /products:
|
||||
1. Zaloguj sie, wybierz klienta, znajdz produkt z breakdownem (kilka kampanii/grup).
|
||||
2. Rozwin produkt -> widoczna kolumna "Akcje" z koszem na koncu tabeli.
|
||||
3. Klik kosz -> dialog z nazwa kampanii i grupy.
|
||||
4. Anuluj -> nic sie nie dzieje.
|
||||
5. Klik kosz -> Usun -> toast sukcesu, wiersz znika z tabeli rozbicia, glowny wiersz produktu ma zaktualizowane sumy (np. impressions, cost, conversions sa pomniejszone o usuniety zakres).
|
||||
6. SQL post-check: brak wpisow w `products_aggregate`, `products_history`, `products_history_30` dla skasowanej trojki; pozostale trojki dla tego product_id sa nietkniete.
|
||||
7. Browser console - brak bledow JS.
|
||||
</verify>
|
||||
<done>AC-3, AC-4 spelnione: dialog potwierdzenia + bezpieczne odswiezenie UI bez utraty paginacji.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
Dodano lokalne usuwanie wpisow statystyczno-historycznych (products_aggregate + products_history + products_history_30) per trojka product_id + campaign_id + ad_group_id z UI breakdown w /products.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Otworz https://adspro.projectpro.pl/products i wybierz klienta z aktywnymi kampaniami.
|
||||
2. Znajdz produkt, ktory wystepuje w >= 2 grupach reklam (lub kampaniach), rozwin breakdown.
|
||||
3. Sprawdz, ze nowa kolumna "Akcje" istnieje i ma czerwona ikone kosza w kazdym wierszu rozbicia.
|
||||
4. Klik kosz na jednym z wierszy -> potwierdz, ze dialog pokazuje wlasciwa nazwe kampanii i grupy reklam.
|
||||
5. Anuluj raz - wiersz nie powinien zniknac.
|
||||
6. Powtorz, kliknij "Usun" - wiersz znika, parent agregat sie aktualizuje (sumy maleja), pozostale wiersze breakdown w tym samym produkcie nadal sa.
|
||||
7. Wejdz na ten sam produkt po przeladowaniu strony - wiersz breakdown dla skasowanej trojki nie wraca (do nastepnego crona sync).
|
||||
8. Sprawdz w przegladarce DevTools, ze response z `/products/delete_product_scope_history/` to `{"status":"ok"}` i nie ma 500.
|
||||
</how-to-verify>
|
||||
<resume-signal>Wpisz "approved" aby zamknac plan, albo opisz problemy do poprawy.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- `autoload/factory/class.Products.php::delete_product()` / `delete_products()` (istniejaca logika usuwania calego produktu - inny przypadek)
|
||||
- `autoload/factory/class.Products.php::get_products_scope_breakdown()` (payload juz zawiera product_id/campaign_id/ad_group_id - nie modyfikujemy SELECT)
|
||||
- Tabela `products` - usuwanie nie tyka rekordu produktu (tylko statystyki+historia)
|
||||
- `services/GoogleAdsApi.php` i jakikolwiek call do Google Ads API - operacja jest scisle lokalna
|
||||
- Logika cron sync (`cron/cron_universal` itd.) - nie zmieniamy; po usunieciu cron moze ponownie zaciagnac dane jezeli produkt jest aktywny w MC, to jest oczekiwane
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Brak operacji bulk (usuwanie wybranych wielu wierszy breakdown jednoczesnie) - jeden wiersz = jeden klik = jedno potwierdzenie
|
||||
- Brak undo / soft-delete - hard delete z DB
|
||||
- Brak zmian w endpoincie `/products/get_products/` - payload juz zawiera potrzebne ID
|
||||
- Brak telemetrii / audit log - jezeli okaze sie potrzebny, oddzielny plan
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Przed zamknieciem planu:
|
||||
- [ ] `php -l autoload/factory/class.Products.php` OK
|
||||
- [ ] `php -l autoload/controls/class.Products.php` OK
|
||||
- [ ] Manual: scenariusz Task 3 verify (kroki 1-7) i checkpoint human-verify (kroki 1-8)
|
||||
- [ ] DevTools: brak bledow JS i 500 z endpointu
|
||||
- [ ] SQL post-check: usunieto dokladnie te trojki, ktore mialy zniknac
|
||||
- [ ] Wszystkie AC-1..AC-4 spelnione
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 3 auto taski + 1 human-verify ukonczone i potwierdzone
|
||||
- Brak nowych bledow lintera / parse errors
|
||||
- UI breakdown zachowuje sie zgodnie z AC-3 i AC-4
|
||||
- Operacja jest atomowa (transakcja) i ograniczona do podanej trojki
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Po ukonczeniu utworz `.paul/phases/05-products-scope-history-delete/05-01-SUMMARY.md` zgodnie z konwencja phase 04 (frontmatter + sekcje Performance / AC Results / Files / Decisions / Deviations / Issues / Next Phase Readiness).
|
||||
</output>
|
||||
149
.paul/phases/05-products-scope-history-delete/05-01-SUMMARY.md
Normal file
149
.paul/phases/05-products-scope-history-delete/05-01-SUMMARY.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
phase: 05-products-scope-history-delete
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [products, datatables, breakdown, delete, history, php, jquery, transactions]
|
||||
|
||||
requires:
|
||||
- phase: 04-products-aggregate-breakdown
|
||||
provides: rozwijane podwiersze breakdown per kampania+grupa z payloadem row_meta.breakdown_rows
|
||||
|
||||
provides:
|
||||
- Lokalne usuwanie wpisow statystyczno-historycznych per trojka product_id+campaign_id+ad_group_id z UI breakdown
|
||||
- Endpoint AJAX `/products/delete_product_scope_history/`
|
||||
- Factory method `\factory\Products::delete_product_scope_history()` (transakcja PDO)
|
||||
- Kolumna "Akcje" w tabeli rozbicia + dialog `$.confirm` z potwierdzeniem
|
||||
- Odswiezony, schludniejszy styl ikony rozwijania breakdown (CSS rotate, hover, stan open)
|
||||
|
||||
affects:
|
||||
- autoload/factory/class.Products.php (delete API)
|
||||
- autoload/controls/class.Products.php (payload breakdown_for_view + nowa akcja)
|
||||
- templates/products/main_view.php (kolumna Akcje + handler + style)
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Hard delete scope-level: AGGREGATE + HISTORY + HISTORY_30 atomowo w transakcji PDO"
|
||||
- "Breakdown payload eksponuje pelna trojke ID (product, campaign, ad_group) dla operacji per-wiersz"
|
||||
- "Toggle expand stylowany CSS-em (rotate 90deg) zamiast swapu klas FA w JS"
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- autoload/factory/class.Products.php
|
||||
- autoload/controls/class.Products.php
|
||||
- templates/products/main_view.php
|
||||
|
||||
key-decisions:
|
||||
- "Hard delete lokalny w transakcji - bez Google Ads API, bez ruszania tabeli `products`"
|
||||
- "Eksponowanie product_id/campaign_id/ad_group_id w breakdown_for_view (poza zaplanowana edycja factory)"
|
||||
- "Style toggle: chevron rotowany przez CSS, JS nie podmienia klas FA"
|
||||
|
||||
patterns-established:
|
||||
- "Operacje per-wiersz w child rows DataTables wymagaja eksponowania ID we payloadzie row_meta - nie polegaj na danych z parent row"
|
||||
- "Transakcja PDO przed serwisem Medoo: `$mdb->pdo->beginTransaction()` + `commit/rollBack` + `$pdo->inTransaction()` guard"
|
||||
|
||||
duration: ~30min
|
||||
started: 2026-04-29T18:35:00Z
|
||||
completed: 2026-04-29T19:05:00Z
|
||||
---
|
||||
|
||||
# Phase 5 Plan 01: Products Scope History Delete (Summary)
|
||||
|
||||
**Dodano w UI /products na rozwijanym podwierszu breakdown przycisk usuwania, ktory atomowo kasuje wpisy z `products_aggregate`, `products_history` i `products_history_30` dla konkretnej trojki product+campaign+ad_group, bez ruszania tabeli `products` i bez wywolan Google Ads API.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~30 min |
|
||||
| Started | 2026-04-29T18:35:00Z |
|
||||
| Completed | 2026-04-29T19:05:00Z |
|
||||
| Tasks | 4/4 (3 auto + 1 human-verify) + 1 unplanned bonus (toggle styling) |
|
||||
| Files modified | 3 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Endpoint usuwa lokalne wpisy dla trojki product+campaign+ad_group | Pass | Factory uzywa `$mdb->delete()` z dokladnym WHERE per kazda z 3 tabel; transakcja PDO; inne trojki nietkniete (zweryfikowane przez usera) |
|
||||
| AC-2: Walidacja parametrow i bledow | Pass | Controller: `product_id <= 0 -> {status:error,message:...}` przed wywolaniem factory; factory tez waliduje (defense in depth) |
|
||||
| AC-3: UI kolumny Akcje + potwierdzenie | Pass | Nowa kolumna "Akcje" widoczna na koncu breakdown; ikona kosza `fa-solid fa-trash` w `btn btn-sm btn-danger`; dialog `$.confirm` z nazwa kampanii/grupy + przyciski "Usun"/"Anuluj" |
|
||||
| AC-4: UI po sukcesie aktualizuje sie bez utraty stanu | Pass | `$tr.remove()` + `products_table.ajax.reload(null,false)`; toast PL na success; `$.alert` z `res.message` na blad - human-verify approved |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Dodano scope-level usuwanie historii produktu z UI breakdown bez ryzyka skasowania samego produktu ani wplywu na inne trojki kampania/grupa.
|
||||
- Atomowa operacja DELETE-z-trzech-tabel w pojedynczej transakcji PDO z guardem `inTransaction()` i `rollBack` na throw.
|
||||
- Naprawiono nieoczywisty bug: breakdown_for_view nie eksponowal ID, przez co handler dostawal `undefined` i nic sie nie dzialo - dolozono trzy pola.
|
||||
- Bonus: ikona rozwijania breakdown przemodelowana na okragly button (hover, fioletowy stan open, CSS rotate chevron) - usunieto JS swap klas FA.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Brak commitu fazowego na tym etapie (working tree zawiera tez `.vscode/ftp-kr.sync.cache.json`, `.serena/project.yml` i `.paul/STATE.md` z innych watkow).
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `autoload/factory/class.Products.php` | Modified (+51) | Nowa metoda `delete_product_scope_history($pid,$cid,$agid)` z transakcja PDO i 3-tabelowym DELETE |
|
||||
| `autoload/controls/class.Products.php` | Modified (+23 +3) | Nowa akcja AJAX `delete_product_scope_history()`; payload `breakdown_for_view` rozszerzony o `product_id`, `campaign_id`, `ad_group_id` |
|
||||
| `templates/products/main_view.php` | Modified (+86, ~30 styling) | Kolumna "Akcje" w `products_build_breakdown_html`, handler kliku z `$.confirm`, przebudowane style `.products-breakdown-toggle` (CSS rotate zamiast JS), CSS `td:last-child` szerokosci 56px |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Eksponowac product/campaign/ad_group ID w `breakdown_for_view` | Bez tego JS dostawal `undefined` ID i handler ucinal sie na walidacji `<= 0` | Ustanawia wzorzec: jak chcesz akcje per-wiersz w child row, dodaj ID do payloadu row_meta |
|
||||
| Stylowac chevron rotacja CSS, nie podmiana klas FA w JS | Mniej kodu JS, plynniejsza animacja, jeden zrodlowy stan (`.products-breakdown-open`) | Pojedynczy CSS transition zarzadza wizualem; usunieto 2 linie JS |
|
||||
| Transakcja PDO + guard `inTransaction()` zamiast nested-transaction errora | Inny kod moze otworzyc transakcje wczesniej (np. w cron/migracji) | Bezpieczne uzycie metody w roznych kontekstach wywolania |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Auto-fixed | 1 | Niezbedne - bez tej korekty UI nie dziala (bug: undefined ID) |
|
||||
| Scope additions | 1 | Maly bonus stylowania ikony toggle na prosbe usera, w obrebie tego samego pliku |
|
||||
| Deferred | 0 | Brak |
|
||||
|
||||
**Total impact:** Plan zrealizowany zgodnie z zakresem; jedna konieczna korekta payloadu (Task 2) i jedno male rozszerzenie kosmetyczne na zyczenie usera.
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. UI Breakdown - brak ID produktu/kampanii/grupy w payloadzie**
|
||||
- Found during: Task 4 (human-verify)
|
||||
- Issue: `js-products-breakdown-delete` po kliknieciu nie robil nic - `parseInt(undefined,10) || 0` dalo `0`, a handler ma `if (product_id <= 0) return`. Powod: `breakdown_for_view` w `controls/class.Products.php:1097` mapowal tylko statystyki + nazwy, bez ID.
|
||||
- Fix: Dodano `product_id`, `campaign_id`, `ad_group_id` jako pierwsze pola mappingu (z fallbackami).
|
||||
- Files: `autoload/controls/class.Products.php`
|
||||
- Verification: Czesc human-verify - klik kosza po patchu wywoluje dialog z poprawnymi nazwami kampanii/grupy.
|
||||
- Commit: TBD (pojedynczy commit fazowy zostanie wykonany w nastepnym etapie)
|
||||
|
||||
### Deferred Items
|
||||
|
||||
Brak.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| Klik w kosz nie wywolywal dialogu (puste dane w `entry.product_id` itd.) | Zlokalizowano: payload breakdown filtrowany w controllerze. Dolozono 3 pola ID i pominieto cache state DataTables (force reload) |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Wzorzec dla operacji per-wiersz w breakdown ustanowiony (ID w row_meta).
|
||||
- Wzorzec transakcyjnego DELETE w factory ustanowiony.
|
||||
- Style toggle bardziej spojne wizualnie - mozna wykorzystac wzorzec rotate-on-state w innych collapsible UI.
|
||||
|
||||
**Concerns:**
|
||||
- Cron sync moze ponownie zaciagnac dane jezeli produkt nadal aktywny w MC dla danej kampanii/grupy - to jest oczekiwane zachowanie, ale uzytkownik powinien byc tego swiadom.
|
||||
- Working tree zawiera zmiany niepowiazane z planem (`.vscode/ftp-kr.sync.cache.json`, `.serena/project.yml`) - przy commit fazowym selektywne stage'owanie.
|
||||
|
||||
**Blockers:**
|
||||
- Brak.
|
||||
|
||||
---
|
||||
*Phase: 05-products-scope-history-delete, Plan: 01*
|
||||
*Completed: 2026-04-29*
|
||||
318
.paul/phases/06-xml-feed-import/06-01-PLAN.md
Normal file
318
.paul/phases/06-xml-feed-import/06-01-PLAN.md
Normal file
@@ -0,0 +1,318 @@
|
||||
---
|
||||
phase: 06-xml-feed-import
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- migrations/029_products_rename_columns_and_xml_feed.sql
|
||||
- autoload/factory/class.Products.php
|
||||
- autoload/controls/class.Products.php
|
||||
- autoload/controls/class.Cron.php
|
||||
- autoload/controls/class.Clients.php
|
||||
- autoload/services/class.GoogleAdsApi.php
|
||||
- autoload/services/class.XmlFeedImporter.php
|
||||
- templates/clients/main_view.php
|
||||
- templates/products/main_view.php
|
||||
- templates/products/product_history.php
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodać obsługę feedu XML (Google Merchant) per klient: pole `xml_feed_url` w edycji klienta, parser XML odporny na duże feedy (kilka tysięcy pozycji), integrację z `cron_universal` która wzbogaca tabelę `products` o dane z feedu (title, description, price, custom_label_1) oraz dodaje nowe rekordy dla pozycji nieobecnych w Google Ads. Dodatkowo: refaktor schematu `products` — rename `name` → `title`, `title` → `title_gmc`, nowe pole `description_gmc` (rozdział danych z feedu od edytowanych/AI).
|
||||
|
||||
## Purpose
|
||||
Klient ma jedno źródło prawdy o swoich produktach (feed XML producenta). Obecnie dane w `products` pochodzą wyłącznie z Google Ads i Merchant API i są ubogie. Wzbogacenie ich o feed pozwoli na: (1) trafniejsze sugestie AI dla title/description, (2) widoczność produktów spoza GA w panelu, (3) świeże ceny i custom_label_1 do segmentacji.
|
||||
|
||||
## Output
|
||||
- Migracja SQL dodająca/renamująca kolumny i pole `xml_feed_url` w `clients`
|
||||
- Service `\services\XmlFeedImporter` (streaming XMLReader, batched upserts)
|
||||
- Hook w `Cron::cron_universal` wywołujący importer po sync produktów GA
|
||||
- Pole `xml_feed_url` w dialogu edycji klienta (`templates/clients/main_view.php` + `controls/Clients.php`)
|
||||
- Wszystkie odwołania do `products.name` / `products.title` zaktualizowane do nowej semantyki
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
<clarifications>
|
||||
- **Pole feed XML** — Jak przechowywać URL feedu XML w bazie?
|
||||
→ Odpowiedź: Jedno pole `xml_feed_url` w tabeli `clients` (jeden feed na klienta).
|
||||
- **Mapowanie** — Po czym łączyć wpisy z feedu z produktami w bazie?
|
||||
→ Odpowiedź: Po `<g:id>` = `products.product_id` (standardowy mapping GMC).
|
||||
- **Zakres pól** — Które pola z feedu XML uzupełniać w `products`?
|
||||
→ Odpowiedź: `title`, `description` (rozdzielone od pola edytowalnego/AI — osobne `title_gmc` jeśli kolizja, `description_gmc` jako nowe pole), `custom_label_1`, `price`. Brakujące kolumny dorobić.
|
||||
- **Rename schematu** — wymaganie dodatkowe z odpowiedzi:
|
||||
→ `products.name` → `products.title` (rename). Stare `products.title` (z GMC API) → `products.title_gmc`. Wszystkie miejsca w kodzie, gdzie te kolumny występują, mają zostać zaktualizowane.
|
||||
- **Brakujące w GA** — Co robić z pozycjami feedu nieobecnymi w `products`?
|
||||
→ Odpowiedź: Dodawać nowe rekordy do `products`.
|
||||
- **Skala** — wymaganie odporności:
|
||||
→ Pobieranie musi działać dla feedów z kilkoma tysiącami produktów (streaming parser, brak ładowania całego XML do pamięci, batched insert/update, transakcje per batch, timeout-safe wznawianie).
|
||||
</clarifications>
|
||||
|
||||
## Project Context
|
||||
@.paul/STATE.md
|
||||
@CLAUDE.md
|
||||
|
||||
## Source Files
|
||||
@autoload/controls/class.Cron.php
|
||||
@autoload/controls/class.Clients.php
|
||||
@autoload/factory/class.Products.php
|
||||
@autoload/controls/class.Products.php
|
||||
@autoload/services/class.GoogleAdsApi.php
|
||||
@templates/clients/main_view.php
|
||||
@migrations/016_products_model_unification.sql
|
||||
@migrations/028_products_custom_label_1.sql
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Schemat bazy danych
|
||||
```gherkin
|
||||
Given klient ma w UI dialog edycji
|
||||
When wykonam migrację 029
|
||||
Then tabela `clients` zawiera kolumnę `xml_feed_url VARCHAR(500) NULL`
|
||||
And tabela `products` zawiera kolumny: `title` (po rename z `name`), `title_gmc` (po rename ze starego `title`), `description_gmc TEXT NULL`, `price DECIMAL(10,2) NULL`
|
||||
And istniejący `custom_label_1` pozostaje bez zmian
|
||||
And migracja jest idempotentna (drugie uruchomienie nie zmienia stanu)
|
||||
```
|
||||
|
||||
## AC-2: Edycja klienta — pole feed XML
|
||||
```gherkin
|
||||
Given otwieram dialog edycji klienta w `/clients`
|
||||
When wpisuję URL `https://pomysloweprezenty.pl/google-feed.xml` w polu "XML Feed URL" i zapisuję
|
||||
Then wartość trafia do `clients.xml_feed_url`
|
||||
And po ponownym otwarciu dialogu pole jest wypełnione
|
||||
And puste pole zapisuje NULL (czyści feed)
|
||||
```
|
||||
|
||||
## AC-3: Cron pobiera feed XML i wzbogaca products (odporność na 1000+ pozycji)
|
||||
```gherkin
|
||||
Given klient ma ustawiony `xml_feed_url`
|
||||
When uruchamia się `cron_universal` dla tego klienta
|
||||
Then `\services\XmlFeedImporter::import_for_client($client_id)` wykonuje się po sync produktów GA
|
||||
And feed jest parsowany strumieniowo (XMLReader) bez ładowania całości do pamięci
|
||||
And produkty są upsertowane w batchach (np. 200 wierszy per batch) w transakcjach
|
||||
And dla pozycji `<g:id>` istniejącej w `products.product_id` aktualizowane są pola: `title_gmc`, `description_gmc`, `custom_label_1`, `price`
|
||||
And dla pozycji nieistniejącej tworzony jest nowy rekord w `products` (z `client_id`, `product_id` = `<g:id>`, oraz pola jak wyżej)
|
||||
And `products.title` i `products.description` (edytowalne/AI) NIE są nadpisywane
|
||||
And błąd parsowania pojedynczej pozycji nie przerywa importu (log + skip)
|
||||
And do `cron_sync_status` lub `settings` zapisywany jest timestamp ostatniego importu per klient
|
||||
And import limity czasu/pamięci PHP są respektowane (max execution time bumped lokalnie, set_time_limit, gc_collect_cycles co batch)
|
||||
```
|
||||
|
||||
## AC-4: Refaktor odwołań do `name` / `title` w kodzie
|
||||
```gherkin
|
||||
Given migracja 029 zmienia nazwy kolumn
|
||||
When wszystkie pliki PHP/template są zaktualizowane
|
||||
Then żadne zapytanie SQL ani odwołanie tablicowe nie używa `products.name` ani starego znaczenia `products.title`
|
||||
And widok `/products` (templates/products/main_view.php) renderuje `title` (dawne `name`) jako głównej nazwy
|
||||
And widok historii produktu pokazuje również `title_gmc` jeśli różny od `title`
|
||||
And edycja produktu w `controls/Products.php` zapisuje do nowych kolumn
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migracja schematu — rename kolumn products + nowe pola + xml_feed_url</name>
|
||||
<files>migrations/029_products_rename_columns_and_xml_feed.sql</files>
|
||||
<action>
|
||||
Stwórz idempotentną migrację SQL w stylu istniejących (PREPARE/EXECUTE z guardem INFORMATION_SCHEMA):
|
||||
|
||||
1. Sprawdź czy `products.name` istnieje → jeśli tak, RENAME do `title_tmp_old` (bo `title` już istnieje i koliduje).
|
||||
Lepsze podejście (kolejność krytyczna):
|
||||
- Jeśli `products.title` istnieje I `products.title_gmc` NIE istnieje → `ALTER TABLE products CHANGE title title_gmc VARCHAR(255) NULL DEFAULT NULL`.
|
||||
- Jeśli `products.name` istnieje I `products.title` NIE istnieje (po poprzednim renamie) → `ALTER TABLE products CHANGE name title VARCHAR(255) NULL DEFAULT NULL`.
|
||||
2. Dodaj `description_gmc TEXT NULL` po `description` (jeśli nie istnieje).
|
||||
3. Dodaj `price DECIMAL(10,2) NULL` po `custom_label_1` (jeśli nie istnieje).
|
||||
4. Dodaj `xml_feed_url VARCHAR(500) NULL DEFAULT NULL` w `clients` po `google_merchant_account_id` (jeśli nie istnieje).
|
||||
5. Dodaj `xml_feed_last_sync_at DATETIME NULL` w `clients` (jeśli nie istnieje).
|
||||
|
||||
Zastosuj wzorzec z migracji 028 (SET @sql = IF(EXISTS(...), 'DO 1', 'ALTER ...'); PREPARE; EXECUTE; DEALLOCATE).
|
||||
Avoid: bezpośrednie ALTER bez guardu (idempotencja jest wymagana — `php install.php --force` musi być bezpieczne).
|
||||
</action>
|
||||
<verify>
|
||||
`php install.php` przechodzi bez błędu;
|
||||
`SHOW COLUMNS FROM products LIKE 'title'`, `'title_gmc'`, `'description_gmc'`, `'price'` zwracają wiersze;
|
||||
`SHOW COLUMNS FROM clients LIKE 'xml_feed_url'`, `'xml_feed_last_sync_at'` zwracają wiersze;
|
||||
drugie uruchomienie `php install.php --force` nie zgłasza błędów typu "Duplicate column".
|
||||
</verify>
|
||||
<done>AC-1 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Refaktor odwołań name/title w PHP i templates</name>
|
||||
<files>autoload/factory/class.Products.php, autoload/controls/class.Products.php, autoload/controls/class.Cron.php, autoload/services/class.GoogleAdsApi.php, templates/products/main_view.php, templates/products/product_history.php</files>
|
||||
<action>
|
||||
Zaktualizuj wszystkie miejsca, gdzie używane są kolumny `products.name` / `products.title` w starym znaczeniu:
|
||||
|
||||
- `p.name` → `p.title`
|
||||
- Stare `p.title` (z GMC) → `p.title_gmc`
|
||||
- `p.description` (z GMC w starym kodzie) → tam gdzie zapisywane przez sync GMC, zmień na `description_gmc`. Gdzie chodzi o edycję/AI — zostaw `description`.
|
||||
|
||||
Pliki do prześwietlenia (z grepu wcześniej):
|
||||
- `autoload/factory/class.Products.php` — linie 11, 12, 208, 225, 227, 550, 586, 620, 649, 827, 854, 937, 1323
|
||||
- `autoload/controls/class.Products.php` — linie 325, 335, 706, 857, 925, 994, 1133 (uwaga: `name` w kontekście DataTables `_POST['order'][0]['name']` to JS, NIE zmieniaj)
|
||||
- `autoload/controls/class.Cron.php` — sekcje sync_products_fetch_for_client (zapisy do products) i mapowanie z GMC API
|
||||
- `autoload/services/class.GoogleAdsApi.php` — sekcje insertujące/aktualizujące products (NIE ruszaj `ad_group.name`, `campaign.name` itp. — to GAQL, nie nasza tabela)
|
||||
- `templates/products/main_view.php` i `product_history.php` — wyświetlanie tytułu
|
||||
|
||||
Reguła: alias `name` (np. `... AS name`) w SELECTach pozostaw — to publiczny kontrakt API/JS i zmienić tylko gdy faktycznie czytane jako `name` z PHP. Zachowaj backward-compat na poziomie response klucza `name` w AJAXach.
|
||||
Avoid: globalny find-replace „name” → „title” (są kolumny `name` w innych tabelach!). Edytuj punktowo, tylko `p.name` / `products.name` / `$row['name']` w kontekście produktów.
|
||||
</action>
|
||||
<verify>
|
||||
`grep -rn "p\.name\|products\.name\|products\`\.\`name" autoload/ templates/products/` zwraca pustą listę (lub tylko świadome aliasy AS name);
|
||||
Otwarcie `/products` w przeglądarce nie wywala błędu SQL;
|
||||
Edycja produktu (zmiana custom_label_1 / min_roas) nadal działa.
|
||||
</verify>
|
||||
<done>AC-4 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: UI edycji klienta — pole xml_feed_url</name>
|
||||
<files>autoload/controls/class.Clients.php, templates/clients/main_view.php</files>
|
||||
<action>
|
||||
1. `templates/clients/main_view.php` — w sekcji `settings-field` (analogicznie do `client-gmc-id`, ok. linia 152) dodaj pole:
|
||||
```html
|
||||
<div class="settings-field">
|
||||
<label for="client-xml-feed-url">XML Feed URL</label>
|
||||
<input type="url" id="client-xml-feed-url" name="xml_feed_url" class="form-control" placeholder="np. https://example.com/google-feed.xml" />
|
||||
<small class="text-muted">Adres feedu XML w formacie Google Merchant — używany do uzupełniania danych produktów</small>
|
||||
</div>
|
||||
```
|
||||
Oraz w sekcji JS prefill (przy `$('#client-gmc-id').val(...)`) dodaj:
|
||||
```js
|
||||
$( '#client-xml-feed-url' ).val( data.xml_feed_url || '' );
|
||||
```
|
||||
|
||||
2. `autoload/controls/class.Clients.php`:
|
||||
- W metodzie save (ok. linia 105–122 — gdzie czytane są pola formularza) dodaj:
|
||||
```php
|
||||
$xml_feed_url = trim( \S::get( 'xml_feed_url' ) );
|
||||
```
|
||||
- W tablicy zapisu dołóż: `'xml_feed_url' => $xml_feed_url ?: null,`
|
||||
- W SELECT klientów dla listy (linia 352) dodaj `xml_feed_url` do listy kolumn.
|
||||
- Walidacja: jeśli `$xml_feed_url` nie jest pusty, sprawdź `filter_var($xml_feed_url, FILTER_VALIDATE_URL)`. Przy błędzie — `\S::alert('Niepoprawny URL feedu XML')` i return.
|
||||
|
||||
Avoid: dodawanie nowych zależności frontend; używaj istniejącego stylu form-control.
|
||||
</action>
|
||||
<verify>
|
||||
Otwarcie dialogu edycji klienta pokazuje pole "XML Feed URL";
|
||||
Zapis URL i ponowne otwarcie dialogu pokazuje wprowadzoną wartość;
|
||||
Pusty zapis czyści wartość w DB (NULL);
|
||||
Niepoprawny URL pokazuje alert i nie zapisuje.
|
||||
</verify>
|
||||
<done>AC-2 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Service XmlFeedImporter (streaming, batched) + integracja z cron_universal</name>
|
||||
<files>autoload/services/class.XmlFeedImporter.php, autoload/controls/class.Cron.php</files>
|
||||
<action>
|
||||
1. Stwórz `autoload/services/class.XmlFeedImporter.php` (namespace `services`):
|
||||
|
||||
Sygnatura: `static public function import_for_client( int $client_id ): array` — zwraca raport `[ 'fetched' => N, 'updated' => N, 'inserted' => N, 'skipped' => N, 'errors' => [] ]`.
|
||||
|
||||
Implementacja:
|
||||
- Pobierz `clients.xml_feed_url` przez `$mdb -> get('clients', ['xml_feed_url'], ['id' => $client_id])`. Jeśli puste → return z `skipped_reason = 'no_feed'`.
|
||||
- Pobierz feed do pliku tymczasowego (`tempnam(sys_get_temp_dir(), 'xmlfeed_')`) cURL-em ze streamingiem (`CURLOPT_FILE`, `CURLOPT_TIMEOUT = 300`, follow redirects, user-agent). Dla feedów >50MB nie ładuj do pamięci.
|
||||
- Otwórz `XMLReader::open($tmp)`. Iteruj po elementach `<item>` (lub `<entry>` w Atom — wykryj namespace).
|
||||
- Dla każdego `<item>` zbuduj asocjację z odczytu `expand()` → SimpleXMLElement → wyciągnij: `g:id`, `title`, `description`, `g:price`, `g:custom_label_1` (z namespace `g:` zarejestrowanym jako `http://base.google.com/ns/1.0`).
|
||||
- Buforuj wpisy w tablicy `$batch` o rozmiarze 200; po osiągnięciu limitu wykonaj UPSERT batch:
|
||||
- `INSERT INTO products (client_id, product_id, title_gmc, description_gmc, custom_label_1, price, created_at) VALUES (...), (...), ... ON DUPLICATE KEY UPDATE title_gmc = VALUES(title_gmc), description_gmc = VALUES(description_gmc), custom_label_1 = COALESCE(VALUES(custom_label_1), custom_label_1), price = VALUES(price)`.
|
||||
- Wykonaj w transakcji `$mdb->pdo->beginTransaction()` / `commit()`. Przy błędzie batchu — rollback, log i kontynuuj.
|
||||
- WAŻNE: nie nadpisuj pól `title` ani `description` (edytowalne/AI). Tylko `_gmc` warianty.
|
||||
- WAŻNE: warunek `ON DUPLICATE KEY` wymaga UNIQUE INDEX na `(client_id, product_id)`. Zweryfikuj że istnieje (z migracji 016/006); jeśli nie, dodaj go w migracji 029.
|
||||
- Po pętli: `gc_collect_cycles()`, usuń plik tymczasowy, zapisz `clients.xml_feed_last_sync_at = NOW()`.
|
||||
- `set_time_limit(600)` na początku, ini_set memory_limit 512M lokalnie.
|
||||
- Każdy złapany Exception z parsowania pojedynczego itemu → log do `errors[]`, `skipped++`, kontynuuj.
|
||||
|
||||
2. `autoload/controls/class.Cron.php` — w `cron_universal()`:
|
||||
- Po wywołaniu `sync_products_fetch_for_client` (ok. linia 116) dla każdego klienta dodaj:
|
||||
```php
|
||||
if ( !empty( $client['xml_feed_url'] ) )
|
||||
{
|
||||
$xml_sync = \services\XmlFeedImporter::import_for_client( (int) $client['id'] );
|
||||
// dołącz $xml_sync do raportu / cron_sync_status
|
||||
}
|
||||
```
|
||||
- Upewnij się, że SELECT klientów w cronie zwraca pole `xml_feed_url`.
|
||||
- Loguj wynik do `logs` (analogicznie do innych sync) oraz aktualizuj `cron_sync_status` jeśli stosowne.
|
||||
|
||||
Avoid:
|
||||
- `simplexml_load_file()` na całym feedzie (eksploduje pamięć dla 5000+ pozycji).
|
||||
- DOMDocument ładowany w całości — to samo.
|
||||
- Brak transakcji na batchach (przy padzie cron zostawi pół-zapisany stan).
|
||||
- Nadpisywanie `products.title` / `products.description` (to są pola edytowalne/AI, kontrolowane przez użytkownika).
|
||||
</action>
|
||||
<verify>
|
||||
`php -r "require 'index.php'; \services\XmlFeedImporter::import_for_client(1);"` przy ustawionym `xml_feed_url` dla klienta 1 kończy się bez błędu;
|
||||
Po pierwszym uruchomieniu `cron_universal` na kliencie z feedem: w `products` pojawiają się/aktualizują wiersze z wypełnionym `title_gmc`, `description_gmc`, `price`;
|
||||
`clients.xml_feed_last_sync_at` jest ustawione;
|
||||
Pamięć PHP w trakcie importu nie przekracza 256MB dla feedu z 5000 pozycji (sprawdź `memory_get_peak_usage(true)` w logu raportu).
|
||||
</verify>
|
||||
<done>AC-3 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
Pole xml_feed_url w edycji klienta + import feedu XML w cron_universal z odporną parsowalnością XMLReader.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Uruchom `php install.php` — migracja 029 przechodzi.
|
||||
2. Otwórz `/clients`, edytuj klienta, wprowadź `https://pomysloweprezenty.pl/google-feed.xml` w "XML Feed URL", zapisz.
|
||||
3. Otwórz dialog ponownie — pole jest wypełnione.
|
||||
4. Wywołaj cron: `curl https://<host>/cron/cron_universal` (lub bezpośrednio uruchom z CLI).
|
||||
5. Sprawdź w `products` że dla tego klienta są wiersze z wypełnionym `title_gmc`, `description_gmc`, `price`.
|
||||
6. Sprawdź w `clients` że `xml_feed_last_sync_at` ma świeży timestamp.
|
||||
7. Otwórz `/products` — strona renderuje się bez błędu SQL, kolumna nazwy pokazuje `title` (dawne `name`).
|
||||
8. Edytuj produkt (np. min_roas, custom_label_1) — zapis działa.
|
||||
9. Sprawdź log z importu: `errors == []` lub akceptowalne (np. pojedyncze pozycje bez g:id).
|
||||
</how-to-verify>
|
||||
<resume-signal>Wpisz "approved" aby zamknąć petle, albo opisz problemy do poprawki</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- `migrations/001`–`028` (zamknięta historia migracji)
|
||||
- `products.title` i `products.description` po renamie — to są pola EDYTOWALNE/AI, importer NIGDY ich nie nadpisuje
|
||||
- `products.id`, `products.product_id`, `products.client_id` (klucze i FK)
|
||||
- Tabele `campaigns`, `campaign_*`, `cron_sync_status` (poza dorzuceniem entry per import XML jeśli pasuje)
|
||||
- Aliasy `... AS name` w SELECTach które są publicznym kontraktem dla DataTables/JS — zachowaj kompatybilność wsteczną na poziomie kluczy w response
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Brak UI do podglądu/diagnozy treści feedu (tylko zapis URL + log w cronie)
|
||||
- Brak retry/exponential backoff dla niedostępnego feedu (jednorazowa próba per cron run)
|
||||
- Brak parsowania `<g:availability>`, `<g:brand>`, `<g:gtin>`, `<g:image_link>` itd. — tylko cztery pola z odpowiedzi (title, description, custom_label_1, price)
|
||||
- Brak supportu dla feedów Atom (tylko RSS/g: namespace) — jeśli wykryty inny format, log "unsupported_format" i skip
|
||||
- Brak harmonogramu osobnego od `cron_universal` — feed pobierany razem z synchronizacją produktów GA
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `php install.php` i `php install.php --force` działają bez błędów
|
||||
- [ ] `grep -rn "p\.name\|products\.name" autoload/ templates/products/` zwraca tylko świadome aliasy
|
||||
- [ ] `/clients` — edycja zapisuje i wczytuje xml_feed_url
|
||||
- [ ] `/products` — strona renderuje się, edycja produktu działa
|
||||
- [ ] Cron `cron_universal` na kliencie z feedem: importuje, aktualizuje `xml_feed_last_sync_at`, NIE nadpisuje `title`/`description` (edytowalnych)
|
||||
- [ ] Test wydajności: feed z 5000+ pozycji parsuje się w <60s i <256MB peak memory
|
||||
- [ ] Wszystkie AC spełnione (AC-1 do AC-4)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Pole xml_feed_url widoczne i funkcjonalne w edycji klienta
|
||||
- Cron pobiera feed XML strumieniowo i aktualizuje products w batchach z transakcjami
|
||||
- Schemat products zrefaktoryzowany (name→title, title→title_gmc, +description_gmc, +price)
|
||||
- Brak regresji w widokach `/products` i `/clients`
|
||||
- Import odporny na feedy z kilkoma tysiącami pozycji
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/06-xml-feed-import/06-01-SUMMARY.md`
|
||||
</output>
|
||||
168
.paul/phases/06-xml-feed-import/06-01-SUMMARY.md
Normal file
168
.paul/phases/06-xml-feed-import/06-01-SUMMARY.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
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*
|
||||
Reference in New Issue
Block a user