feat(v0.1): historia cen + jawnosc cen — milestone Initial Release

Historia cen:
- Tabela wp_price_history z WP Cronem dziennym (snapshot cen)
- AJAX endpoint apartamenty_get_price_history (zabezpieczony nonce)
- Popup "Historia cen" w widgecie — vanilla JS, modal zgodny z projektem

Jawnosc cen:
- Endpointy /ceny-mieszkan.xml + /dane-gov-pl.xml (XSD-compliant)
- Pliki MD5 dla obu XML
- Strona admina: Narzedzia -> Jawnosc Cen z URL-ami do Ministerstwa
- Transient cache 1h z inwalidacja przez cron

Dokumentacja: docs/readme.md + docs/jawnosc-cen.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 15:40:29 +01:00
parent c954889b64
commit 972c69b136
19 changed files with 2854 additions and 3 deletions

142
docs/jawnosc-cen.md Normal file
View File

@@ -0,0 +1,142 @@
# Jawność cen — dokumentacja
## Co zostało zbudowane
Cztery publiczne endpointy HTTP oraz strona administracyjna, spełniające wymóg ustawy o jawności cen nieruchomości.
### Endpointy
| URL | Opis |
|-----|------|
| `/ceny-mieszkan.xml` | XML z cenami wszystkich lokali + historia zmian z bazy danych |
| `/ceny-mieszkan.md5` | Hash MD5 powyższego pliku XML (32-znakowy lowercase hex) |
| `/dane-gov-pl.xml` | Katalog zgodny z XSD portalu dane.gov.pl, wskazujący na plik cen |
| `/dane-gov-pl.md5` | Hash MD5 katalogu |
Wszystkie endpointy są publicznie dostępne bez logowania.
### Strona administracyjna
**wp-admin → Narzędzia → Jawność Cen**
Pokazuje oba URL-e do zgłoszenia do Ministerstwa z przyciskami „Kopiuj URL" i „Otwórz XML".
---
## Jak to działa
### Plik cen (`/ceny-mieszkan.xml`)
- Pobiera wszystkie opublikowane posty typu `apartamenty`
- Dla każdego lokalu odczytuje pola ACF: `information_type`, `information_floor`, `information_floor_space`, `information_price`, `information_price_m2`, `information_status`
- Dołącza historię cen z tabeli `wp_price_history` (ta sama tabela co cron dzienny)
- Wynik cachowany w transiencie WordPress na **1 godzinę**
- Cache jest automatycznie czyszczony przy każdym uruchomieniu crona dziennego
Struktura XML:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<lokale inwestycja="Wyszyńskiego 12" generowany="2026-03-12T10:00:00+01:00">
<lokal id="123">
<nazwa>A1 Lokal 101</nazwa>
<typ>mieszkanie</typ>
<pietro>1</pietro>
<powierzchnia>35,68</powierzchnia>
<status>dostępny</status>
<cena_brutto>677 920</cena_brutto>
<cena_za_m2>19 000</cena_za_m2>
<data_aktualizacji>2026-03-12</data_aktualizacji>
<historia_cen>
<zmiana data="2026-03-12">
<cena_brutto>677 920</cena_brutto>
<cena_za_m2>19 000</cena_za_m2>
</zmiana>
</historia_cen>
</lokal>
</lokale>
```
### Katalog dane.gov.pl (`/dane-gov-pl.xml`)
Statyczny XML generowany dynamicznie — URL zasobu pobierany przez `home_url()`, data przez `date()`. Zgodny z XSD `otwarte_dane_latest.xsd` portalu dane.gov.pl.
### Kod źródłowy
Plik: `wp-content/plugins/elementor-addon/elementor-addon.php`
| Funkcja | Opis |
|---------|------|
| `apartamenty_xml_rewrite_rules()` | Rejestruje reguły URL dla endpointów |
| `apartamenty_xml_query_vars()` | Dodaje query vars do WordPress |
| `apartamenty_generate_price_xml()` | Generuje XML cen z cachowaniem |
| `apartamenty_generate_datagov_xml()` | Generuje XML katalogu dane.gov.pl |
| `apartamenty_xml_template_redirect()` | Obsługuje żądania HTTP i wysyła odpowiedź |
| `apartamenty_jawnosc_cen_menu()` | Rejestruje stronę w menu Narzędzia |
| `apartamenty_jawnosc_cen_page()` | Renderuje stronę administracyjną |
---
## Co musi zrobić Klient
### Krok 1: Flush rewrite rules (jednorazowo po wdrożeniu)
Po każdym wdrożeniu zmian w pluginie należy odświeżyć reguły permalink:
**wp-admin → Ustawienia → Bezpośrednie odnośniki → kliknij „Zapisz zmiany"**
Bez tego kroku endpointy XML zwracają błąd 404.
### Krok 2: Zgłoszenie do portalu dane.gov.pl
1. Zaloguj się na [dane.gov.pl](https://dane.gov.pl) (konto instytucjonalne dewelopera lub pełnomocnika)
2. W panelu wydawcy wybierz „Dodaj zbiór danych" lub „Zasilanie automatyczne (harvester)"
3. Podaj URL katalogu:
```
https://wyszynskiego12.pagedev.pl/dane-gov-pl.xml
```
4. Portal będzie automatycznie pobierał ten plik (codziennie) i aktualizował dane w rejestrze
> Jeśli portal dane.gov.pl wymaga wcześniejszej rejestracji instytucji — należy ją przeprowadzić osobno. Kontakt: **kontakt@dane.gov.pl**
### Krok 3 (opcjonalnie): Weryfikacja endpointów przed zgłoszeniem
Przed podaniem URL-a do Ministerstwa warto sprawdzić każdy endpoint ręcznie:
| URL | Oczekiwany wynik |
|-----|-----------------|
| `/ceny-mieszkan.xml` | XML z listą lokali, Content-Type: application/xml |
| `/ceny-mieszkan.md5` | 32-znakowy ciąg liter i cyfr, np. `a3f2b1c9...` |
| `/dane-gov-pl.xml` | XML z elementem `<datasets>`, URL w `<resource>` wskazuje na `/ceny-mieszkan.xml` |
| `/dane-gov-pl.md5` | 32-znakowy ciąg liter i cyfr |
---
## Aktualizacja danych
Dane w pliku XML są aktualizowane **automatycznie**:
- Ceny pobierane są z pól ACF w WordPress — wystarczy zaktualizować pole `information_price` w edytorze posta, a nowa cena pojawi się w XML po max. 1 godzinie (czas życia cache)
- Historia cen zapisywana jest codziennie przez WP Cron (szczegóły w `docs/readme.md`)
- Cache XML czyszczony jest przy każdym uruchomieniu crona
**Ręczne wymuszenie odświeżenia XML** (np. po pilnej zmianie ceny):
Przez WP-CLI (SSH):
```bash
wp transient delete apartamenty_price_xml_cache
```
Lub przez phpMyAdmin / SQL:
```sql
DELETE FROM wp_options WHERE option_name = '_transient_apartamenty_price_xml_cache';
DELETE FROM wp_options WHERE option_name = '_transient_timeout_apartamenty_price_xml_cache';
```
---
## Uwagi techniczne
- Endpointy nie wymagają żadnej konfiguracji po stronie serwera (nginx/Apache) — działają przez mechanizm rewrite rules WordPress
- XML generowany jest jako czysty string PHP z `htmlspecialchars(ENT_XML1)` — bezpieczny dla znaków specjalnych w nazwach lokali
- Katalog dane.gov.pl zawiera hardcodowane dane inwestycji (Wyszyńskiego 12) — jeśli dane inwestycji się zmienią, należy zaktualizować funkcję `apartamenty_generate_datagov_xml()` w pluginie

103
docs/readme.md Normal file
View File

@@ -0,0 +1,103 @@
# Historia cen — dokumentacja
## Cron dzienny: zapisywanie historii cen
### Co robi
Raz na dobę WP Cron uruchamia zadanie `apartamenty_record_prices`, które:
1. Pobiera wszystkie opublikowane posty typu `apartamenty`
2. Odczytuje aktualne ceny z pól ACF:
- `information_price` — cena brutto (np. `"677 920"`)
- `information_price_m2` — cena za m² (np. `"19 000"`)
- `information_floor_space` — metraż (np. `"35,68"`)
3. Zapisuje jeden rekord dziennie do tabeli `wp_price_history`
4. Używa `INSERT IGNORE` — jeśli rekord dla danego apartamentu i daty już istnieje, pomija (bez duplikatów)
### Tabela bazy danych
**Nazwa:** `wp_price_history`
| Kolumna | Typ | Opis |
|---------|-----|------|
| `id` | BIGINT UNSIGNED AUTO_INCREMENT | Klucz główny |
| `post_id` | BIGINT UNSIGNED | ID posta apartamentu |
| `price` | VARCHAR(50) | Cena brutto jako string, np. `"677 920"` |
| `price_m2` | VARCHAR(50) | Cena za m² jako string, np. `"19 000"` |
| `floor_space` | VARCHAR(50) | Metraż jako string, np. `"35,68"` |
| `recorded_at` | DATE | Data zapisu, np. `"2026-03-12"` |
Unikalny klucz: `(post_id, recorded_at)` — jeden wpis na apartament na dzień.
### Harmonogram
- **Częstotliwość:** raz dziennie (`daily`)
- **Hook WP Cron:** `apartamenty_record_prices`
- **Rejestracja:** przy każdym żądaniu strony (`wp` action), jeśli zadanie nie jest jeszcze zaplanowane
### Kod źródłowy
Plik: `wp-content/plugins/elementor-addon/elementor-addon.php`
- `elementor_addon_schedule_cron()` — rejestruje zadanie w WP Cron
- `elementor_addon_record_prices()` — wykonuje zapis cen
### Ważne: WP Cron wymaga ruchu na stronie
WP Cron nie jest prawdziwym cronem systemowym — uruchamia się przy odwiedzeniu strony przez użytkownika. Jeśli strona ma mały ruch, zadanie może się nie wykonać o dokładnej porze.
**Rozwiązanie: prawdziwy cron systemowy (zalecane na produkcji)**
Wyłącz WP Cron w `wp-config.php`:
```php
define( 'DISABLE_WP_CRON', true );
```
Dodaj zadanie w cPanel → Cron Jobs (lub przez SSH):
```
0 6 * * * wget -q -O /dev/null "https://wyszynskiego12.pagedev.pl/wp-cron.php?doing_wp_cron" >/dev/null 2>&1
```
lub z curl:
```
0 6 * * * curl -s "https://wyszynskiego12.pagedev.pl/wp-cron.php?doing_wp_cron" > /dev/null 2>&1
```
Powyższe uruchamia cron codziennie o 6:00.
### Ręczne uruchomienie (debugowanie)
Aby wymusić zapis raz ręcznie bez czekania na cron, wklej w przeglądarce (zalogowany jako admin):
```
https://wyszynskiego12.pagedev.pl/wp-cron.php?doing_wp_cron
```
Lub przez WP-CLI (SSH):
```bash
wp cron event run apartamenty_record_prices
```
### Sprawdzenie następnego uruchomienia (WP-CLI)
```bash
wp cron event list
```
### Weryfikacja zapisanych danych (SQL)
```sql
SELECT * FROM wp_price_history ORDER BY recorded_at DESC LIMIT 20;
```
Liczba rekordów per apartament:
```sql
SELECT post_id, COUNT(*) as wpisy, MIN(recorded_at) as od, MAX(recorded_at) as do
FROM wp_price_history
GROUP BY post_id;
```