update
This commit is contained in:
@@ -1,56 +1,21 @@
|
|||||||
# STATE
|
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: (ad-hoc) Products - XML feed import
|
Phase: 07-xml-feed-cron-refresh — Complete
|
||||||
Phase: 6 of 6 (XML Feed Import) - Completed
|
Plan: 07-01 complete
|
||||||
Plan: 06-01 unified (loop closed)
|
Status: UNIFY complete. Phase complete — ready for next phase.
|
||||||
Status: UNIFY complete
|
Last activity: 2026-04-30T07:19:05.331Z
|
||||||
Last activity: 2026-04-30T01:15:00Z - Zamknieto petle planu 06-01
|
|
||||||
|
|
||||||
Progress:
|
|
||||||
- Milestone: [██████████] 100%
|
|
||||||
- Phase 6: [██████████] 100%
|
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN --> APPLY --> UNIFY
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ✓ ✓ [Petla zamknieta]
|
✓ ✓ ✓ [Phase complete — ready for next phase]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-04-30
|
Last session: 2026-04-30
|
||||||
Stopped at: Loop closed, gotowe do nowego /paul:plan
|
Stopped at: Plan 07-01 complete
|
||||||
Next action: Jesli chcesz kontynuowac - uruchom /paul:plan z kolejnym zadaniem
|
Next action: paul_workflow('plan') for next phase
|
||||||
Resume file: .paul/phases/06-xml-feed-import/06-01-SUMMARY.md
|
Resume file: .paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md
|
||||||
|
|
||||||
## Historia zrealizowanych planow
|
|
||||||
|
|
||||||
- `01-01-PLAN.md` - CL3 -> CL1 w tabeli /products (completed 2026-04-22)
|
|
||||||
- `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
|
|
||||||
|
|
||||||
| Date | Decision | Phase | Impact |
|
|
||||||
|------|----------|-------|--------|
|
|
||||||
| 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).
|
|
||||||
- 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).
|
|
||||||
59
.paul/STATE.md.bak
Normal file
59
.paul/STATE.md.bak
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# STATE
|
||||||
|
|
||||||
|
## Current Position
|
||||||
|
|
||||||
|
Milestone: (ad-hoc) Products - XML feed import
|
||||||
|
Phase: 7 of 7 (XML Feed Cron Refresh) - Planning
|
||||||
|
Plan: 07-01 created, awaiting approval
|
||||||
|
Status: APPLY complete — 3/3 PASS, ready for UNIFY
|
||||||
|
Last activity: 2026-04-30T09:20:00+02:00 - Created .paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md
|
||||||
|
|
||||||
|
Progress:
|
||||||
|
- Milestone: [#########-] 90%
|
||||||
|
- Phase 7: [----------] 0%
|
||||||
|
|
||||||
|
## Loop Position
|
||||||
|
|
||||||
|
Current loop state:
|
||||||
|
```
|
||||||
|
PLAN --> APPLY --> UNIFY
|
||||||
|
* o o [Plan created, awaiting approval]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Continuity
|
||||||
|
|
||||||
|
Last session: 2026-04-30
|
||||||
|
Stopped at: Plan 07-01 created
|
||||||
|
Next action: Review and approve plan, then run $paul-apply 07-01
|
||||||
|
Resume file: .paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md
|
||||||
|
|
||||||
|
## Historia zrealizowanych planow
|
||||||
|
|
||||||
|
- `01-01-PLAN.md` - CL3 -> CL1 w tabeli /products (completed 2026-04-22)
|
||||||
|
- `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
|
||||||
|
|
||||||
|
| Date | Decision | Phase | Impact |
|
||||||
|
|------|----------|-------|--------|
|
||||||
|
| 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 |
|
||||||
|
| 2026-04-30 | XML feed ma byc osobnym cronem `/cron/cron_xml_feed_import`, niezaleznym od `cron_universal`, z recznym odswiezeniem w `/clients` | 7 | Czytelny harmonogram, brak dublowania importu z produktami Google Ads, latwiejsza diagnostyka feedu |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- PAUL framework dziala tutaj w trybie ad-hoc (bez ROADMAP.md i PROJECT.md).
|
||||||
|
- 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).
|
||||||
|
- Plan 07-01 obejmuje wydzielenie XML feed z `cron_universal`, reczny refresh w `/clients` i diagnostyke `offer_id=2084` dla `pomysloweprezenty.pl`.
|
||||||
@@ -2,28 +2,16 @@
|
|||||||
|
|
||||||
## Co zrobiono
|
## Co zrobiono
|
||||||
|
|
||||||
- [Phase 6, Plan 06-01] XML feed import per klient + refaktor schematu products (source/edit split) + integracja z cron_universal
|
- [07-xml-feed-cron-refresh, Plan 01]
|
||||||
- Dodano pole `xml_feed_url` w edycji klienta (templates/clients/main_view.php) z walidacja URL po stronie serwera
|
- Task 1: Wydziel osobny cron XML feed
|
||||||
- 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)
|
- Task 2: Dodaj reczne odswiezanie XML Feed w /clients
|
||||||
- Nowy serwis `\services\XmlFeedImporter` - XMLReader streaming + batche 200 w transakcjach + manual upsert (SELECT IN + UPDATE/INSERT); odporny na feedy 5000+ pozycji
|
- Task 3: Zdiagnozuj i napraw custom_label_1 dla pomysloweprezenty.pl offer_id 2084
|
||||||
- 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
|
## 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.Cron.php`
|
||||||
|
- `autoload/controls/class.Users.php`
|
||||||
- `autoload/controls/class.Clients.php`
|
- `autoload/controls/class.Clients.php`
|
||||||
- `autoload/services/class.SupplementalFeed.php`
|
|
||||||
- `templates/clients/main_view.php`
|
- `templates/clients/main_view.php`
|
||||||
- `api.php`
|
- `autoload/services/class.XmlFeedImporter.php`
|
||||||
- `.paul/phases/06-xml-feed-import/06-01-PLAN.md`
|
- `autoload/controls/class.Cron.php`
|
||||||
- `.paul/phases/06-xml-feed-import/06-01-SUMMARY.md`
|
|
||||||
- `.paul/STATE.md`
|
|
||||||
208
.paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md
Normal file
208
.paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
phase: 07-xml-feed-cron-refresh
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- autoload/controls/class.Cron.php
|
||||||
|
- autoload/controls/class.Clients.php
|
||||||
|
- autoload/controls/class.Users.php
|
||||||
|
- templates/clients/main_view.php
|
||||||
|
- autoload/services/class.XmlFeedImporter.php
|
||||||
|
autonomous: true
|
||||||
|
delegation: off
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Wydzielic import danych produktow z XML feed klienta do osobnego crona `/cron/cron_xml_feed_import`, dodac reczne odswiezanie XML feed w `/clients` oraz zdiagnozowac i naprawic brak `custom_label_1` dla produktu `offer_id=2084` klienta `pomysloweprezenty.pl`.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Import XML feed jest niezalezny od pobierania statystyk produktow z Google Ads. Trzymanie go w `cron_universal` utrudnia diagnostyke, harmonogram i reczne odswiezanie z poziomu klienta. Osobny cron daje kontrolowany punkt uruchomienia i jasny raport, a analiza `offer_id=2084` ma potwierdzic, czy problem wynika z endpointu, mapowania klienta, parsera XML, duplikatow w `products`, czy braku pola w feedzie.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Nowa akcja `\controls\Cron::cron_xml_feed_import()` dostepna jako `/cron/cron_xml_feed_import`.
|
||||||
|
- Usuniecie automatycznego importu XML z `cron_universal`.
|
||||||
|
- Nowa opcja w menu odswiezania klienta na `/clients`: XML Feed.
|
||||||
|
- `Clients::force_sync` uruchamia import XML natychmiast dla wskazanego klienta i zwraca raport JSON.
|
||||||
|
- Diagnostyka i ewentualny fix pobierania `custom_label_1` dla `pomysloweprezenty.pl`, `offer_id=2084`.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
<clarifications>
|
||||||
|
- **Endpoint** - Jaki endpoint ma miec osobny cron XML?
|
||||||
|
-> Odpowiedz: `/cron/cron_xml_feed_import`, metoda `\controls\Cron::cron_xml_feed_import()`.
|
||||||
|
- **Zakres diagnozy** - Czy produkt `offer_id=2084` klienta `pomysloweprezenty.pl` robimy w tym samym planie?
|
||||||
|
-> Odpowiedz: Tak, w tym samym planie.
|
||||||
|
- **Cron universal** - Czy zostawic import XML jako czesc `cron_universal`?
|
||||||
|
-> Odpowiedz: Nie. XML feed ma byc niezalezny od pobierania produktow.
|
||||||
|
- **UI clients** - Gdzie ma byc mozliwosc odswiezenia?
|
||||||
|
-> Odpowiedz: W `/clients`, w obecnym miejscu odswiezania danych klienta.
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/phases/06-xml-feed-import/06-01-SUMMARY.md
|
||||||
|
@CLAUDE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@autoload/controls/class.Cron.php
|
||||||
|
@autoload/controls/class.Clients.php
|
||||||
|
@autoload/controls/class.Users.php
|
||||||
|
@templates/clients/main_view.php
|
||||||
|
@autoload/services/class.XmlFeedImporter.php
|
||||||
|
@autoload/factory/class.Clients.php
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Osobny cron XML feed
|
||||||
|
```gherkin
|
||||||
|
Given klient ma ustawiony `clients.xml_feed_url`
|
||||||
|
When wywolam `/cron/cron_xml_feed_import` bez `client_id`
|
||||||
|
Then cron wybiera aktywnego klienta z `xml_feed_url` i uruchamia `XmlFeedImporter::import_for_client()`
|
||||||
|
And zwraca JSON z `client_id`, `feed_url`, `fetched`, `updated`, `inserted`, `skipped`, `errors`, `peak_memory_mb`, `duration_ms`
|
||||||
|
And ustawia `clients.xml_feed_last_sync_at` przez importer po udanym przebiegu
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Reczne odswiezanie XML feed w /clients
|
||||||
|
```gherkin
|
||||||
|
Given jestem na `https://adspro.projectpro.pl/clients`
|
||||||
|
When rozwijam menu odswiezania przy kliencie z `xml_feed_url`
|
||||||
|
Then widze opcje `XML Feed`
|
||||||
|
When klikam `XML Feed`
|
||||||
|
Then aplikacja uruchamia import natychmiast dla tego klienta
|
||||||
|
And pokazuje komunikat z liczba pobranych/zaktualizowanych/dodanych/pominietych pozycji albo czytelny blad
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: XML feed niezalezny od cron_universal
|
||||||
|
```gherkin
|
||||||
|
Given uruchamia sie `/cron/cron_universal`
|
||||||
|
When cron synchronizuje kampanie i produkty Google Ads
|
||||||
|
Then nie wywoluje `XmlFeedImporter::import_for_client()`
|
||||||
|
And response `cron_universal` nie zawiera juz importu XML jako czesci sync produktow
|
||||||
|
And import XML mozna uruchomic tylko przez `/cron/cron_xml_feed_import` albo reczna akcje `/clients`
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Diagnoza i fix custom_label_1 dla offer_id 2084
|
||||||
|
```gherkin
|
||||||
|
Given klient `pomysloweprezenty.pl` ma skonfigurowany XML feed
|
||||||
|
When uruchomie import XML dla tego klienta
|
||||||
|
Then importer poprawnie odczytuje `g:custom_label_1` dla pozycji `g:id=2084`, jesli pole istnieje w feedzie
|
||||||
|
And rekord albo rekordy `products` tego klienta z `offer_id='2084'` maja uzupelnione `custom_label_1`
|
||||||
|
And jesli feed nie zawiera tej wartosci, raport diagnostyczny jasno wskazuje `custom_label_1_missing_in_feed`
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Wydziel osobny cron XML feed</name>
|
||||||
|
<files>autoload/controls/class.Cron.php, autoload/controls/class.Users.php</files>
|
||||||
|
<action>
|
||||||
|
Dodaj publiczna metode `cron_xml_feed_import()` w `autoload/controls/class.Cron.php`.
|
||||||
|
|
||||||
|
Wymagane zachowanie:
|
||||||
|
- Ustaw `self::$current_cron_action = __FUNCTION__` i wywolaj `self::touch_cron_invocation(__FUNCTION__)`.
|
||||||
|
- Obsluz `client_id` z requestu: jesli podany, importuj dokladnie tego aktywnego klienta z niepustym `xml_feed_url`.
|
||||||
|
- Bez `client_id` wybierz jednego aktywnego klienta z niepustym `xml_feed_url`, najlepiej po `xml_feed_last_sync_at ASC` z `NULL` jako pierwsze. To ogranicza czas jednego uruchomienia i pasuje do istniejacych cronow per klient.
|
||||||
|
- Uruchom `\services\XmlFeedImporter::import_for_client((int) $client['id'])` i zwroc `self::output_cron_response()` z raportem.
|
||||||
|
- Nie wymagaj Google Ads API ani Merchant API do importu XML.
|
||||||
|
- Dodaj endpoint do dashboardu cron w `autoload/controls/class.Users.php` jako `/cron/cron_xml_feed_import` z opisem harmonogramu.
|
||||||
|
|
||||||
|
Usun wywolania `XmlFeedImporter::import_for_client()` z `cron_universal`, z obu sciezek: wymuszonego `client_id` i automatycznego pipeline, jezeli wystepuja.
|
||||||
|
|
||||||
|
Avoid: laczenie statusu XML z `cron_sync_status` pipeline `products`, bo XML feed ma byc niezaleznym procesem.
|
||||||
|
</action>
|
||||||
|
<verify>`php -l autoload/controls/class.Cron.php` i `php -l autoload/controls/class.Users.php`; grep `XmlFeedImporter::import_for_client` pokazuje wywolanie w `cron_xml_feed_import` i ewentualnie w `Clients::force_sync`, ale nie w `cron_universal`.</verify>
|
||||||
|
<done>AC-1 i AC-3 satisfied</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Dodaj reczne odswiezanie XML Feed w /clients</name>
|
||||||
|
<files>autoload/controls/class.Clients.php, templates/clients/main_view.php</files>
|
||||||
|
<action>
|
||||||
|
Rozszerz istniejace menu odswiezania w `templates/clients/main_view.php`:
|
||||||
|
- Dla klienta z niepustym `xml_feed_url` pokaz przycisk `XML Feed` w dropdownie `sync-dropdown-menu`.
|
||||||
|
- Uzyj istniejacego `syncFromMenu(id, pipeline, btn)` z nowym `pipeline = 'xml_feed'`.
|
||||||
|
- Dodaj etykiete `xml_feed: 'XML feedu'` w obiekcie `labels`.
|
||||||
|
- Dla odpowiedzi natychmiastowej pokaz komunikat z liczbami z raportu: fetched/updated/inserted/skipped; przy bledach pokaz czytelny komunikat.
|
||||||
|
|
||||||
|
W `autoload/controls/class.Clients.php` znajdz metode `force_sync` i dodaj obsluge `pipeline === 'xml_feed'`:
|
||||||
|
- Waliduj klienta, aktywnosc i obecne `xml_feed_url`.
|
||||||
|
- Uruchom `\services\XmlFeedImporter::import_for_client($id)` natychmiast.
|
||||||
|
- Zwroc JSON `{ success: true, immediate: true, pipeline: 'xml_feed', report: ... }` albo `{ success: false, message: ... }`.
|
||||||
|
- Nie kolejkuj tego przez `cron_sync_status`, bo wymaganie mowi o niezaleznym odswiezeniu w `/clients`.
|
||||||
|
|
||||||
|
Avoid: wymagania `google_ads_customer_id` dla przycisku XML feed. Do XML wystarczy aktywny klient i `xml_feed_url`.
|
||||||
|
</action>
|
||||||
|
<verify>`php -l autoload/controls/class.Clients.php`; reczne klikniecie `/clients` -> XML Feed zwraca alert z raportem i aktualizuje `clients.xml_feed_last_sync_at`.</verify>
|
||||||
|
<done>AC-2 satisfied</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Zdiagnozuj i napraw custom_label_1 dla pomysloweprezenty.pl offer_id 2084</name>
|
||||||
|
<files>autoload/services/class.XmlFeedImporter.php, autoload/controls/class.Cron.php</files>
|
||||||
|
<action>
|
||||||
|
Dodaj minimalna diagnostyke ukierunkowana na przypadki braku pola po imporcie, bez stalego logowania kazdego produktu:
|
||||||
|
- Dodaj opcjonalny parametr requestu dla crona, np. `debug_offer_id=2084` albo `offer_id=2084`, ktory przekazesz do importera tylko w raporcie diagnostycznym.
|
||||||
|
- W `XmlFeedImporter` dodaj lekka obsluge diagnostyczna: kiedy parsowany `offer_id` odpowiada debugowanemu ID, zapisz w raporcie `debug_offer` z polami: `found_in_feed`, `custom_label_1_raw_present`, `custom_label_1_value`, `title_present`, `matched_existing_rows`, `updated_rows`.
|
||||||
|
- Sprawdz parser `extract_item_fields()`: ma obslugiwac `g:custom_label_1` w namespace `http://base.google.com/ns/1.0`; jesli realny feed uzywa innej formy (np. bez namespace albo z prefiksem zachowanym jako nazwa), dodaj punktowa obsluge fallback bez ladowania calego XML do pamieci.
|
||||||
|
- Sprawdz `flush_batch()`: dla istniejacych rekordow z `offer_id=2084` `custom_label_1` ma sie aktualizowac, gdy wartosc z feedu nie jest pusta. Jesli feed ma pusta wartosc, nie nadpisuj istniejacej wartosci pustka.
|
||||||
|
- Jesli klient `pomysloweprezenty.pl` ma wiele rekordow `products` z tym samym `offer_id`, zachowaj aktualne zalozenie z fazy 06: aktualizuj wszystkie legacy duplikaty.
|
||||||
|
|
||||||
|
Po implementacji uruchom lub opisz weryfikacje:
|
||||||
|
`/cron/cron_xml_feed_import/client_id=<ID_KLIENTA>&debug_offer_id=2084`
|
||||||
|
i sprawdz w raporcie, czy `debug_offer` wskazuje przyczyne.
|
||||||
|
|
||||||
|
Avoid: hardcodowania domeny `pomysloweprezenty.pl` albo `2084` w logice produkcyjnej. To ma byc parametr diagnostyczny.
|
||||||
|
</action>
|
||||||
|
<verify>`php -l autoload/services/class.XmlFeedImporter.php`; import dla klienta `pomysloweprezenty.pl` z `debug_offer_id=2084` zwraca `debug_offer`; po imporcie SELECT po `client_id` i `offer_id='2084'` pokazuje uzupelnione `custom_label_1` albo raport jasno pokazuje brak wartosci w feedzie.</verify>
|
||||||
|
<done>AC-4 satisfied</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- `migrations/001`-`029` bez wyraznej potrzeby; ten plan nie wymaga zmiany schematu.
|
||||||
|
- Semantyka z fazy 06: `title`/`description` sa danymi zrodlowymi, `title_gmc`/`description_gmc` sa edytowalne i ida do supplemental feed.
|
||||||
|
- Importer nie moze nadpisywac `custom_label_1` pustka z feedu.
|
||||||
|
- Nie laczyc XML feed z Google Ads API ani Merchant API.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Brak nowego dashboardu historii importow XML.
|
||||||
|
- Brak kolejki `cron_sync_status` dla XML feed.
|
||||||
|
- Brak deduplikacji `products` dla legacy duplikatow `(client_id, offer_id)`.
|
||||||
|
- Brak nowych pol z feedu poza diagnostyka `custom_label_1`.
|
||||||
|
- Brak przebudowy calego UI `/clients`; tylko dodatkowa opcja w istniejacym dropdownie.
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `php -l autoload/controls/class.Cron.php`
|
||||||
|
- [ ] `php -l autoload/controls/class.Clients.php`
|
||||||
|
- [ ] `php -l autoload/services/class.XmlFeedImporter.php`
|
||||||
|
- [ ] `rg -n "XmlFeedImporter::import_for_client" autoload/controls/class.Cron.php autoload/controls/class.Clients.php` potwierdza brak wywolania w `cron_universal`
|
||||||
|
- [ ] `/cron/cron_xml_feed_import?client_id=<id>` zwraca raport importu bez wymagania Google Ads API
|
||||||
|
- [ ] `/clients` pokazuje opcje `XML Feed` dla klienta z `xml_feed_url`
|
||||||
|
- [ ] Klikniecie `XML Feed` w `/clients` wykonuje import natychmiast i pokazuje raport
|
||||||
|
- [ ] Debug `offer_id=2084` dla `pomysloweprezenty.pl` wskazuje przyczyne braku `custom_label_1` albo naprawia zapis
|
||||||
|
- [ ] Wszystkie AC-1 do AC-4 spelnione
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- XML feed ma osobny endpoint cron i nie jest czescia `cron_universal`.
|
||||||
|
- Klient moze recznie odswiezyc XML feed z `/clients`.
|
||||||
|
- Import XML dziala bez konfiguracji Google Ads/Merchant API.
|
||||||
|
- Problem `custom_label_1` dla `offer_id=2084` jest zdiagnozowany i naprawiony, jezeli wartosc istnieje w feedzie.
|
||||||
|
- Brak regresji w dotychczasowym pobieraniu kampanii i produktow Google Ads.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
40
.paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md
Normal file
40
.paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
phase: 07-xml-feed-cron-refresh
|
||||||
|
plan: 01
|
||||||
|
completed: 2026-04-30T07:19:05.331Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 07-01 Summary
|
||||||
|
|
||||||
|
****
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status |
|
||||||
|
|-----------|--------|
|
||||||
|
| Task 1: Wydziel osobny cron XML feed | Pass — Dodano Cron::cron_xml_feed_import, endpoint w dashboardzie cron, usunieto wywolanie XmlFeedImporter z cron_universal. Lint OK; grep potwierdza wywolania tylko w nowym cronie i Clients::force_sync. |
|
||||||
|
| Task 2: Dodaj reczne odswiezanie XML Feed w /clients | Pass — Dodano opcje XML Feed w dropdownie /clients oraz obsluge pipeline xml_feed w Clients::force_sync z natychmiastowym importem i raportem. Lint OK. |
|
||||||
|
| Task 3: Zdiagnozuj i napraw custom_label_1 dla pomysloweprezenty.pl offer_id 2084 | Pass — Dodano debug_offer_id/offer_id, raport debug_offer, fallback odczytu custom_label_1 po localName oraz metryki matched_existing_rows/updated_rows. Runtime import/SELECT nie zostal wykonany, bo lokalny MySQL odmawia polaczenia. |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Task 1: Wydziel osobny cron XML feed: Dodano Cron::cron_xml_feed_import, endpoint w dashboardzie cron, usunieto wywolanie XmlFeedImporter z cron_universal. Lint OK; grep potwierdza wywolania tylko w nowym cronie i Clients::force_sync.
|
||||||
|
- Task 2: Dodaj reczne odswiezanie XML Feed w /clients: Dodano opcje XML Feed w dropdownie /clients oraz obsluge pipeline xml_feed w Clients::force_sync z natychmiastowym importem i raportem. Lint OK.
|
||||||
|
- Task 3: Zdiagnozuj i napraw custom_label_1 dla pomysloweprezenty.pl offer_id 2084: Dodano debug_offer_id/offer_id, raport debug_offer, fallback odczytu custom_label_1 po localName oraz metryki matched_existing_rows/updated_rows. Runtime import/SELECT nie zostal wykonany, bo lokalny MySQL odmawia polaczenia.
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- `autoload/controls/class.Users.php`
|
||||||
|
- `autoload/controls/class.Clients.php`
|
||||||
|
- `templates/clients/main_view.php`
|
||||||
|
- `autoload/services/class.XmlFeedImporter.php`
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Runtime verification against local DB was not possible: local MySQL refused connection, so `/cron/cron_xml_feed_import?client_id=<id>&debug_offer_id=2084` and SELECT for offer_id=2084 must be verified on the target environment.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 07-xml-feed-cron-refresh, Plan: 01*
|
||||||
|
*Completed: 2026-04-30*
|
||||||
119
.vscode/ftp-kr.sync.cache.json
vendored
119
.vscode/ftp-kr.sync.cache.json
vendored
@@ -9,8 +9,8 @@
|
|||||||
},
|
},
|
||||||
"api.php": {
|
"api.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 30668,
|
"size": 33086,
|
||||||
"lmtime": 1777417113752,
|
"lmtime": 1777532181358,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -95,14 +95,14 @@
|
|||||||
},
|
},
|
||||||
"class.Clients.php": {
|
"class.Clients.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 13909,
|
"size": 14217,
|
||||||
"lmtime": 1772117575587,
|
"lmtime": 1777532181359,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.Cron.php": {
|
"class.Cron.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 184760,
|
"size": 185385,
|
||||||
"lmtime": 1774557774097,
|
"lmtime": 1777532181362,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.FacebookAds.php": {
|
"class.FacebookAds.php": {
|
||||||
@@ -125,8 +125,8 @@
|
|||||||
},
|
},
|
||||||
"class.Products.php": {
|
"class.Products.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 52633,
|
"size": 55416,
|
||||||
"lmtime": 1776810435280,
|
"lmtime": 1777532181364,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.Site.php": {
|
"class.Site.php": {
|
||||||
@@ -193,8 +193,8 @@
|
|||||||
},
|
},
|
||||||
"class.Products.php": {
|
"class.Products.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 44169,
|
"size": 47877,
|
||||||
"lmtime": 1776810339287,
|
"lmtime": 1777532181365,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.Users.php": {
|
"class.Users.php": {
|
||||||
@@ -243,8 +243,14 @@
|
|||||||
},
|
},
|
||||||
"class.SupplementalFeed.php": {
|
"class.SupplementalFeed.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 10533,
|
"size": 10874,
|
||||||
"lmtime": 1776844880606,
|
"lmtime": 1777532181367,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"class.XmlFeedImporter.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 11131,
|
||||||
|
"lmtime": 1777532181367,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -731,17 +737,23 @@
|
|||||||
"lmtime": 1772671313573,
|
"lmtime": 1772671313573,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"demo_data.sql": {
|
|
||||||
"type": "-",
|
|
||||||
"size": 21146,
|
|
||||||
"lmtime": 0,
|
|
||||||
"modified": true
|
|
||||||
},
|
|
||||||
"028_products_custom_label_1.sql": {
|
"028_products_custom_label_1.sql": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 527,
|
"size": 527,
|
||||||
"lmtime": 1776810261993,
|
"lmtime": 1776810261993,
|
||||||
"modified": false
|
"modified": false
|
||||||
|
},
|
||||||
|
"029_products_rename_columns_and_xml_feed.sql": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 4361,
|
||||||
|
"lmtime": 1777532181369,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"demo_data.sql": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 21146,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
".paul": {
|
".paul": {
|
||||||
@@ -757,6 +769,24 @@
|
|||||||
"size": 742,
|
"size": 742,
|
||||||
"lmtime": 1777064721127,
|
"lmtime": 1777064721127,
|
||||||
"modified": false
|
"modified": false
|
||||||
|
},
|
||||||
|
"2026-04-25.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 439,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"2026-04-29.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1140,
|
||||||
|
"lmtime": 1777532181346,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"2026-04-30.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1995,
|
||||||
|
"lmtime": 1777532181346,
|
||||||
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
@@ -815,6 +845,18 @@
|
|||||||
"size": 1614,
|
"size": 1614,
|
||||||
"lmtime": 1777064743676,
|
"lmtime": 1777064743676,
|
||||||
"modified": false
|
"modified": false
|
||||||
|
},
|
||||||
|
"governance_2026-04-29.jsonl": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 5115,
|
||||||
|
"lmtime": 1777532181347,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"governance_2026-04-30.jsonl": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 14402,
|
||||||
|
"lmtime": 1777532181349,
|
||||||
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"phases": {
|
"phases": {
|
||||||
@@ -859,12 +901,41 @@
|
|||||||
"lmtime": 1777064708436,
|
"lmtime": 1777064708436,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"04-products-aggregate-breakdown": {},
|
||||||
|
"05-products-scope-history-delete": {
|
||||||
|
"05-01-PLAN.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 17658,
|
||||||
|
"lmtime": 1777532181351,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"05-01-SUMMARY.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 8203,
|
||||||
|
"lmtime": 1777532181351,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"06-xml-feed-import": {
|
||||||
|
"06-01-PLAN.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 19931,
|
||||||
|
"lmtime": 1777532181353,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"06-01-SUMMARY.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 11842,
|
||||||
|
"lmtime": 1777532181354,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"STATE.md": {
|
"STATE.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 2531,
|
"size": 3487,
|
||||||
"lmtime": 1777064743120,
|
"lmtime": 1777532181345,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -897,8 +968,8 @@
|
|||||||
"clients": {
|
"clients": {
|
||||||
"main_view.php": {
|
"main_view.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 16977,
|
"size": 17449,
|
||||||
"lmtime": 1772117630298,
|
"lmtime": 1777532181370,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -923,8 +994,8 @@
|
|||||||
"products": {
|
"products": {
|
||||||
"main_view.php": {
|
"main_view.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 83538,
|
"size": 92780,
|
||||||
"lmtime": 1777064546782,
|
"lmtime": 1777532181372,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"product_history.php": {
|
"product_history.php": {
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ class Clients
|
|||||||
|
|
||||||
$deleted_select = self::clients_has_deleted_column() ? 'COALESCE(deleted, 0) AS deleted' : '0 AS deleted';
|
$deleted_select = self::clients_has_deleted_column() ? 'COALESCE(deleted, 0) AS deleted' : '0 AS deleted';
|
||||||
$client = $mdb -> query(
|
$client = $mdb -> query(
|
||||||
"SELECT id, COALESCE(active, 0) AS active, " . $deleted_select . ", google_ads_customer_id, google_merchant_account_id, facebook_ads_account_id
|
"SELECT id, COALESCE(active, 0) AS active, " . $deleted_select . ", google_ads_customer_id, google_merchant_account_id, facebook_ads_account_id, xml_feed_url
|
||||||
FROM clients
|
FROM clients
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
LIMIT 1",
|
LIMIT 1",
|
||||||
@@ -440,6 +440,33 @@ class Clients
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if ( $pipeline === 'xml_feed' )
|
||||||
|
{
|
||||||
|
$xml_feed_url = trim( (string) ( $client['xml_feed_url'] ?? '' ) );
|
||||||
|
if ( $xml_feed_url === '' )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'success' => false, 'message' => 'Klient nie ma ustawionego XML Feed URL.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$report = \services\XmlFeedImporter::import_for_client( $id );
|
||||||
|
echo json_encode( [
|
||||||
|
'success' => empty( $report['errors'] ),
|
||||||
|
'pipeline' => 'xml_feed',
|
||||||
|
'immediate' => true,
|
||||||
|
'report' => $report,
|
||||||
|
'message' => !empty( $report['errors'] ) ? implode( '; ', array_slice( (array) $report['errors'], 0, 3 ) ) : ''
|
||||||
|
] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
catch ( \Throwable $e )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'success' => false, 'message' => 'Blad importu XML feed: ' . $e -> getMessage() ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Domyslny reset (wszystkie pipeline oparte o cron_sync_status).
|
// Domyslny reset (wszystkie pipeline oparte o cron_sync_status).
|
||||||
|
|||||||
@@ -132,23 +132,6 @@ class Cron
|
|||||||
$products_temp_rows_total += (int) self::rebuild_products_temp_for_client( (int) $client['id'] );
|
$products_temp_rows_total += (int) self::rebuild_products_temp_for_client( (int) $client['id'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
$xml_feed_report = null;
|
|
||||||
if ( !empty( $client['xml_feed_url'] ) )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$xml_feed_report = \services\XmlFeedImporter::import_for_client( (int) $client['id'] );
|
|
||||||
if ( !empty( $xml_feed_report['errors'] ) )
|
|
||||||
{
|
|
||||||
$products_errors = array_merge( $products_errors, (array) $xml_feed_report['errors'] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch ( \Throwable $e )
|
|
||||||
{
|
|
||||||
$products_errors[] = 'XML feed import: ' . $e -> getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$errors = array_merge( $campaign_errors, $products_errors );
|
$errors = array_merge( $campaign_errors, $products_errors );
|
||||||
|
|
||||||
self::output_cron_response( [
|
self::output_cron_response( [
|
||||||
@@ -169,7 +152,6 @@ class Cron
|
|||||||
'products_fetch_skipped_reasons' => array_keys( $products_fetch_skipped_reasons ),
|
'products_fetch_skipped_reasons' => array_keys( $products_fetch_skipped_reasons ),
|
||||||
'history_30_products' => $history_30_products_total,
|
'history_30_products' => $history_30_products_total,
|
||||||
'products_temp_rows' => $products_temp_rows_total,
|
'products_temp_rows' => $products_temp_rows_total,
|
||||||
'xml_feed' => $xml_feed_report,
|
|
||||||
'errors' => $errors
|
'errors' => $errors
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
@@ -567,6 +549,87 @@ class Cron
|
|||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function cron_xml_feed_import()
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
self::$current_cron_action = __FUNCTION__;
|
||||||
|
self::touch_cron_invocation( __FUNCTION__ );
|
||||||
|
|
||||||
|
$clients_not_deleted_sql_c = self::sql_clients_not_deleted( 'c' );
|
||||||
|
$client_id = (int) \S::get( 'client_id' );
|
||||||
|
$debug_offer_id = trim( (string) \S::get( 'debug_offer_id' ) );
|
||||||
|
if ( $debug_offer_id === '' )
|
||||||
|
{
|
||||||
|
$debug_offer_id = trim( (string) \S::get( 'offer_id' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
$where_client = '';
|
||||||
|
if ( $client_id > 0 )
|
||||||
|
{
|
||||||
|
$where_client = 'AND c.id = :client_id';
|
||||||
|
$params[':client_id'] = $client_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = $mdb -> query(
|
||||||
|
"SELECT c.id, c.name, c.xml_feed_url, c.xml_feed_last_sync_at
|
||||||
|
FROM clients c
|
||||||
|
WHERE " . $clients_not_deleted_sql_c . "
|
||||||
|
AND COALESCE(c.active, 0) = 1
|
||||||
|
AND TRIM(COALESCE(c.xml_feed_url, '')) <> ''
|
||||||
|
" . $where_client . "
|
||||||
|
ORDER BY CASE WHEN c.xml_feed_last_sync_at IS NULL THEN 0 ELSE 1 END ASC,
|
||||||
|
c.xml_feed_last_sync_at ASC,
|
||||||
|
c.id ASC
|
||||||
|
LIMIT 1",
|
||||||
|
$params
|
||||||
|
) -> fetch( \PDO::FETCH_ASSOC );
|
||||||
|
|
||||||
|
if ( !$client )
|
||||||
|
{
|
||||||
|
self::output_cron_response( [
|
||||||
|
'result' => $client_id > 0 ? 'Nie znaleziono aktywnego klienta z ustawionym XML Feed URL.' : 'Brak aktywnych klientow z ustawionym XML Feed URL.',
|
||||||
|
'client_id' => $client_id ?: null,
|
||||||
|
'errors' => []
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$report = \services\XmlFeedImporter::import_for_client(
|
||||||
|
(int) $client['id'],
|
||||||
|
[ 'debug_offer_id' => $debug_offer_id ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch ( \Throwable $e )
|
||||||
|
{
|
||||||
|
self::output_cron_response( [
|
||||||
|
'result' => 'Import XML feed zakonczony bledem.',
|
||||||
|
'client_id' => (int) $client['id'],
|
||||||
|
'client_name' => (string) ( $client['name'] ?? '' ),
|
||||||
|
'feed_url' => (string) ( $client['xml_feed_url'] ?? '' ),
|
||||||
|
'errors' => [ $e -> getMessage() ]
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = (array) ( $report['errors'] ?? [] );
|
||||||
|
self::output_cron_response( [
|
||||||
|
'result' => empty( $errors ) ? 'Import XML feed zakonczony.' : 'Import XML feed zakonczony z bledami.',
|
||||||
|
'client_id' => (int) $client['id'],
|
||||||
|
'client_name' => (string) ( $client['name'] ?? '' ),
|
||||||
|
'feed_url' => (string) ( $report['feed_url'] ?? ( $client['xml_feed_url'] ?? '' ) ),
|
||||||
|
'fetched' => (int) ( $report['fetched'] ?? 0 ),
|
||||||
|
'updated' => (int) ( $report['updated'] ?? 0 ),
|
||||||
|
'inserted' => (int) ( $report['inserted'] ?? 0 ),
|
||||||
|
'skipped' => (int) ( $report['skipped'] ?? 0 ),
|
||||||
|
'peak_memory_mb' => (float) ( $report['peak_memory_mb'] ?? 0 ),
|
||||||
|
'duration_ms' => (int) ( $report['duration_ms'] ?? 0 ),
|
||||||
|
'debug_offer' => $report['debug_offer'] ?? null,
|
||||||
|
'errors' => $errors
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
static public function cron_products_urls()
|
static public function cron_products_urls()
|
||||||
{
|
{
|
||||||
global $mdb, $settings;
|
global $mdb, $settings;
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ class Users
|
|||||||
$cron_endpoints = [
|
$cron_endpoints = [
|
||||||
[ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy', 'plan' => '' ],
|
[ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy', 'plan' => '' ],
|
||||||
[ 'name' => 'Cron uniwersalny (Google Ads)', 'path' => '/cron/cron_universal', 'action' => 'cron_universal', 'plan' => 'Co 1 min: kampanie (wczoraj) + frazy/produkty (7 dni wstecz) + Merchant URL + supplemental feed (raz dziennie)' ],
|
[ 'name' => 'Cron uniwersalny (Google Ads)', 'path' => '/cron/cron_universal', 'action' => 'cron_universal', 'plan' => 'Co 1 min: kampanie (wczoraj) + frazy/produkty (7 dni wstecz) + Merchant URL + supplemental feed (raz dziennie)' ],
|
||||||
|
[ 'name' => 'Cron XML Feed produktow', 'path' => '/cron/cron_xml_feed_import', 'action' => 'cron_xml_feed_import', 'plan' => 'Co 5-15 min: jeden aktywny klient z XML Feed URL, niezaleznie od Google Ads' ],
|
||||||
[ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Co 15 min: alerty produktowe z Google Merchant' ],
|
[ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Co 15 min: alerty produktowe z Google Merchant' ],
|
||||||
[ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ],
|
[ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ],
|
||||||
[ 'name' => 'Cron Facebook Ads', 'path' => '/cron/cron_facebook_ads', 'action' => 'cron_facebook_ads', 'plan' => 'Co 5 min: 30 dni wstecz od wczoraj, blokada ponownego pobrania w tym samym dniu' ],
|
[ 'name' => 'Cron Facebook Ads', 'path' => '/cron/cron_facebook_ads', 'action' => 'cron_facebook_ads', 'plan' => 'Co 5 min: 30 dni wstecz od wczoraj, blokada ponownego pobrania w tym samym dniu' ],
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ class XmlFeedImporter
|
|||||||
* @param int $client_id
|
* @param int $client_id
|
||||||
* @return array raport z polami: feed_url, fetched, updated, inserted, skipped, errors, peak_memory_mb, duration_ms
|
* @return array raport z polami: feed_url, fetched, updated, inserted, skipped, errors, peak_memory_mb, duration_ms
|
||||||
*/
|
*/
|
||||||
static public function import_for_client( $client_id )
|
static public function import_for_client( $client_id, array $options = [] )
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
|
||||||
$client_id = (int) $client_id;
|
$client_id = (int) $client_id;
|
||||||
|
$debug_offer_id = trim( (string) ( $options['debug_offer_id'] ?? '' ) );
|
||||||
$report = [
|
$report = [
|
||||||
'feed_url' => '',
|
'feed_url' => '',
|
||||||
'fetched' => 0,
|
'fetched' => 0,
|
||||||
@@ -33,6 +34,18 @@ class XmlFeedImporter
|
|||||||
'peak_memory_mb' => 0,
|
'peak_memory_mb' => 0,
|
||||||
'duration_ms' => 0,
|
'duration_ms' => 0,
|
||||||
];
|
];
|
||||||
|
if ( $debug_offer_id !== '' )
|
||||||
|
{
|
||||||
|
$report['debug_offer'] = [
|
||||||
|
'offer_id' => $debug_offer_id,
|
||||||
|
'found_in_feed' => false,
|
||||||
|
'custom_label_1_raw_present' => false,
|
||||||
|
'custom_label_1_value' => '',
|
||||||
|
'title_present' => false,
|
||||||
|
'matched_existing_rows' => 0,
|
||||||
|
'updated_rows' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if ( $client_id <= 0 )
|
if ( $client_id <= 0 )
|
||||||
{
|
{
|
||||||
@@ -105,6 +118,22 @@ class XmlFeedImporter
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( $debug_offer_id !== '' && (string) $item['offer_id'] === $debug_offer_id )
|
||||||
|
{
|
||||||
|
$report['debug_offer']['found_in_feed'] = true;
|
||||||
|
$report['debug_offer']['custom_label_1_raw_present'] = !empty( $item['custom_label_1_raw_present'] );
|
||||||
|
$report['debug_offer']['custom_label_1_value'] = (string) ( $item['custom_label_1'] ?? '' );
|
||||||
|
$report['debug_offer']['title_present'] = trim( (string) ( $item['title'] ?? '' ) ) !== '';
|
||||||
|
if ( !$report['debug_offer']['custom_label_1_raw_present'] )
|
||||||
|
{
|
||||||
|
$report['debug_offer']['missing_reason'] = 'custom_label_1_missing_in_feed';
|
||||||
|
}
|
||||||
|
else if ( trim( (string) $item['custom_label_1'] ) === '' )
|
||||||
|
{
|
||||||
|
$report['debug_offer']['missing_reason'] = 'custom_label_1_empty_in_feed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$report['fetched']++;
|
$report['fetched']++;
|
||||||
$batch[] = $item;
|
$batch[] = $item;
|
||||||
|
|
||||||
@@ -134,6 +163,11 @@ class XmlFeedImporter
|
|||||||
$reader -> close();
|
$reader -> close();
|
||||||
@unlink( $tmp_file );
|
@unlink( $tmp_file );
|
||||||
|
|
||||||
|
if ( $debug_offer_id !== '' && empty( $report['debug_offer']['found_in_feed'] ) )
|
||||||
|
{
|
||||||
|
$report['debug_offer']['missing_reason'] = 'offer_id_missing_in_feed';
|
||||||
|
}
|
||||||
|
|
||||||
$mdb -> update( 'clients', [ 'xml_feed_last_sync_at' => date( 'Y-m-d H:i:s' ) ], [ 'id' => $client_id ] );
|
$mdb -> update( 'clients', [ 'xml_feed_last_sync_at' => date( 'Y-m-d H:i:s' ) ], [ 'id' => $client_id ] );
|
||||||
|
|
||||||
$report['peak_memory_mb'] = round( memory_get_peak_usage( true ) / 1024 / 1024, 1 );
|
$report['peak_memory_mb'] = round( memory_get_peak_usage( true ) / 1024 / 1024, 1 );
|
||||||
@@ -233,7 +267,22 @@ class XmlFeedImporter
|
|||||||
$description = trim( (string) $sxe -> description );
|
$description = trim( (string) $sxe -> description );
|
||||||
}
|
}
|
||||||
|
|
||||||
$custom_label_1 = isset( $g -> custom_label_1 ) ? trim( (string) $g -> custom_label_1 ) : '';
|
$custom_label_1_raw_present = isset( $g -> custom_label_1 );
|
||||||
|
$custom_label_1 = $custom_label_1_raw_present ? trim( (string) $g -> custom_label_1 ) : '';
|
||||||
|
if ( !$custom_label_1_raw_present && isset( $sxe -> custom_label_1 ) )
|
||||||
|
{
|
||||||
|
$custom_label_1_raw_present = true;
|
||||||
|
$custom_label_1 = trim( (string) $sxe -> custom_label_1 );
|
||||||
|
}
|
||||||
|
if ( !$custom_label_1_raw_present )
|
||||||
|
{
|
||||||
|
$custom_label_1_fallback = self::get_text_by_local_name( $doc, 'custom_label_1' );
|
||||||
|
if ( $custom_label_1_fallback !== null )
|
||||||
|
{
|
||||||
|
$custom_label_1_raw_present = true;
|
||||||
|
$custom_label_1 = trim( (string) $custom_label_1_fallback );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$price = null;
|
$price = null;
|
||||||
if ( isset( $g -> price ) )
|
if ( isset( $g -> price ) )
|
||||||
@@ -251,10 +300,25 @@ class XmlFeedImporter
|
|||||||
'title' => self::truncate( $title, 255 ),
|
'title' => self::truncate( $title, 255 ),
|
||||||
'description' => $description,
|
'description' => $description,
|
||||||
'custom_label_1' => self::truncate( $custom_label_1, 255 ),
|
'custom_label_1' => self::truncate( $custom_label_1, 255 ),
|
||||||
|
'custom_label_1_raw_present' => $custom_label_1_raw_present,
|
||||||
'price' => $price,
|
'price' => $price,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static private function get_text_by_local_name( \DOMDocument $doc, $local_name )
|
||||||
|
{
|
||||||
|
$local_name = (string) $local_name;
|
||||||
|
foreach ( $doc -> getElementsByTagName( '*' ) as $node )
|
||||||
|
{
|
||||||
|
if ( $node -> localName === $local_name )
|
||||||
|
{
|
||||||
|
return $node -> textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static private function parse_price( $raw )
|
static private function parse_price( $raw )
|
||||||
{
|
{
|
||||||
if ( $raw === '' )
|
if ( $raw === '' )
|
||||||
@@ -341,15 +405,22 @@ class XmlFeedImporter
|
|||||||
$updated_count = 0;
|
$updated_count = 0;
|
||||||
$inserted_count = 0;
|
$inserted_count = 0;
|
||||||
|
|
||||||
|
$debug_offer_id = (string) ( $report['debug_offer']['offer_id'] ?? '' );
|
||||||
|
|
||||||
foreach ( $batch as $item )
|
foreach ( $batch as $item )
|
||||||
{
|
{
|
||||||
$title = $item['title'] !== '' ? $item['title'] : null;
|
$title = $item['title'] !== '' ? $item['title'] : null;
|
||||||
$desc = $item['description'] !== '' ? $item['description'] : null;
|
$desc = $item['description'] !== '' ? $item['description'] : null;
|
||||||
$cl1 = $item['custom_label_1'] !== '' ? $item['custom_label_1'] : null;
|
$cl1 = $item['custom_label_1'] !== '' ? $item['custom_label_1'] : null;
|
||||||
$price = $item['price'];
|
$price = $item['price'];
|
||||||
|
$is_debug_offer = $debug_offer_id !== '' && (string) $item['offer_id'] === $debug_offer_id;
|
||||||
|
|
||||||
if ( !empty( $existing[ $item['offer_id'] ] ) )
|
if ( !empty( $existing[ $item['offer_id'] ] ) )
|
||||||
{
|
{
|
||||||
|
if ( $is_debug_offer )
|
||||||
|
{
|
||||||
|
$report['debug_offer']['matched_existing_rows'] = count( $existing[ $item['offer_id'] ] );
|
||||||
|
}
|
||||||
// aktualizujemy WSZYSTKIE legacy duplikaty (utrzymujemy spojnosc danych)
|
// aktualizujemy WSZYSTKIE legacy duplikaty (utrzymujemy spojnosc danych)
|
||||||
foreach ( $existing[ $item['offer_id'] ] as $row_id )
|
foreach ( $existing[ $item['offer_id'] ] as $row_id )
|
||||||
{
|
{
|
||||||
@@ -361,6 +432,10 @@ class XmlFeedImporter
|
|||||||
':id' => $row_id,
|
':id' => $row_id,
|
||||||
] );
|
] );
|
||||||
$updated_count++;
|
$updated_count++;
|
||||||
|
if ( $is_debug_offer )
|
||||||
|
{
|
||||||
|
$report['debug_offer']['updated_rows']++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -374,6 +449,10 @@ class XmlFeedImporter
|
|||||||
':price' => $price,
|
':price' => $price,
|
||||||
] );
|
] );
|
||||||
$inserted_count++;
|
$inserted_count++;
|
||||||
|
if ( $is_debug_offer )
|
||||||
|
{
|
||||||
|
$report['debug_offer']['inserted'] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,11 @@
|
|||||||
<i class="fa-brands fa-facebook-f"></i> Facebook Ads
|
<i class="fa-brands fa-facebook-f"></i> Facebook Ads
|
||||||
</button>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if ( !empty( $client['xml_feed_url'] ) ): ?>
|
||||||
|
<button type="button" onclick="syncFromMenu(<?= $client['id']; ?>, 'xml_feed', this)">
|
||||||
|
<i class="fa-solid fa-file-code"></i> XML Feed
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
||||||
@@ -183,6 +188,7 @@ function openClientForm()
|
|||||||
$( '#client-gads-id' ).val( '' );
|
$( '#client-gads-id' ).val( '' );
|
||||||
$( '#client-fbads-id' ).val( '' );
|
$( '#client-fbads-id' ).val( '' );
|
||||||
$( '#client-gmc-id' ).val( '' );
|
$( '#client-gmc-id' ).val( '' );
|
||||||
|
$( '#client-xml-feed-url' ).val( '' );
|
||||||
$( '#client-gads-start' ).val( '' );
|
$( '#client-gads-start' ).val( '' );
|
||||||
$( '#client-modal' ).fadeIn();
|
$( '#client-modal' ).fadeIn();
|
||||||
}
|
}
|
||||||
@@ -300,7 +306,8 @@ function syncFromMenu( id, pipeline, btn )
|
|||||||
products: 'produktow',
|
products: 'produktow',
|
||||||
campaigns_product_alerts_merchant: 'walidacji Merchant',
|
campaigns_product_alerts_merchant: 'walidacji Merchant',
|
||||||
supplemental_feed: 'supplemental feed',
|
supplemental_feed: 'supplemental feed',
|
||||||
facebook_ads: 'Facebook Ads'
|
facebook_ads: 'Facebook Ads',
|
||||||
|
xml_feed: 'XML feedu'
|
||||||
};
|
};
|
||||||
|
|
||||||
$.post( '/clients/force_sync', { id: id, pipeline: pipeline }, function( response )
|
$.post( '/clients/force_sync', { id: id, pipeline: pipeline }, function( response )
|
||||||
@@ -312,9 +319,23 @@ function syncFromMenu( id, pipeline, btn )
|
|||||||
|
|
||||||
if ( data.success )
|
if ( data.success )
|
||||||
{
|
{
|
||||||
var msg = data.immediate
|
var msg = '';
|
||||||
? 'Supplemental feed wygenerowany pomyslnie.'
|
if ( data.immediate && pipeline === 'xml_feed' )
|
||||||
: 'Synchronizacja ' + labels[ pipeline ] + ' zostala zakolejkowana. Dane zostana pobrane przy najblizszym uruchomieniu CRON.';
|
{
|
||||||
|
var report = data.report || {};
|
||||||
|
msg = 'XML feed odswiezony. Pobrane: ' + ( report.fetched || 0 )
|
||||||
|
+ ', zaktualizowane: ' + ( report.updated || 0 )
|
||||||
|
+ ', dodane: ' + ( report.inserted || 0 )
|
||||||
|
+ ', pominiete: ' + ( report.skipped || 0 ) + '.';
|
||||||
|
}
|
||||||
|
else if ( data.immediate )
|
||||||
|
{
|
||||||
|
msg = 'Supplemental feed wygenerowany pomyslnie.';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msg = 'Synchronizacja ' + labels[ pipeline ] + ' zostala zakolejkowana. Dane zostana pobrane przy najblizszym uruchomieniu CRON.';
|
||||||
|
}
|
||||||
|
|
||||||
$.alert({
|
$.alert({
|
||||||
title: data.immediate ? 'Gotowe' : 'Zakolejkowano',
|
title: data.immediate ? 'Gotowe' : 'Zakolejkowano',
|
||||||
|
|||||||
Reference in New Issue
Block a user