--- phase: 09-finalizacja plan: 02 subsystem: integrations tags: [ical, google-calendar, sync, multi-yacht, prefix-matching] requires: - phase: 04-frontend-kalendarz provides: widget kalendarza per-yacht (już filtruje po _booking_yacht_id) - phase: 08-gcal-synchronizacja provides: per-yacht ICal_Feed/ICal_Import + OAuth GCal_Service (zachowane jako fallback) provides: - Globalny iCal Export feed (jeden URL z tokenem dla wszystkich jachtów) - Globalny iCal Import URL (jeden wspólny kalendarz Google → wiele jachtów) - Mechanizm dopasowania jachtu po prefiksie SUMMARY (separator " - ") - Pole _yacht_gcal_alias (krótki alias dla skróconej nazwy w GCal) - Anti-loop (eventy zaimportowane z GCal nie są re-eksportowane) - Settings UI: nowa sekcja "Globalna synchronizacja iCal" affects: - przyszłe plany sprzątające pola per-yacht (Google Calendar ID, iCal Import URL w yacht-edit) - ewentualne wycofanie OAuth push (drugi mechanizm, klient go nie używa) tech-stack: added: - mb_strtolower/mb_substr/mb_strpos do parsowania prefiksu (poprawne dla polskich znaków) patterns: - "Lookup map yachts: lowercase(alias|post_title) => yacht_id" - "Anti-loop via _booking_source flag (ical_import_global)" - "Token-based public feed authorization (hash_equals)" key-files: created: [] modified: - wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-feed.php - wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php - wp-content/plugins/yacht-booking-system/includes/class-yacht.php - wp-content/plugins/yacht-booking-system/includes/class-installer.php - wp-content/plugins/yacht-booking-system/admin/class-admin.php - wp-content/plugins/yacht-booking-system/admin/views/yacht-edit.php key-decisions: - "Separator prefiksu: ' - ' (spacja-myślnik-spacja) — zgodne z istniejącym formatem per-yacht ICal_Feed::output_ics" - "Lookup case-insensitive po post_title lub _yacht_gcal_alias (priorytet alias)" - "Eventy bez separatora lub bez dopasowania — IGNOROWANE (nie blokują)" - "iCal jako mechanizm zamiast OAuth — klient nie chce konfigurować Google Cloud Console" - "Per-yacht feedy + OAuth zostają jako kompatybilność wsteczna (out of scope ich sprzątania)" patterns-established: - "Globalny feed: home_url('/yacht-ical-global/{token}.ics') — token w wp_options" - "Booking source 'ical_import_global' identyfikuje rezerwacje z globalnego importu (vs 'ical_import' per-yacht)" - "exact match po lowercase prefiksu — ŻADNYCH partial/substring matchings (uniknięcie kolizji 'Maja' vs 'Maja Słoneczna')" duration: ~25min started: 2026-05-06T11:00:00Z completed: 2026-05-06T11:25:00Z --- # Phase 9 Plan 02: Globalna synchronizacja iCal Summary **Dwukierunkowa synchronizacja całej floty z jednym wspólnym Google Calendar przez iCal feedy, z automatycznym przypisaniem eventów do jachtów po prefiksie nazwy w tytule (separator " - ").** ## Performance | Metric | Value | |--------|-------| | Duration | ~25min | | Tasks | 3 auto + 1 human-verify (wszystkie zaakceptowane) | | Files modified | 6 | ## Acceptance Criteria Results | Criterion | Status | Notes | |-----------|--------|-------| | AC-1: Globalny iCal Export feed | Pass | `output_global_ics()` zwraca .ics ze wszystkimi rezerwacjami, każda z prefiksem `{yacht_title} - {customer_name}`, anti-loop pomija source `ical_import_global` | | AC-2: Globalny Import — dopasowanie po prefiksie | Pass | `run_global_import()` + `match_yacht_by_prefix()` parsuje SUMMARY przed pierwszym ` - `, dopasowuje case-insensitive (mb_*), eventy bez dopasowania logowane i pomijane | | AC-3: Globalny Import — czyszczenie usuniętych | Pass | `existing_global_map` vs `seen_uids` — zniknięte UID-y → `Availability::clear_booking_availability()` + `wp_delete_post` | | AC-4: Alias jachtu | Pass | `Yacht::get_gcal_alias()` + `update_gcal_alias()`, build_yacht_lookup_map() używa aliasu z priorytetem | | AC-5: Frontend filtrowanie (regresja) | Pass | Bez zmian w widget — działa przez `_booking_yacht_id` jak dotąd; klient zweryfikował na produkcji | | AC-6: Settings UI | Pass | Nowa sekcja "Globalna synchronizacja iCal" (krok 1: export URL + kopiuj + regeneracja tokenu, krok 2: import URL + zapis, krok 3: importuj teraz, instrukcja) + alias w yacht-edit | ## Accomplishments - Klient może subskrybować jeden URL pluginu w Google Calendar i widzieć rezerwacje WSZYSTKICH jachtów w jednym widoku floty - Klient może tworzyć ręczne eventy w GCal z tytułem `"Nazwa - opis"` — plugin automatycznie przypisuje je do właściwego jachtu i blokuje availability tylko dla tego jachtu - Frontend kalendarz per-jacht pokazuje tylko swoje rezerwacje (bez zmian w widgecie — działa dzięki poprawnemu przypisaniu `_booking_yacht_id` przy imporcie) - Anti-loop zapobiega duplikacji (eventy zaimportowane z GCal nie są wysyłane z powrotem) - Stale cleanup automatycznie usuwa rezerwacje gdy event zostanie skasowany w GCal - Per-yacht feedy + OAuth push zostały zachowane (kompatybilność wsteczna) ## Files Created/Modified | File | Change | Purpose | |------|--------|---------| | `integrations/ical/class-ical-feed.php` | Modified | +rewrite rule `^yacht-ical-global/`, +query var `yacht_ical_global`, +`get_global_feed_token()`, +`regenerate_global_token()`, +`get_global_feed_url()`, +`output_global_ics()` (anti-loop, prefiks per yacht) | | `integrations/ical/class-ical-import.php` | Modified | +`GLOBAL_IMPORT_SOURCE`, +`SUMMARY_SEPARATOR`, +`run_global_import()`, +`build_yacht_lookup_map()` (alias priority), +`match_yacht_by_prefix()` (mb_*), +`get_existing_global_import_map()`, +`upsert_global_booking()`, cron `yacht_booking_ical_global_import` (hourly), zmiana visibility `parse_ics` na protected | | `includes/class-yacht.php` | Modified | +`get_gcal_alias()`, +`update_gcal_alias()` (meta `_yacht_gcal_alias`) | | `includes/class-installer.php` | Modified | +domyślne opcje: `yacht_booking_global_ical_import_url`, `yacht_booking_global_ical_token`, `yacht_booking_global_ical_last_import` | | `admin/class-admin.php` | Modified | +obsługa 3 form actions (save/regenerate token/run import), +4 admin notices, +sekcja UI "Globalna synchronizacja iCal" w `render_google_calendar_settings()`, +zapis aliasu w `save_yacht()` | | `admin/views/yacht-edit.php` | Modified | +pole `yacht_gcal_alias` z opisem | ## Decisions Made | Decision | Rationale | Impact | |----------|-----------|--------| | Separator " - " (spacja-myślnik-spacja) | Zgodność z istniejącym formatem `output_ics` (per-yacht feed już używał tego stylu) | Klient nie musi uczyć się nowej konwencji; istniejące eksporty działają tak samo | | Lookup po lowercase pełnej nazwy/aliasu (exact, nie substring) | Uniknięcie kolizji typu "Maja" trafia w "Maja" i "Maja Słoneczna" jednocześnie | Klient z długimi nazwami flot musi ustawić aliasy, ale ma pewność rozróżnienia | | Eventy bez dopasowania — ignorowane (nie blokują) | Klient nie chce niespodzianek (przypadkowa blokada całej floty przez literówkę) | Dyscyplina nazewnictwa w GCal, ale przewidywalność | | Anti-loop przez `_booking_source` flag | Najprostsze, niezawodne | Bez ryzyka eskalacji duplikatów między iteracjami | | Per-yacht feedy zostają | Kompatybilność wsteczna (potencjalni subskrybenci OTA) | Out of scope ich usunięcia — decyzja na osobny plan po stabilizacji | | Cron co godzinę dla globalnego importu | Spójne z per-yacht cron | Klient widzi import w panelu lub może wymusić "Importuj teraz" | ## Deviations from Plan ### Summary | Type | Count | Impact | |------|-------|--------| | Auto-fixed | 0 | — | | Scope additions | 0 | — | | Deferred | 0 | — | **Total impact:** Plan wykonany dokładnie zgodnie ze specyfikacją. Brak odchyleń. ### Auto-fixed Issues Brak. ### Deferred Items Brak. ## Issues Encountered Brak — żadnych problemów podczas implementacji ani weryfikacji manualnej. ## Otwarte pytania (do następnego planu) Klient w trakcie planowania prosił "zapytaj po zrobieniu importu i eksportu" — te pytania zostają otwarte: 1. **Pola per-yacht "Google Calendar ID" i "iCal Import URL" w yacht-edit** — czy je usunąć/ukryć teraz, gdy klient będzie używał globalnej synchronizacji? Jeśli tak: usunąć z UI ale zachować dane w meta na wypadek migracji. 2. **OAuth push do GCal (sync_to_gcal cron + on_booking_created hook)** — drugi mechanizm jest aktywny tylko gdy admin podłączył OAuth. Klient nie planuje tego używać. Czy wyłączyć całkowicie (żeby uniknąć ewentualnych duplikatów gdy ktoś przez pomyłkę połączy OAuth) czy zostawić jako opcję? ## Reproduction Path (do testów regresyjnych) 1. Settings → Google Calendar → sekcja "Globalna synchronizacja iCal" → skopiuj Export URL → wklej w GCal "Z URL-a" 2. Utwórz w GCal event `"NazwaJachtu - Test"` (data za tydzień, całodniowy) 3. Skopiuj iCal URL kalendarza Google → wklej w pole "iCal Import URL" → Zapisz → "Importuj teraz" 4. WP Admin → Rezerwacje → pojawia się booking `"GCal: NazwaJachtu - Test"` (source `ical_import_global`) 5. Frontend kalendarz tego jachtu → data zablokowana; inny jacht → data wolna 6. Usuń event w GCal → "Importuj teraz" → booking zniknął, daty zwolnione ## Next Phase Readiness **Ready:** - Globalna synchronizacja iCal w pełni funkcjonalna i zweryfikowana przez klienta - Architektura per-yacht zachowana (no breaking changes) - 2 z 4 plans w fazie 9 ukończone **Concerns:** - Ewentualna kolizja gdy admin przypadkowo użyje OAuth obok iCal — drugi mechanizm tworzyłby duplikaty. Mitigacja: GCal_Service::create_event ustawia `_gcal_event_id` którego nie zerujemy, ale plugin już teraz ma anti-loop tylko na imporcie globalnym, nie na OAuth pull. Klient nie używa OAuth → praktyczny ryzyko zerowe. - Klient powinien wykonać deploy FTP + jednorazowy "Save" na stronie Permalinks żeby flush rewrite rules zarejestrowało nową regułę `^yacht-ical-global/` (dokumentowane w instrukcji checkpointu). **Blockers:** None --- *Phase: 09-finalizacja, Plan: 02* *Completed: 2026-05-06*