---
phase: 07-pre-expansion-fixes
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - src/Modules/Settings/AllegroApiClient.php
  - src/Modules/Settings/AllegroOAuthClient.php
  - src/Modules/Settings/ShopproApiClient.php
  - src/Modules/Settings/ApaczkaApiClient.php
  - src/Core/Application.php
  - database/migrations/20260313_000049_add_cron_last_run_at_setting.sql
  - .env.example
autonomous: true
---

<objective>
## Goal
Naprawić trzy problemy stabilności przed rozbudową: zduplikowany numer migracji 000014, brak weryfikacji SSL w 4 klientach API, cron throttle przechowywany w sesji zamiast bazie danych.

## Purpose
Każdy nowy klient API dodany bez wzorca SSL verification powiela podatność. Cron throttle w sesji powoduje wielokrotne uruchamianie crona przy wielu aktywnych sesjach. Duplikat migracji wprowadza niejednoznaczność przy deploy na nowe środowisko. Wszystkie trzy to fundament do naprawy przed dodawaniem nowych integracji.

## Output
- 4 pliki ApiClient z `CURLOPT_SSL_VERIFYPEER => true` + `CURLOPT_CAINFO`
- `.env.example` z `CURL_CA_BUNDLE_PATH`
- `Application::isWebCronThrottled()` czytający timestamp z `app_settings` zamiast `$_SESSION`
- Nowa migracja seed dla klucza `cron_web_last_run_at` w `app_settings`
- Migracja 000014b (rename duplikatu)
</objective>

<context>
## Project Context
@.paul/PROJECT.md

## Source Files
@src/Modules/Settings/AllegroApiClient.php
@src/Modules/Settings/AllegroOAuthClient.php
@src/Core/Application.php
@.env.example
</context>

<acceptance_criteria>

## AC-1: SSL weryfikowany w każdym cURL wywołaniu
```gherkin
Given 4 klasy ApiClient (AllegroApiClient, AllegroOAuthClient, ShopproApiClient, ApaczkaApiClient) wykonują cURL bez CURLOPT_SSL_VERIFYPEER
When dowolny ApiClient wykonuje zapytanie HTTP
Then CURLOPT_SSL_VERIFYPEER jest ustawione na true
  AND CURLOPT_CAINFO wskazuje na ścieżkę z .env (lub systemowy bundle jako fallback)
  AND .env.example zawiera CURL_CA_BUNDLE_PATH z komentarzem
```

## AC-2: Web cron throttle oparty na DB, nie sesji
```gherkin
Given Application::isWebCronThrottled() zapisuje/czyta timestamp z $_SESSION['cron_web_last_run_at']
When isWebCronThrottled() jest wywołane
Then timestamp jest czytany z app_settings (klucz: cron_web_last_run_at) zamiast $_SESSION
  AND zapis po uruchomieniu crona idzie do app_settings (UPDATE lub INSERT ON DUPLICATE KEY)
  AND $_SESSION['cron_web_last_run_at'] nie jest już używany
```

## AC-3: Migracja 000014 zdeduplikowana
```gherkin
Given dwa pliki mają sekwencję 000014 w nazwie
When sprawdzasz listę migracji
Then jeden z nich jest przemianowany na 000014b
  AND plik zawiera idempotentny INSERT (ON DUPLICATE KEY UPDATE lub INSERT IGNORE)
    żeby powtórne uruchomienie na istniejącej bazie nie powodowało błędu
```

</acceptance_criteria>

<tasks>

<task type="auto">
  <name>Task 1: Dodaj SSL verification do 4 ApiClient klas</name>
  <files>
    src/Modules/Settings/AllegroApiClient.php,
    src/Modules/Settings/AllegroOAuthClient.php,
    src/Modules/Settings/ShopproApiClient.php,
    src/Modules/Settings/ApaczkaApiClient.php,
    .env.example
  </files>
  <action>
    Dla każdej z 4 klas:
    1. Odczytaj plik — zidentyfikuj wszystkie `curl_setopt_array($ch, [...])` wywołania
    2. Do każdego array dodaj (jeśli nie ma):
       ```php
       CURLOPT_SSL_VERIFYPEER => true,
       CURLOPT_SSL_VERIFYHOST => 2,
       CURLOPT_CAINFO => $this->getCaBundlePath(),
       ```
    3. Dodaj prywatną metodę pomocniczą do każdej klasy:
       ```php
       private function getCaBundlePath(): string
       {
           $envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
           if ($envPath !== '' && is_file($envPath)) {
               return $envPath;
           }
           // Windows XAMPP fallback
           $xamppCa = 'C:/xampp/php/extras/ssl/cacert.pem';
           if (is_file($xamppCa)) {
               return $xamppCa;
           }
           // System default (Linux/macOS)
           return '/etc/ssl/certs/ca-certificates.crt';
       }
       ```
       Jeśli wszystkie 4 klasy mają wspólny trait lub klasę bazową — wydziel metodę tam.
       Jeśli nie — dodaj do każdej osobno (duplikacja jest OK, unikamy przedwczesnej abstrakcji).

    4. W `.env.example` dodaj sekcję:
       ```
       # SSL/TLS — ścieżka do CA bundle dla cURL
       # Windows XAMPP: C:/xampp/php/extras/ssl/cacert.pem
       # Linux: /etc/ssl/certs/ca-certificates.crt
       CURL_CA_BUNDLE_PATH=
       ```

    WAŻNE: Nie dodawaj CURLOPT_SSL_VERIFYPEER => false nigdzie. Jeśli CA bundle nie istnieje,
    metoda getCaBundlePath() zwraca ścieżkę — PHP zgłosi błąd cURL jeśli plik nie istnieje.
    To jest pożądane zachowanie (fail-fast, nie silent MITM).
  </action>
  <verify>
    php -l src/Modules/Settings/AllegroApiClient.php
    php -l src/Modules/Settings/AllegroOAuthClient.php
    php -l src/Modules/Settings/ShopproApiClient.php
    php -l src/Modules/Settings/ApaczkaApiClient.php
    grep -n "CURLOPT_SSL_VERIFYPEER" src/Modules/Settings/AllegroApiClient.php
    grep -c "CURLOPT_SSL_VERIFYPEER" src/Modules/Settings/ShopproApiClient.php
  </verify>
  <done>AC-1 satisfied: wszystkie 4 ApiClienty mają CURLOPT_SSL_VERIFYPEER => true</done>
</task>

<task type="auto">
  <name>Task 2: Cron throttle z $_SESSION → app_settings DB</name>
  <files>
    src/Core/Application.php,
    database/migrations/20260313_000049_add_cron_last_run_at_setting.sql
  </files>
  <action>
    W `Application.php`, znajdź metodę `isWebCronThrottled()` (linie ~268-285).

    Obecna logika:
    ```php
    $lastRunAt = isset($_SESSION['cron_web_last_run_at']) ? (int) $_SESSION['cron_web_last_run_at'] : 0;
    // ...
    $_SESSION['cron_web_last_run_at'] = $now;
    ```

    Zastąp odczyt sesji odczytem z DB:
    ```php
    $lastRunAt = $this->getWebCronLastRunAt();
    ```

    I zapis sesji zapisem do DB:
    ```php
    $this->setWebCronLastRunAt($now);
    ```

    Dodaj prywatne metody pomocnicze:
    ```php
    private function getWebCronLastRunAt(): int
    {
        try {
            $stmt = $this->db->pdo()->prepare(
                "SELECT setting_value FROM app_settings WHERE setting_key = 'cron_web_last_run_at'"
            );
            $stmt->execute();
            $value = $stmt->fetchColumn();
            return $value !== false ? (int) $value : 0;
        } catch (Throwable) {
            return 0;
        }
    }

    private function setWebCronLastRunAt(int $timestamp): void
    {
        try {
            $this->db->pdo()->prepare(
                "INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at)
                 VALUES ('cron_web_last_run_at', :ts, NOW(), NOW())
                 ON DUPLICATE KEY UPDATE setting_value = :ts, updated_at = NOW()"
            )->execute(['ts' => (string) $timestamp]);
        } catch (Throwable) {
            // throttle failure is non-critical — GET_LOCK is the real guard
        }
    }
    ```

    Sprawdź jak Application uzyskuje dostęp do PDO — jeśli przez `$this->db->pdo()` lub
    `$this->db->query()` — dostosuj. Sprawdź jak CronHandlerFactory i inne miejsca używają DB.

    Usuń odwołania do `$_SESSION['cron_web_last_run_at']` całkowicie.

    Stwórz migrację `20260313_000049_add_cron_last_run_at_setting.sql`:
    ```sql
    INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at)
    VALUES ('cron_web_last_run_at', '0', NOW(), NOW())
    ON DUPLICATE KEY UPDATE updated_at = updated_at;
    ```
  </action>
  <verify>
    php -l src/Core/Application.php
    grep -n "SESSION.*cron_web\|cron_web.*SESSION" src/Core/Application.php
    # Powinno zwrócić 0 wyników
    grep -n "getWebCronLastRunAt\|setWebCronLastRunAt" src/Core/Application.php
  </verify>
  <done>AC-2 satisfied: isWebCronThrottled() nie używa $_SESSION, czyta/zapisuje app_settings</done>
</task>

<task type="auto">
  <name>Task 3: Rename duplikatu migracji 000014</name>
  <files>
    database/migrations/20260301_000014b_add_products_sku_format_setting.sql
  </files>
  <action>
    Migracja `20260301_000014_add_products_sku_format_setting.sql` ma ten sam numer co
    `20260227_000014_create_product_integration_translations.sql`.

    Ponieważ migrator śledzi migracje po **pełnej nazwie pliku** (nie numerze sekwencji),
    rename jest bezpieczny dla nowych instalacji. Dla istniejących instalacji plik pod nową
    nazwą będzie wyglądał jako nowy — dlatego zawartość musi być idempotentna.

    Kroki:
    1. Odczytaj zawartość `20260301_000014_add_products_sku_format_setting.sql`
    2. Sprawdź czy INSERT używa `ON DUPLICATE KEY UPDATE` lub `INSERT IGNORE`
       - Jeśli nie: zmień INSERT na idempotentny (INSERT IGNORE lub ON DUPLICATE KEY)
    3. Utwórz nowy plik `20260301_000014b_add_products_sku_format_setting.sql` z zaktualizowaną treścią
    4. Usuń stary plik `20260301_000014_add_products_sku_format_setting.sql`

    UWAGA: Użyj `git mv` (przez bash) żeby git śledził rename:
    ```bash
    git mv "database/migrations/20260301_000014_add_products_sku_format_setting.sql" \
           "database/migrations/20260301_000014b_add_products_sku_format_setting.sql"
    ```
    Potem edytuj plik (jeśli INSERT wymaga korekty na idempotentny).
  </action>
  <verify>
    ls database/migrations/ | grep "000014"
    # Powinno pokazać: 20260227_000014_create... i 20260301_000014b_add...
    # NIE powinno być: 20260301_000014_add... (stary plik)
  </verify>
  <done>AC-3 satisfied: brak duplikatu, jeden plik przemianowany na 000014b</done>
</task>

</tasks>

<boundaries>

## DO NOT CHANGE
- Logika biznesowa żadnego ApiClienta — tylko dodanie SSL options do istniejących curl_setopt_array
- GET_LOCK logika w Application.php — nie ruszamy mutex, tylko session→DB dla timestamp
- Inne klucze w app_settings
- Istniejące migracje poza rename 000014

## SCOPE LIMITS
- Nie implementuj rotacji CSRF tokenów (osobny deferred concern)
- Nie dodawaj path traversal check dla label files (osobny deferred concern)
- CA bundle path — środowisko aplikacyjne, nie certyfikat self-signed

</boundaries>

<verification>
Przed zamknięciem planu:
- [ ] php -l na wszystkich 4 ApiClient plikach
- [ ] php -l src/Core/Application.php
- [ ] grep "SSL_VERIFYPEER" — każdy ApiClient ma ten wpis
- [ ] grep "SESSION.*cron_web" Application.php — 0 wyników
- [ ] ls database/migrations/ | grep "000014" — tylko 000014 i 000014b
- [ ] Plik migracji 000049 istnieje
</verification>

<success_criteria>
- 4 ApiClienty z SSL verification
- .env.example z CURL_CA_BUNDLE_PATH
- Application::isWebCronThrottled() bez $_SESSION
- Migracja 000049 (cron_web_last_run_at seed)
- Rename 000014 → 000014b
- Zero błędów składniowych PHP
</success_criteria>

<output>
Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-02-SUMMARY.md`
</output>
