diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index fa4ec06..b53600c 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -125,8 +125,8 @@ }, "class.Products.php": { "type": "-", - "size": 49998, - "lmtime": 1772612176689, + "size": 50211, + "lmtime": 1772916736693, "modified": false }, "class.Site.php": { @@ -193,8 +193,8 @@ }, "class.Products.php": { "type": "-", - "size": 41405, - "lmtime": 1772612327756, + "size": 42460, + "lmtime": 1772916728114, "modified": false }, "class.Users.php": { @@ -786,8 +786,8 @@ "products": { "main_view.php": { "type": "-", - "size": 77412, - "lmtime": 1772612193848, + "size": 77741, + "lmtime": 1772916753426, "modified": false }, "product_history.php": { diff --git a/api.php b/api.php index b2099f2..74db4bf 100644 --- a/api.php +++ b/api.php @@ -37,6 +37,52 @@ $mdb = new medoo( [ return R::getRedBean() -> dispense( $type ); } ); +function api_json_response( $data, $http_code = 200 ) +{ + http_response_code( (int) $http_code ); + echo json_encode( $data ); + exit; +} + +function api_validate_api_key( $mdb ) +{ + $api_key = trim( (string) \S::get( 'api_key' ) ); + $stored_key = trim( (string) $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => 'api_key' ] ) ); + + if ( $api_key === '' || $stored_key === '' || !hash_equals( $stored_key, $api_key ) ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Invalid api_key' ], 401 ); + } +} + +function api_get_product_by_offer_and_client( $mdb, $offer_id, $client_id ) +{ + return $mdb -> query( + 'SELECT p.id, p.name, p.title, p.google_product_category + FROM products p + JOIN clients cl ON p.client_id = cl.id + WHERE p.offer_id = :offer_id + AND cl.id = :client_id + LIMIT 1', + [ + ':offer_id' => (string) $offer_id, + ':client_id' => (int) $client_id + ] + ) -> fetch( \PDO::FETCH_ASSOC ); +} + +function api_normalize_product_text( $value ) +{ + $value = trim( (string) $value ); + + if ( $value === '' ) + { + return null; + } + + return $value; +} + // dodawanie domeny przez API if ( \S::get( 'action' ) == 'domain_tester_add' ) { @@ -124,14 +170,7 @@ if ( \S::get( 'action' ) == 'campaign_comment_add' ) // Zmiana custom_label_4 dla produktu przez API if ( \S::get( 'action' ) == 'product_custom_label_4_set' ) { - $api_key = trim( \S::get( 'api_key' ) ); - $stored_key = $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => 'api_key' ] ); - - if ( !$api_key || !$stored_key || $api_key !== $stored_key ) - { - echo json_encode( [ 'result' => 'error', 'message' => 'Invalid api_key' ] ); - exit; - } + api_validate_api_key( $mdb ); $offer_id = trim( \S::get( 'offer_id' ) ); $client_id_param = trim( \S::get( 'client_id' ) ); @@ -139,34 +178,166 @@ if ( \S::get( 'action' ) == 'product_custom_label_4_set' ) if ( !$offer_id || !$client_id_param ) { - echo json_encode( [ 'result' => 'error', 'message' => 'Missing required params: offer_id, client_id' ] ); - exit; + api_json_response( [ 'result' => 'error', 'message' => 'Missing required params: offer_id, client_id' ], 422 ); } - $product = $mdb -> query( - 'SELECT p.id - FROM products p - JOIN clients cl ON p.client_id = cl.id - WHERE p.offer_id = :offer_id - AND cl.id = :client_id - LIMIT 1', - [ - ':offer_id' => $offer_id, - ':client_id' => (int) $client_id_param - ] - ) -> fetch( \PDO::FETCH_ASSOC ); + $product = api_get_product_by_offer_and_client( $mdb, $offer_id, (int) $client_id_param ); if ( !$product ) { - echo json_encode( [ 'result' => 'error', 'message' => 'Product not found' ] ); - exit; + api_json_response( [ 'result' => 'error', 'message' => 'Product not found' ], 404 ); } \factory\Products::set_product_data( $product['id'], 'custom_label_4', $custom_label_4 ); \factory\Products::add_product_comment( $product['id'], 'Zmiana etykiety 4 na: ' . $custom_label_4 . ' (API)' ); - echo json_encode( [ 'result' => 'ok' ] ); - exit; + api_json_response( [ 'result' => 'ok' ] ); +} + +// Zmiana tytulu produktu przez API +if ( \S::get( 'action' ) == 'product_title_set' ) +{ + api_validate_api_key( $mdb ); + + $offer_id = trim( (string) \S::get( 'offer_id' ) ); + $client_id_param = (int) \S::get( 'client_id' ); + $new_title = api_normalize_product_text( \S::get( 'title' ) ); + + if ( $offer_id === '' || $client_id_param <= 0 ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Missing required params: offer_id, client_id' ], 422 ); + } + + $product = api_get_product_by_offer_and_client( $mdb, $offer_id, $client_id_param ); + + if ( !$product ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Product not found' ], 404 ); + } + + $old_title = (string) ( $product['title'] ?? '' ); + \factory\Products::set_product_data( (int) $product['id'], 'title', $new_title ); + + $old_title_for_log = trim( $old_title ) !== '' ? $old_title : '[pusty]'; + $new_title_for_log = $new_title !== null ? $new_title : '[pusty]'; + \factory\Products::add_product_comment( + (int) $product['id'], + 'Zmiana tytulu przez API: ' . $old_title_for_log . ' -> ' . $new_title_for_log + ); + + api_json_response( [ + 'result' => 'ok', + 'product_id' => (int) $product['id'], + 'offer_id' => $offer_id, + 'client_id' => $client_id_param, + 'title' => $new_title + ] ); +} + +// Sprawdzenie, czy tytul produktu byl juz zmieniony +if ( \S::get( 'action' ) == 'product_title_changed_check' ) +{ + api_validate_api_key( $mdb ); + + $offer_id = trim( (string) \S::get( 'offer_id' ) ); + $client_id_param = (int) \S::get( 'client_id' ); + + if ( $offer_id === '' || $client_id_param <= 0 ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Missing required params: offer_id, client_id' ], 422 ); + } + + $product = api_get_product_by_offer_and_client( $mdb, $offer_id, $client_id_param ); + + if ( !$product ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Product not found' ], 404 ); + } + + $base_name = trim( (string) ( $product['name'] ?? '' ) ); + $custom_title = trim( (string) ( $product['title'] ?? '' ) ); + $is_changed = $custom_title !== '' && $custom_title !== $base_name; + + api_json_response( [ + 'result' => 'ok', + 'product_id' => (int) $product['id'], + 'offer_id' => $offer_id, + 'client_id' => $client_id_param, + 'title_changed' => $is_changed, + 'default_name' => $base_name, + 'custom_title' => $custom_title !== '' ? $custom_title : null + ] ); +} + +// Zmiana Google Product Category przez API +if ( \S::get( 'action' ) == 'product_google_category_set' ) +{ + api_validate_api_key( $mdb ); + + $offer_id = trim( (string) \S::get( 'offer_id' ) ); + $client_id_param = (int) \S::get( 'client_id' ); + $google_category = api_normalize_product_text( \S::get( 'google_product_category' ) ); + + if ( $offer_id === '' || $client_id_param <= 0 ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Missing required params: offer_id, client_id' ], 422 ); + } + + $product = api_get_product_by_offer_and_client( $mdb, $offer_id, $client_id_param ); + + if ( !$product ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Product not found' ], 404 ); + } + + $old_category = (string) ( $product['google_product_category'] ?? '' ); + \factory\Products::set_product_data( (int) $product['id'], 'google_product_category', $google_category ); + + $old_category_for_log = trim( $old_category ) !== '' ? $old_category : '[pusty]'; + $new_category_for_log = $google_category !== null ? $google_category : '[pusty]'; + \factory\Products::add_product_comment( + (int) $product['id'], + 'Zmiana Google Product Category przez API: ' . $old_category_for_log . ' -> ' . $new_category_for_log + ); + + api_json_response( [ + 'result' => 'ok', + 'product_id' => (int) $product['id'], + 'offer_id' => $offer_id, + 'client_id' => $client_id_param, + 'google_product_category' => $google_category + ] ); +} + +// Odczyt Google Product Category przez API +if ( \S::get( 'action' ) == 'product_google_category_get' ) +{ + api_validate_api_key( $mdb ); + + $offer_id = trim( (string) \S::get( 'offer_id' ) ); + $client_id_param = (int) \S::get( 'client_id' ); + + if ( $offer_id === '' || $client_id_param <= 0 ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Missing required params: offer_id, client_id' ], 422 ); + } + + $product = api_get_product_by_offer_and_client( $mdb, $offer_id, $client_id_param ); + + if ( !$product ) + { + api_json_response( [ 'result' => 'error', 'message' => 'Product not found' ], 404 ); + } + + $google_category = trim( (string) ( $product['google_product_category'] ?? '' ) ); + + api_json_response( [ + 'result' => 'ok', + 'product_id' => (int) $product['id'], + 'offer_id' => $offer_id, + 'client_id' => $client_id_param, + 'google_product_category' => $google_category !== '' ? $google_category : null + ] ); } // Open Page Rank - zapis @@ -181,4 +352,4 @@ if ( \S::get( 'action' ) == 'domain_opr_save' ) echo json_encode( ['result' => 'ok'] ); exit; -} \ No newline at end of file +} diff --git a/docs/api-public-product-management.md b/docs/api-public-product-management.md new file mode 100644 index 0000000..8a58370 --- /dev/null +++ b/docs/api-public-product-management.md @@ -0,0 +1,278 @@ +# AdsPRO API - Zarzadzanie produktami (publiczna dokumentacja) + +Ten dokument opisuje endpointy HTTP dostepne w `api.php` do zarzadzania danymi produktow. +Format jest przygotowany pod integracje automatyczne (AI/agent/workflow). + +## 1. Informacje bazowe + +- Metoda: `GET` lub `POST` (`application/x-www-form-urlencoded` albo query string). +- Endpoint bazowy: `https://TWOJA-DOMENA/api.php` +- Parametr routingu: `action` +- Odpowiedzi: JSON +- Kodowanie: UTF-8 + +Przyklady zakladaja, ze: +- domena: `https://example.com` +- klucz API: `YOUR_API_KEY` +- klient: `client_id=12` +- produkt: `offer_id=SKU-123` + +## 2. Autoryzacja + +Kazdy endpoint produktowy wymaga poprawnego parametru: + +- `api_key` - klucz porownywany z `settings.setting_key = api_key` + +Brak lub zly klucz zwraca: + +```json +{ + "result": "error", + "message": "Invalid api_key" +} +``` + +HTTP status: `401` + +## 3. Wspolne parametry + +- `offer_id` (string, wymagany) - zewnetrzny identyfikator produktu. +- `client_id` (int, wymagany) - lokalny identyfikator klienta. + +Jesli produkt nie istnieje dla pary `offer_id + client_id`, API zwraca: + +```json +{ + "result": "error", + "message": "Product not found" +} +``` + +HTTP status: `404` + +## 4. Endpointy produktowe + +### 4.1 Ustawienie tytulu produktu + +- `action=product_title_set` +- Cel: zapisuje `products.title` + +Parametry: +- `api_key` (string, wymagany) +- `offer_id` (string, wymagany) +- `client_id` (int, wymagany) +- `title` (string, opcjonalny) + +Uwagi: +- `title` jest przycinany (`trim`). +- Pusty `title` ustawia `products.title = NULL` (czyszczenie pola). +- Zmiana zapisuje komentarz techniczny w `products_comments`. + +Przyklad: + +```bash +curl -X POST "https://example.com/api.php" \ + -d "action=product_title_set" \ + -d "api_key=YOUR_API_KEY" \ + -d "client_id=12" \ + -d "offer_id=SKU-123" \ + -d "title=Buty biegowe meskie Air Run 42" +``` + +Przyklad odpowiedzi: + +```json +{ + "result": "ok", + "product_id": 987, + "offer_id": "SKU-123", + "client_id": 12, + "title": "Buty biegowe meskie Air Run 42" +} +``` + +### 4.2 Sprawdzenie, czy tytul byl zmieniony + +- `action=product_title_changed_check` +- Cel: sprawdza, czy custom tytul rozni sie od bazowej nazwy produktu + +Logika pola `title_changed`: +- `true` gdy `products.title` jest niepuste i inne niz `products.name` +- `false` w pozostalych przypadkach + +Parametry: +- `api_key` (string, wymagany) +- `offer_id` (string, wymagany) +- `client_id` (int, wymagany) + +Przyklad: + +```bash +curl -G "https://example.com/api.php" \ + --data-urlencode "action=product_title_changed_check" \ + --data-urlencode "api_key=YOUR_API_KEY" \ + --data-urlencode "client_id=12" \ + --data-urlencode "offer_id=SKU-123" +``` + +Przyklad odpowiedzi: + +```json +{ + "result": "ok", + "product_id": 987, + "offer_id": "SKU-123", + "client_id": 12, + "title_changed": true, + "default_name": "Buty Air Run - wariant bazowy", + "custom_title": "Buty biegowe meskie Air Run 42" +} +``` + +### 4.3 Ustawienie Google Product Category + +- `action=product_google_category_set` +- Cel: zapisuje `products.google_product_category` + +Parametry: +- `api_key` (string, wymagany) +- `offer_id` (string, wymagany) +- `client_id` (int, wymagany) +- `google_product_category` (string, opcjonalny) + +Uwagi: +- wartosc jest przycinana (`trim`) +- pusta wartosc ustawia `NULL` (czyszczenie pola) +- zmiana zapisuje komentarz techniczny w `products_comments` + +Przyklad: + +```bash +curl -X POST "https://example.com/api.php" \ + -d "action=product_google_category_set" \ + -d "api_key=YOUR_API_KEY" \ + -d "client_id=12" \ + -d "offer_id=SKU-123" \ + -d "google_product_category=Apparel & Accessories > Shoes" +``` + +Przyklad odpowiedzi: + +```json +{ + "result": "ok", + "product_id": 987, + "offer_id": "SKU-123", + "client_id": 12, + "google_product_category": "Apparel & Accessories > Shoes" +} +``` + +### 4.4 Odczyt Google Product Category + +- `action=product_google_category_get` +- Cel: odczytuje `products.google_product_category` + +Parametry: +- `api_key` (string, wymagany) +- `offer_id` (string, wymagany) +- `client_id` (int, wymagany) + +Przyklad: + +```bash +curl -G "https://example.com/api.php" \ + --data-urlencode "action=product_google_category_get" \ + --data-urlencode "api_key=YOUR_API_KEY" \ + --data-urlencode "client_id=12" \ + --data-urlencode "offer_id=SKU-123" +``` + +Przyklad odpowiedzi: + +```json +{ + "result": "ok", + "product_id": 987, + "offer_id": "SKU-123", + "client_id": 12, + "google_product_category": "Apparel & Accessories > Shoes" +} +``` + +Jesli kategoria nie jest ustawiona: + +```json +{ + "result": "ok", + "product_id": 987, + "offer_id": "SKU-123", + "client_id": 12, + "google_product_category": null +} +``` + +### 4.5 Ustawienie custom_label_4 (istniejacy endpoint) + +- `action=product_custom_label_4_set` +- Cel: zapisuje `products.custom_label_4` + +Parametry: +- `api_key` (string, wymagany) +- `offer_id` (string, wymagany) +- `client_id` (int, wymagany) +- `custom_label_4` (string, opcjonalny) + +Przyklad: + +```bash +curl -X POST "https://example.com/api.php" \ + -d "action=product_custom_label_4_set" \ + -d "api_key=YOUR_API_KEY" \ + -d "client_id=12" \ + -d "offer_id=SKU-123" \ + -d "custom_label_4=bestseller" +``` + +Przyklad odpowiedzi: + +```json +{ + "result": "ok" +} +``` + +## 5. Walidacja i bledy + +### 5.1 Brak wymaganych parametrow + +```json +{ + "result": "error", + "message": "Missing required params: offer_id, client_id" +} +``` + +HTTP status: `422` + +### 5.2 Nieznana akcja + +`api.php` nie zwraca centralnego bledu dla nieznanej `action`. +Przy integracji AI zawsze ustawiaj jawnie `action` i weryfikuj, czy odpowiedz to JSON. + +## 6. Zalecany kontrakt dla agentow AI + +- Zawsze wysylaj `api_key`, `action`, `client_id`, `offer_id`. +- Po `product_title_set` od razu wywolaj `product_title_changed_check`. +- Po `product_google_category_set` od razu wywolaj `product_google_category_get`. +- Traktuj `null` jako brak wartosci. +- Przy statusach `401`, `404`, `422` przerywaj workflow i zwracaj czytelny blad operatorowi. + +## 7. Minimalny scenariusz end-to-end + +1. Ustaw tytul (`product_title_set`) +2. Potwierdz zmiane (`product_title_changed_check`) +3. Ustaw kategorie (`product_google_category_set`) +4. Odczytaj kategorie (`product_google_category_get`) + +To daje prosty, deterministyczny przeplyw dla automatyzacji AI.