Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bbcc032c2 | ||
| c9cb10950f | |||
| 1cd18c052f | |||
| d60e335ca6 | |||
| eb7badab65 | |||
| d83d0ecdea | |||
| b8ed7a46d8 | |||
| f9e5efbffb | |||
| 7fc8cff474 | |||
| 218a0e8956 | |||
| 0a14c92109 | |||
| c59501603d | |||
| 6f94daeb76 | |||
| 72159062f5 | |||
| 2461087d9b | |||
| 6434933dfb | |||
| 28f53b7998 | |||
| 98029b1720 | |||
| a531fabeaf | |||
| c53778ab36 | |||
| 52119a0724 | |||
| 97d7473753 | |||
| 754f004096 | |||
| 3b2d156e84 | |||
| c44f59894e | |||
| fe2a77e995 | |||
| f0b1152ab1 | |||
| 44ac25b063 | |||
| ee8459ca2a | |||
| 8e2e070eb7 | |||
| ec4e25946d | |||
| 4f66dbe42c | |||
| 4e720c5689 | |||
| 702e3a94be | |||
| 9a351c16ee | |||
| 4056296dab | |||
| 7158f4d369 | |||
| 1d613d8226 | |||
| 174a85a707 | |||
| 835386a887 | |||
| d2277c6d9d | |||
| 908c997b91 | |||
| 9d3ae9a470 | |||
| 09d266204e | |||
| 8f43f5ab4d | |||
| b17463bcbc | |||
| 76de81bca4 | |||
| 842ed77f5b | |||
| 96ed86649a | |||
| fdc4cac593 | |||
| 8f67d9de0a | |||
| 3ae0bc95e0 | |||
| 92ec5e1194 |
@@ -48,7 +48,29 @@
|
|||||||
"Bash(python3:*)",
|
"Bash(python3:*)",
|
||||||
"Bash(python:*)",
|
"Bash(python:*)",
|
||||||
"Bash(grep:*)",
|
"Bash(grep:*)",
|
||||||
"Bash(grep ^<b>ver:*)"
|
"Bash(grep ^<b>ver:*)",
|
||||||
|
"Bash(claude mcp:*)",
|
||||||
|
"mcp__serena__get_current_config",
|
||||||
|
"Bash(cd \"C:\\\\visual studio code\\\\projekty\\\\shopPRO\" && npx sass admin/layout/style-scss/style.scss admin/layout/style-css/style.css --no-source-map 2>&1 || sass admin/layout/style-scss/style.scss admin/layout/style-css/style.css --no-source-map 2>&1)",
|
||||||
|
"Bash(echo no 7z:*)",
|
||||||
|
"Bash(cd \"C:/visual studio code/projekty/shopPRO\" && php -r \"\n\\\\$files = [\n 'admin/templates/integrations/logs.php',\n 'admin/templates/site/main-layout.php',\n 'autoload/Domain/Integrations/IntegrationsRepository.php',\n 'autoload/admin/Controllers/IntegrationsController.php',\n];\n\\\\$zipPath = 'updates/0.30/ver_0.310.zip';\nif \\(!is_dir\\('updates/0.30'\\)\\) mkdir\\('updates/0.30', 0777, true\\);\nif \\(file_exists\\(\\\\$zipPath\\)\\) unlink\\(\\\\$zipPath\\);\n\\\\$zip = new ZipArchive\\(\\);\nif \\(\\\\$zip->open\\(\\\\$zipPath, ZipArchive::CREATE\\) !== true\\) { echo 'Cannot create ZIP'; exit\\(1\\); }\nforeach \\(\\\\$files as \\\\$f\\) {\n if \\(file_exists\\(\\\\$f\\)\\) {\n \\\\$zip->addFile\\(\\\\$f, \\\\$f\\);\n echo \\\\\"Added: \\\\$f\\\\n\\\\\";\n } else {\n echo \\\\\"MISSING: \\\\$f\\\\n\\\\\";\n }\n}\n\\\\$zip->close\\(\\);\necho \\\\\"ZIP created: \\\\$zipPath \\(\\\\\".filesize\\(\\\\$zipPath\\).\\\\\" bytes\\)\\\\n\\\\\";\n\" 2>&1)",
|
||||||
|
"Bash(where jar:*)",
|
||||||
|
"Bash(echo php not found:*)",
|
||||||
|
"Bash(/c/xampp/php/php:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(rm -rf \"/c/visual studio code/projekty/shopPRO/temp/temp_313\" && cd \"/c/visual studio code/projekty/shopPRO\" && powershell -File ./build-update.ps1 -FromTag v0.312 -ToTag v0.313 -ChangelogEntry \"FIX - sync płatności Apilo \\(int cast na apilo_order_id PPxxxxxx dawał 0\\) + logowanie decyzji sync do pp_log\" 2>&1)",
|
||||||
|
"Bash(which php:*)",
|
||||||
|
"mcp__serena__replace_symbol_body",
|
||||||
|
"mcp__serena__insert_after_symbol",
|
||||||
|
"Bash(php:*)",
|
||||||
|
"Bash(rm -rf \"C:/visual studio code/projekty/shopPRO/temp/temp_314\" && cd \"C:/visual studio code/projekty/shopPRO\" && powershell -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.313 -ToTag v0.314 -ChangelogEntry \"FIX - naprawa globalnej wyszukiwarki admin \\(Content-Type, Cache-Control, POST, try/catch\\), NEW - title strony z numerem zamówienia\" 2>&1)",
|
||||||
|
"mcp__serena__initial_instructions",
|
||||||
|
"mcp__serena__list_memories",
|
||||||
|
"mcp__serena__find_referencing_symbols",
|
||||||
|
"Bash(cd C:\\\\visual studio code\\\\projekty\\\\shopPRO:*)",
|
||||||
|
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && rm -rf temp/temp_317 && powershell -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.316 -ToTag v0.317 -ChangelogEntry \"FIX - klucz API: fix zapisu \\(brakowalo w whiteliście\\), przycisk Generuj losowy klucz, ulepszony routing API\" 2>&1)",
|
||||||
|
"Bash(./test.ps1)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
203
.htaccess
203
.htaccess
@@ -7,67 +7,25 @@ Options -Indexes
|
|||||||
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
||||||
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
|
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
|
||||||
|
|
||||||
# Przekierowanie z http na https, jeśli nie zawiera www
|
# Przekierowanie z http na https, jesli nie zawiera www
|
||||||
RewriteCond %{HTTPS} off
|
RewriteCond %{HTTPS} off
|
||||||
|
RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]
|
||||||
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
|
|
||||||
# Usuwanie końcowego slash'a dla niekatalogów
|
# Usuwanie koncowego slasha dla niekatalogów
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC] # Wyklucza ścieżki rozpoczynające się od "admin/"
|
RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC]
|
||||||
RewriteCond %{REQUEST_URI} (.+)/$
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
RewriteRule ^ %1 [R=301,L]
|
RewriteRule ^ %1 [R=301,L]
|
||||||
|
|
||||||
ErrorDocument 404 /index.php
|
|
||||||
|
|
||||||
RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]
|
RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]
|
||||||
RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]
|
RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]
|
||||||
RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [QSA,L]
|
RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]
|
||||||
|
|
||||||
RewriteRule ^admin/$ admin/index.php [L]
|
RewriteRule ^admin/$ admin/index.php [L]
|
||||||
|
|
||||||
RewriteRule ^wyszukiwarka/(.*)/([0-9]*)$ index.php?module=search&action=search_results&query=$1&bs=$2 [L]
|
|
||||||
RewriteRule ^wyszukiwarka/(.*)$ index.php?module=search&action=search_results&query=$1&bs=1 [L]
|
|
||||||
RewriteRule ^zamowienie/([a-zA-Z0-9-]*)$ index.php?module=shop_order&action=order_details&order_hash=$1 [L]
|
|
||||||
RewriteRule ^potwierdzenie-platnosci/([a-zA-Z0-9-]*)$ index.php?module=shop_order&action=payment_confirmation&order_hash=$1 [L]
|
|
||||||
RewriteRule ^tpay-status$ index.php?module=shop_order&action=payment_status_tpay%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^platnosc-status$ index.php?module=shop_order&action=payment_status_hotpay%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^przelewy24-status$ index.php?module=shop_order&action=payment_status_przelewy24pl%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^koszyk$ index.php?module=shop_basket&action=main_view [L]
|
|
||||||
RewriteRule ^koszyk-podsumowanie$ index.php?module=shop_basket&action=summary_view [L]
|
|
||||||
RewriteRule ^zloz-zamowienie$ index.php?module=shop_basket&action=basket_save [L]
|
|
||||||
RewriteRule ^rejestracja$ index.php?module=shop_client&action=register_form [L]
|
|
||||||
RewriteRule ^logowanie$ index.php?module=shop_client&action=login_form [L]
|
|
||||||
RewriteRule ^wylogowanie$ index.php?module=shop_client&action=logout [L]
|
|
||||||
RewriteRule ^odzyskiwanie-hasla$ index.php?module=shop_client&action=recover_password [L]
|
|
||||||
RewriteRule ^panel-klienta/zamowienia$ index.php?module=shop_client&action=client_orders [L]
|
|
||||||
RewriteRule ^panel-klienta/adresy$ index.php?module=shop_client&action=client_addresses [L]
|
|
||||||
RewriteRule ^panel-klienta/nowy-adres$ index.php?module=shop_client&action=address_edit [L]
|
|
||||||
RewriteRule ^panel-klienta/edytuj-adres/([0-9]*)$ index.php?module=shop_client&action=address_edit&id=$1 [L]
|
|
||||||
RewriteRule ^panel-klienta/usun-adres/([0-9]*)$ index.php?module=shop_client&action=address_delete&id=$1 [L]
|
|
||||||
RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]
|
RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]
|
||||||
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopBasket/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopClient/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopProduct/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopCoupon/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/search/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopBasket/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopClient/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopProduct/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopCoupon/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/search/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
|
|
||||||
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php
|
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php
|
||||||
RewriteRule ^ /%1 [R=301,L]
|
RewriteRule ^ /%1 [R=301,L]
|
||||||
<IfModule mod_deflate.c>
|
<IfModule mod_deflate.c>
|
||||||
@@ -116,168 +74,17 @@ ExpiresByType image/svg+xml "access plus 1 month"
|
|||||||
Order Deny,Allow
|
Order Deny,Allow
|
||||||
Deny from all
|
Deny from all
|
||||||
</Files>
|
</Files>
|
||||||
RewriteRule ^pl/$ index.php?a=change_language&id=pl [L]
|
|
||||||
RewriteRule ^en/$ index.php?a=change_language&id=en [L]
|
|
||||||
RewriteRule ^newsletter/signin/$ index.php?module=newsletter&action=signin [L]
|
|
||||||
RewriteRule ^newsletter/confirm/hash=(.*)$ index.php?module=newsletter&action=confirm&hash=$1 [L]
|
|
||||||
RewriteRule ^newsletter/unsubscribe/hash=(.*)$ index.php?module=newsletter&action=unsubscribe&hash=$1 [L]
|
|
||||||
RewriteRule ^producenci$ index.php?module=shop_producer&action=list&layout_id=2&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^producent/bibs$ index.php?module=shop_producer&action=products&producer_id=3&layout_id=2&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^producent/bibs/([0-9]+)$ index.php?module=shop_producer&action=products&producer_id=3&layout_id=2&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
|
|
||||||
RewriteRule ^sen-i-otulenie$ index.php?category=10&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^sen-i-otulenie/([0-9]+)$ index.php?category=10&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^sen-i-otulenie/1$ sen-i-otulenie [R=301,L]
|
|
||||||
RewriteRule ^kocyki-minky$ index.php?category=5&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kocyki-minky/([0-9]+)$ index.php?category=5&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kocyki-minky/1$ kocyki-minky [R=301,L]
|
|
||||||
RewriteRule ^kocyki-niemowlece-minky-50x70$ index.php?category=6&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kocyki-niemowlece-minky-50x70/([0-9]+)$ index.php?category=6&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kocyki-niemowlece-minky-50x70/1$ kocyki-niemowlece-minky-50x70 [R=301,L]
|
|
||||||
RewriteRule ^kocyki-sredniaka-minky-75x100$ index.php?category=7&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kocyki-sredniaka-minky-75x100/([0-9]+)$ index.php?category=7&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kocyki-sredniaka-minky-75x100/1$ kocyki-sredniaka-minky-75x100 [R=301,L]
|
|
||||||
RewriteRule ^kocyki-przedszkolaka-minky-100x130$ index.php?category=8&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kocyki-przedszkolaka-minky-100x130/([0-9]+)$ index.php?category=8&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kocyki-przedszkolaka-minky-100x130/1$ kocyki-przedszkolaka-minky-100x130 [R=301,L]
|
|
||||||
RewriteRule ^poduszki$ index.php?category=2&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^poduszki/([0-9]+)$ index.php?category=2&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^poduszki/1$ poduszki [R=301,L]
|
|
||||||
RewriteRule ^poduszki-niemowlaka-minky-25x35$ index.php?category=18&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^poduszki-niemowlaka-minky-25x35/([0-9]+)$ index.php?category=18&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^poduszki-niemowlaka-minky-25x35/1$ poduszki-niemowlaka-minky-25x35 [R=301,L]
|
|
||||||
RewriteRule ^poduszki/gwiazdki-40x40$ index.php?category=9&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^poduszki/gwiazdki-40x40/([0-9]+)$ index.php?category=9&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^poduszki/gwiazdki-40x40/1$ poduszki/gwiazdki-40x40 [R=301,L]
|
|
||||||
RewriteRule ^rozki$ index.php?category=1&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^rozki/([0-9]+)$ index.php?category=1&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^rozki/1$ rozki [R=301,L]
|
|
||||||
RewriteRule ^akcesoria$ index.php?category=4&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^akcesoria/([0-9]+)$ index.php?category=4&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^akcesoria/1$ akcesoria [R=301,L]
|
|
||||||
RewriteRule ^metryczki-dzieciece$ index.php?category=11&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^metryczki-dzieciece/([0-9]+)$ index.php?category=11&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^metryczki-dzieciece/1$ metryczki-dzieciece [R=301,L]
|
|
||||||
RewriteRule ^metryczki-ze-zdjeciem$ index.php?category=39&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^metryczki-ze-zdjeciem/([0-9]+)$ index.php?category=39&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^metryczki-ze-zdjeciem/1$ metryczki-ze-zdjeciem [R=301,L]
|
|
||||||
RewriteRule ^metryczki-dla-dziewczynki$ index.php?category=40&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^metryczki-dla-dziewczynki/([0-9]+)$ index.php?category=40&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^metryczki-dla-dziewczynki/1$ metryczki-dla-dziewczynki [R=301,L]
|
|
||||||
RewriteRule ^metryczki-dla-chlopca$ index.php?category=41&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^metryczki-dla-chlopca/([0-9]+)$ index.php?category=41&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^metryczki-dla-chlopca/1$ metryczki-dla-chlopca [R=301,L]
|
|
||||||
RewriteRule ^termofory-dla-dzieci$ index.php?category=17&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^termofory-dla-dzieci/([0-9]+)$ index.php?category=17&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^termofory-dla-dzieci/1$ termofory-dla-dzieci [R=301,L]
|
|
||||||
RewriteRule ^zawieszki$ index.php?category=43&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zawieszki/([0-9]+)$ index.php?category=43&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zawieszki/1$ zawieszki [R=301,L]
|
|
||||||
RewriteRule ^zawieszki-dekoracyjne$ index.php?category=32&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zawieszki-dekoracyjne/([0-9]+)$ index.php?category=32&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zawieszki-dekoracyjne/1$ zawieszki-dekoracyjne [R=301,L]
|
|
||||||
RewriteRule ^zawieszki-do-smoczkow-i-gryzakow$ index.php?category=44&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zawieszki-do-smoczkow-i-gryzakow/([0-9]+)$ index.php?category=44&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zawieszki-do-smoczkow-i-gryzakow/1$ zawieszki-do-smoczkow-i-gryzakow [R=301,L]
|
|
||||||
RewriteRule ^zawieszki-do-wozka$ index.php?category=45&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zawieszki-do-wozka/([0-9]+)$ index.php?category=45&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zawieszki-do-wozka/1$ zawieszki-do-wozka [R=301,L]
|
|
||||||
RewriteRule ^odziez-dziecieca$ index.php?category=12&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^odziez-dziecieca/([0-9]+)$ index.php?category=12&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^odziez-dziecieca/1$ odziez-dziecieca [R=301,L]
|
|
||||||
RewriteRule ^apaszki$ index.php?category=35&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^apaszki/([0-9]+)$ index.php?category=35&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^apaszki/1$ apaszki [R=301,L]
|
|
||||||
RewriteRule ^kominy-dzieciece$ index.php?category=15&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kominy-dzieciece/([0-9]+)$ index.php?category=15&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kominy-dzieciece/1$ kominy-dzieciece [R=301,L]
|
|
||||||
RewriteRule ^opaski$ index.php?category=37&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^opaski/([0-9]+)$ index.php?category=37&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^opaski/1$ opaski [R=301,L]
|
|
||||||
RewriteRule ^opaski-pin-up$ index.php?category=38&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^opaski-pin-up/([0-9]+)$ index.php?category=38&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^opaski-pin-up/1$ opaski-pin-up [R=301,L]
|
|
||||||
RewriteRule ^turbany$ index.php?category=14&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^turbany/([0-9]+)$ index.php?category=14&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^turbany/1$ turbany [R=301,L]
|
|
||||||
RewriteRule ^ubrania-dla-dziewczynek$ index.php?category=13&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^ubrania-dla-dziewczynek/([0-9]+)$ index.php?category=13&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^ubrania-dla-dziewczynek/1$ ubrania-dla-dziewczynek [R=301,L]
|
|
||||||
RewriteRule ^zestawy-i-kolekcje$ index.php?category=16&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zestawy-i-kolekcje/([0-9]+)$ index.php?category=16&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zestawy-i-kolekcje/1$ zestawy-i-kolekcje [R=301,L]
|
|
||||||
RewriteRule ^zestawy$ index.php?category=20&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zestawy/([0-9]+)$ index.php?category=20&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zestawy/1$ zestawy [R=301,L]
|
|
||||||
RewriteRule ^komplet-niemowlaka$ index.php?category=24&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^komplet-niemowlaka/([0-9]+)$ index.php?category=24&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^komplet-niemowlaka/1$ komplet-niemowlaka [R=301,L]
|
|
||||||
RewriteRule ^komplet-sredniaka$ index.php?category=28&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^komplet-sredniaka/([0-9]+)$ index.php?category=28&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^komplet-sredniaka/1$ komplet-sredniaka [R=301,L]
|
|
||||||
RewriteRule ^kolekcje$ index.php?category=29&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kolekcje/([0-9]+)$ index.php?category=29&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kolekcje/1$ kolekcje [R=301,L]
|
|
||||||
RewriteRule ^mama-bear-chmurki-mietowe$ index.php?category=36&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^mama-bear-chmurki-mietowe/([0-9]+)$ index.php?category=36&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^mama-bear-chmurki-mietowe/1$ mama-bear-chmurki-mietowe [R=301,L]
|
|
||||||
RewriteRule ^koniki-na-biegunach$ index.php?category=31&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^koniki-na-biegunach/([0-9]+)$ index.php?category=31&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^koniki-na-biegunach/1$ koniki-na-biegunach [R=301,L]
|
|
||||||
RewriteRule ^kroliki-na-hustawkach$ index.php?category=30&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kroliki-na-hustawkach/([0-9]+)$ index.php?category=30&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kroliki-na-hustawkach/1$ kroliki-na-hustawkach [R=301,L]
|
|
||||||
RewriteRule ^wyprzedaz$ index.php?category=27&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^wyprzedaz/([0-9]+)$ index.php?category=27&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^wyprzedaz/1$ wyprzedaz [R=301,L]
|
|
||||||
RewriteRule ^en/kocyk-minky-niemowlaka-50x70-en$ index.php?category=6&lang=en&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^en/kocyk-minky-niemowlaka-50x70-en/([0-9]+)$ index.php?category=6&lang=en&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^en/kocyk-minky-niemowlaka-50x70-en/1$ en/kocyk-minky-niemowlaka-50x70-en [R=301,L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/home$
|
RewriteCond %{REQUEST_URI} ^/home$
|
||||||
RewriteRule ^(.*)$ http://www.shoppro.project-dc.pl/ [R=permanent,L]
|
RewriteRule ^(.*)$ http://www.shoppro.project-dc.pl/ [R=permanent,L]
|
||||||
RewriteCond %{REQUEST_URI} ^/home-1$
|
RewriteCond %{REQUEST_URI} ^/home-1$
|
||||||
RewriteRule ^(.*)$ http://www.shoppro.project-dc.pl/ [R=permanent,L]
|
RewriteRule ^(.*)$ http://www.shoppro.project-dc.pl/ [R=permanent,L]
|
||||||
RewriteRule ^$ index.php?a=page&id=6&lang=pl [L]
|
RewriteRule ^$ index.php?a=page&id=6&lang=pl [L]
|
||||||
RewriteRule ^home$ index.php?a=page&id=6&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^home/([0-9]+)$ index.php?a=page&id=6&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^home/1$ home [R=301,L]
|
|
||||||
RewriteRule ^regulamin$ index.php?a=page&id=12&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^regulamin/([0-9]+)$ index.php?a=page&id=12&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^regulamin/1$ regulamin [R=301,L]
|
|
||||||
RewriteRule ^formy-platnosci$ index.php?a=page&id=13&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^formy-platnosci/([0-9]+)$ index.php?a=page&id=13&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^formy-platnosci/1$ formy-platnosci [R=301,L]
|
|
||||||
RewriteRule ^koszty-dostawy$ index.php?a=page&id=14&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^koszty-dostawy/([0-9]+)$ index.php?a=page&id=14&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^koszty-dostawy/1$ koszty-dostawy [R=301,L]
|
|
||||||
RewriteRule ^zwroty-i-reklamacje$ index.php?a=page&id=15&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zwroty-i-reklamacje/([0-9]+)$ index.php?a=page&id=15&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^zwroty-i-reklamacje/1$ zwroty-i-reklamacje [R=301,L]
|
|
||||||
RewriteRule ^o-nas$ index.php?a=page&id=4&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^o-nas/([0-9]+)$ index.php?a=page&id=4&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^o-nas/1$ o-nas [R=301,L]
|
|
||||||
RewriteRule ^blog$ index.php?a=page&id=9&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^blog/([0-9]+)$ index.php?a=page&id=9&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^blog/1$ blog [R=301,L]
|
|
||||||
RewriteRule ^kontakt$ index.php?a=page&id=5&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kontakt/([0-9]+)$ index.php?a=page&id=5&lang=pl&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^kontakt/1$ kontakt [R=301,L]
|
|
||||||
RewriteRule ^kolka-u-niemowlat-przyczyny-objawy-leczenie$ index.php?article=11&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^spacery-z-niemowlakiem-jak-sie-do-nich-przygotowac$ index.php?article=12&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^jak-wybrac-kocyk-i-poduszke-niemowlaka$ index.php?article=10&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^jak-wzmocnic-odpornosc-dziecka-w-trakcie-zimy-sprawdzone-sposoby-na-odpornosc$ index.php?article=13&lang=pl&%{QUERY_STRING} [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/home-en$
|
RewriteCond %{REQUEST_URI} ^/home-en$
|
||||||
RewriteRule ^(.*)$ http://www.shoppro.project-dc.pl/en/ [R=permanent,L]
|
RewriteRule ^(.*)$ http://www.shoppro.project-dc.pl/en/ [R=permanent,L]
|
||||||
RewriteCond %{REQUEST_URI} ^/home-en-1$
|
RewriteCond %{REQUEST_URI} ^/home-en-1$
|
||||||
RewriteRule ^(.*)$ http://www.shoppro.project-dc.pl/en/ [R=permanent,L]
|
RewriteRule ^(.*)$ http://www.shoppro.project-dc.pl/en/ [R=permanent,L]
|
||||||
RewriteRule ^$ index.php?a=page&id=6&lang=en [L]
|
RewriteRule ^$ index.php?a=page&id=6&lang=en [L]
|
||||||
RewriteRule ^en/home-en$ index.php?a=page&id=6&lang=en&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^en/home-en/([0-9]+)$ index.php?a=page&id=6&lang=en&bs=$1&%{QUERY_STRING} [L]
|
|
||||||
RewriteRule ^en/home-en/1$ en/home-en [R=301,L]
|
|
||||||
RewriteRule ^en/tytul-en$ index.php?article=13&lang=en&%{QUERY_STRING} [L]
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
RewriteRule ^ index.php [L]
|
RewriteRule ^ index.php [L]
|
||||||
# <FilesMatch "\.(php4|php5|php3|php2|php|phtml)$">
|
|
||||||
# SetHandler application/x-lsphp83 /opt/alt/php83 usr/bin/lsphp
|
|
||||||
# </FilesMatch>
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
47
.serena/memories/code_style_and_conventions.md
Normal file
47
.serena/memories/code_style_and_conventions.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Code Style and Conventions
|
||||||
|
|
||||||
|
## PHP Version
|
||||||
|
PHP 7.4 — no PHP 8.0+ features allowed.
|
||||||
|
|
||||||
|
## File Naming
|
||||||
|
- New classes: `ClassName.php` (no prefix)
|
||||||
|
- Legacy classes: `class.ClassName.php` (leave until migrated)
|
||||||
|
|
||||||
|
## DI Pattern (all new code)
|
||||||
|
```php
|
||||||
|
class ExampleRepository {
|
||||||
|
private $db;
|
||||||
|
public function __construct($db) {
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
public function find(int $id): ?array {
|
||||||
|
return $this->db->get('pp_table', '*', ['id' => $id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controller Wiring
|
||||||
|
- Admin: `admin\App::getControllerFactories()`
|
||||||
|
- Frontend: `front\App::getControllerFactories()`
|
||||||
|
- API: `api\ApiRouter::getControllerFactories()`
|
||||||
|
|
||||||
|
## Medoo ORM Pitfalls
|
||||||
|
- `$mdb->delete($table, $where)` takes 2 arguments, NOT 3
|
||||||
|
- `$mdb->get()` returns `null` when no record, NOT `false`
|
||||||
|
- After `$mdb->insert()`, check `$mdb->id()` to confirm success
|
||||||
|
|
||||||
|
## Test Conventions
|
||||||
|
- Extend `PHPUnit\Framework\TestCase`
|
||||||
|
- Mock Medoo: `$this->createMock(\medoo::class)`
|
||||||
|
- AAA pattern: Arrange, Act, Assert
|
||||||
|
- Mirror source structure: `tests/Unit/Domain/{Module}/{Class}Test.php`
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
- Redis via `\Shared\Cache\CacheHandler`
|
||||||
|
- Key pattern: `shop\product:{id}:{lang}:{permutation_hash}`
|
||||||
|
- Default TTL: 86400 (24h)
|
||||||
|
- Data serialized — use `unserialize()` after `get()`
|
||||||
|
|
||||||
|
## Database
|
||||||
|
- Table prefix: `pp_`
|
||||||
|
- Key tables: `pp_shop_products`, `pp_shop_orders`, `pp_shop_categories`, `pp_shop_clients`
|
||||||
65
.serena/memories/project_overview.md
Normal file
65
.serena/memories/project_overview.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# shopPRO — Project Overview
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
shopPRO is a PHP e-commerce platform with an admin panel, customer-facing storefront, and REST API.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Language**: PHP 7.4 (production runs PHP < 8.0 — do NOT use PHP 8.0+ syntax!)
|
||||||
|
- **ORM**: Medoo (`$mdb` global, injected via DI in new code)
|
||||||
|
- **Caching**: Redis via `\Shared\Cache\CacheHandler`
|
||||||
|
- **Testing**: PHPUnit 9.6 via `phpunit.phar`
|
||||||
|
- **Frontend**: Custom template engine (`\Shared\Tpl\Tpl`)
|
||||||
|
- **Database**: MySQL with `pp_` table prefix
|
||||||
|
- **Platform**: Windows (development), Linux (production)
|
||||||
|
|
||||||
|
## PHP 7.4 Constraint — CRITICAL
|
||||||
|
Do NOT use any PHP 8.0+ features:
|
||||||
|
- No `match` expressions (use ternary/if-else)
|
||||||
|
- No named arguments
|
||||||
|
- No union types (`int|string`)
|
||||||
|
- No `str_contains()`, `str_starts_with()`, `str_ends_with()`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
Domain-Driven Design with Dependency Injection.
|
||||||
|
|
||||||
|
### Layers
|
||||||
|
1. **Domain** (`autoload/Domain/`) — Business logic repositories, 27 modules
|
||||||
|
2. **Admin** (`autoload/admin/`) — Admin panel controllers, support, validation, view models
|
||||||
|
3. **Frontend** (`autoload/front/`) — Customer-facing controllers and views
|
||||||
|
4. **API** (`autoload/api/`) — REST API controllers
|
||||||
|
5. **Shared** (`autoload/Shared/`) — Cache, Email, Helpers, Html, Image, Tpl
|
||||||
|
|
||||||
|
### Domain Modules
|
||||||
|
Article, Attribute, Banner, Basket, Cache, Category, Client, Coupon, Dashboard, Dictionaries, Integrations, Languages, Layouts, Newsletter, Order, Pages, PaymentMethod, Producer, Product, ProductSet, Promotion, Scontainers, Settings, ShopStatus, Transport, Update, User
|
||||||
|
|
||||||
|
### Entry Points
|
||||||
|
- `index.php` — Frontend
|
||||||
|
- `admin/index.php` — Admin panel
|
||||||
|
- `api.php` — REST API
|
||||||
|
- `ajax.php` — Frontend AJAX
|
||||||
|
- `admin/ajax.php` — Admin AJAX
|
||||||
|
- `cron.php` — CRON jobs
|
||||||
|
|
||||||
|
### Namespace Conventions (case-sensitive on Linux!)
|
||||||
|
- `\Domain\` → `autoload/Domain/` (uppercase D)
|
||||||
|
- `\admin\Controllers\` → `autoload/admin/Controllers/` (lowercase a)
|
||||||
|
- `\Shared\` → `autoload/Shared/`
|
||||||
|
- `\front\` → `autoload/front/`
|
||||||
|
- `\api\` → `autoload/api/`
|
||||||
|
|
||||||
|
### Autoloader
|
||||||
|
Custom autoloader (not Composer at runtime). Tries:
|
||||||
|
1. `autoload/{namespace}/class.{ClassName}.php` (legacy)
|
||||||
|
2. `autoload/{namespace}/{ClassName}.php` (PSR-4 style)
|
||||||
|
|
||||||
|
### Key Classes
|
||||||
|
- `\admin\App` — Admin router
|
||||||
|
- `\front\App` — Frontend router
|
||||||
|
- `\front\LayoutEngine` — Frontend layout engine
|
||||||
|
- `\Shared\Helpers\Helpers` — Utility methods
|
||||||
|
- `\Shared\Tpl\Tpl` — Template engine
|
||||||
|
- `\Shared\Cache\CacheHandler` — Redis cache
|
||||||
|
- `\api\ApiRouter` — REST API router
|
||||||
|
|
||||||
|
## Test Suite
|
||||||
|
765 tests, 2153 assertions. Tests mirror source structure in `tests/Unit/`.
|
||||||
41
.serena/memories/suggested_commands.md
Normal file
41
.serena/memories/suggested_commands.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Suggested Commands
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
```bash
|
||||||
|
# Full test suite (recommended, PowerShell)
|
||||||
|
./test.ps1
|
||||||
|
|
||||||
|
# Specific test file
|
||||||
|
./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php
|
||||||
|
|
||||||
|
# Specific test method
|
||||||
|
./test.ps1 --filter testGetQuantityReturnsCorrectValue
|
||||||
|
|
||||||
|
# Via composer
|
||||||
|
composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
## System Utilities (Windows with Git Bash)
|
||||||
|
```bash
|
||||||
|
# Use Unix-style commands (Git Bash shell)
|
||||||
|
ls # list directory
|
||||||
|
grep -r # search content (prefer Serena tools instead)
|
||||||
|
git status # git operations
|
||||||
|
git log --oneline -10
|
||||||
|
git diff
|
||||||
|
git add <file>
|
||||||
|
git commit -m "message"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
```bash
|
||||||
|
# No build step — PHP is interpreted
|
||||||
|
# No linting/formatting tool configured
|
||||||
|
# Entry points are served via web server (XAMPP)
|
||||||
|
```
|
||||||
|
|
||||||
|
## PHP binary
|
||||||
|
```
|
||||||
|
C:\xampp\php\php.exe
|
||||||
|
```
|
||||||
25
.serena/memories/task_completion_checklist.md
Normal file
25
.serena/memories/task_completion_checklist.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Task Completion Checklist
|
||||||
|
|
||||||
|
When user says "KONIEC PRACY", execute in order:
|
||||||
|
|
||||||
|
1. **Run tests** — `./test.ps1`
|
||||||
|
2. **Update documentation if needed**:
|
||||||
|
- `docs/DATABASE_STRUCTURE.md`
|
||||||
|
- `docs/PROJECT_STRUCTURE.md`
|
||||||
|
- `docs/FORM_EDIT_SYSTEM.md`
|
||||||
|
- `docs/CHANGELOG.md`
|
||||||
|
- `docs/TESTING.md`
|
||||||
|
3. **SQL migrations** (if DB changes): place in `migrations/{version}.sql`
|
||||||
|
- NOT in `updates/` — build script reads from `migrations/` automatically
|
||||||
|
4. **Commit** changes
|
||||||
|
5. **Push** to remote
|
||||||
|
|
||||||
|
## Key Documentation Files
|
||||||
|
- `docs/MEMORY.md` — project memory, known issues
|
||||||
|
- `docs/PROJECT_STRUCTURE.md` — architecture
|
||||||
|
- `docs/DATABASE_STRUCTURE.md` — full DB schema
|
||||||
|
- `docs/TESTING.md` — test suite guide
|
||||||
|
- `docs/FORM_EDIT_SYSTEM.md` — form system
|
||||||
|
- `docs/CHANGELOG.md` — version history
|
||||||
|
- `docs/API.md` — REST API docs
|
||||||
|
- `docs/UPDATE_INSTRUCTIONS.md` — update packages
|
||||||
@@ -27,6 +27,7 @@ project_name: "shopPRO"
|
|||||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||||
languages:
|
languages:
|
||||||
- typescript
|
- typescript
|
||||||
|
- php
|
||||||
|
|
||||||
# the encoding used by text files in the project
|
# the encoding used by text files in the project
|
||||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
@@ -115,3 +116,10 @@ initial_prompt: ""
|
|||||||
# override of the corresponding setting in serena_config.yml, see the documentation there.
|
# override of the corresponding setting in serena_config.yml, see the documentation there.
|
||||||
# If null or missing, the value from the global config is used.
|
# If null or missing, the value from the global config is used.
|
||||||
symbol_info_budget:
|
symbol_info_budget:
|
||||||
|
|
||||||
|
# The language backend to use for this project.
|
||||||
|
# If not set, the global setting from serena_config.yml is used.
|
||||||
|
# Valid values: LSP, JetBrains
|
||||||
|
# Note: the backend is fixed at startup. If a project with a different backend
|
||||||
|
# is activated post-init, an error will be returned.
|
||||||
|
language_backend:
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ layout/style-scss/style.scss
|
|||||||
layout/style-scss/_mixins.scss
|
layout/style-scss/_mixins.scss
|
||||||
layout/style-scss/_mixins.css
|
layout/style-scss/_mixins.css
|
||||||
|
|
||||||
|
# macOS metadata
|
||||||
|
*.DS_Store
|
||||||
|
|
||||||
# Temp / cache / backups
|
# Temp / cache / backups
|
||||||
temp/
|
temp/
|
||||||
backups/
|
backups/
|
||||||
|
|||||||
6
.vscode/ftp-kr.json
vendored
6
.vscode/ftp-kr.json
vendored
@@ -14,6 +14,10 @@
|
|||||||
".git",
|
".git",
|
||||||
".svn",
|
".svn",
|
||||||
"/.vscode",
|
"/.vscode",
|
||||||
"/temp/*"
|
"/temp",
|
||||||
|
"/.serena",
|
||||||
|
"/.claude",
|
||||||
|
"/docs",
|
||||||
|
"/tests"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ composer test
|
|||||||
|
|
||||||
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
|
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
|
||||||
|
|
||||||
Current suite: **750 tests, 2114 assertions**.
|
Current suite: **805 tests, 2253 assertions**.
|
||||||
|
|
||||||
### Creating Updates
|
### Creating Updates
|
||||||
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs.
|
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs.
|
||||||
@@ -116,7 +116,7 @@ All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `fro
|
|||||||
- Constructor DI with `$db` (Medoo instance)
|
- Constructor DI with `$db` (Medoo instance)
|
||||||
- Methods serve both admin and frontend (shared Domain, no separate services)
|
- Methods serve both admin and frontend (shared Domain, no separate services)
|
||||||
|
|
||||||
**Domain Modules**: Article, Attribute, Banner, Basket, Cache, Category, Client, Coupon, Dashboard, Dictionaries, Integrations, Languages, Layouts, Newsletter, Order, Pages, PaymentMethod, Producer, Product, ProductSet, Promotion, Scontainers, Settings, ShopStatus, Transport, Update, User
|
**Domain Modules**: Article, Attribute, Banner, Basket, Cache, Category, Client, Coupon, CronJob, Dashboard, Dictionaries, Integrations, Languages, Layouts, Newsletter, Order, Pages, PaymentMethod, Producer, Product, ProductSet, Promotion, Scontainers, Settings, ShopStatus, Transport, Update, User
|
||||||
|
|
||||||
**Admin Controllers** (`autoload/admin/Controllers/`):
|
**Admin Controllers** (`autoload/admin/Controllers/`):
|
||||||
- DI via constructor (repositories injected)
|
- DI via constructor (repositories injected)
|
||||||
@@ -223,3 +223,5 @@ Before starting implementation, review current state of docs (see AGENTS.md for
|
|||||||
- `docs/CHANGELOG.md` — version history
|
- `docs/CHANGELOG.md` — version history
|
||||||
- `docs/API.md` — REST API documentation (ordersPRO)
|
- `docs/API.md` — REST API documentation (ordersPRO)
|
||||||
- `docs/UPDATE_INSTRUCTIONS.md` — how to build client update packages
|
- `docs/UPDATE_INSTRUCTIONS.md` — how to build client update packages
|
||||||
|
|
||||||
|
## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP
|
||||||
@@ -162,7 +162,7 @@ $isCompactColumn = function(array $column): bool {
|
|||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Szukaj</button>
|
<button type="submit" class="btn btn-primary btn-sm">Szukaj</button>
|
||||||
<a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm">Wyczyść</a>
|
<a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm js-table-filters-clear">Wyczyść</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,6 +312,40 @@ $isCompactColumn = function(array $column): bool {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Table state persistence — redirect ASAP to saved view
|
||||||
|
(function() {
|
||||||
|
var basePath = <?= json_encode($list->basePath); ?>;
|
||||||
|
var stateKey = 'tableListQuery_' + basePath;
|
||||||
|
var clearKey = 'tableListCleared_' + basePath;
|
||||||
|
|
||||||
|
var pathname = window.location.pathname.replace(/\/+$/, '/');
|
||||||
|
var bp = basePath.replace(/\/+$/, '/');
|
||||||
|
|
||||||
|
var queryPart = '';
|
||||||
|
if (pathname.length > bp.length && pathname.indexOf(bp) === 0) {
|
||||||
|
queryPart = pathname.substring(bp.length);
|
||||||
|
}
|
||||||
|
if (!queryPart && window.location.search) {
|
||||||
|
queryPart = window.location.search.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var justCleared = sessionStorage.getItem(clearKey) === '1';
|
||||||
|
sessionStorage.removeItem(clearKey);
|
||||||
|
|
||||||
|
if (queryPart) {
|
||||||
|
localStorage.setItem(stateKey, queryPart);
|
||||||
|
} else if (!justCleared) {
|
||||||
|
var saved = localStorage.getItem(stateKey);
|
||||||
|
if (saved) {
|
||||||
|
window.location.replace(basePath + saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function($) {
|
(function($) {
|
||||||
if (!$) {
|
if (!$) {
|
||||||
@@ -550,5 +584,17 @@ $isCompactColumn = function(array $column): bool {
|
|||||||
$(document).on('change.tablePerPage', '.js-per-page-select', function() {
|
$(document).on('change.tablePerPage', '.js-per-page-select', function() {
|
||||||
$(this).closest('form').trigger('submit');
|
$(this).closest('form').trigger('submit');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Table state clear on "Wyczyść" ---
|
||||||
|
var stateStorageKey = 'tableListQuery_' + <?= json_encode($list->basePath); ?>;
|
||||||
|
var stateClearKey = 'tableListCleared_' + <?= json_encode($list->basePath); ?>;
|
||||||
|
|
||||||
|
$(document).off('click.tableClearState', '.js-table-filters-clear');
|
||||||
|
$(document).on('click.tableClearState', '.js-table-filters-clear', function() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(stateStorageKey);
|
||||||
|
sessionStorage.setItem(stateClearKey, '1');
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
})(window.jQuery);
|
})(window.jQuery);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
19
admin/templates/integrations/logs.php
Normal file
19
admin/templates/integrations/logs.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
||||||
|
|
||||||
|
<div class="mt15">
|
||||||
|
<a href="/admin/integrations/logs_clear/" class="btn btn-danger btn-sm"
|
||||||
|
onclick="return confirm('Na pewno chcesz usunac wszystkie logi?');">
|
||||||
|
<i class="fa fa-trash"></i> Wyczysc wszystkie logi
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$('body').on('click', '.log-context-btn', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var id = $(this).data('id');
|
||||||
|
$('#log-context-' + id).toggle();
|
||||||
|
$(this).text($('#log-context-' + id).is(':visible') ? 'Ukryj' : 'Pokaz');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -91,6 +91,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- API key -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-lg-3 control-label" for="inputDefault">API key</label>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<div class="bs-component">
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control" type="text" id="api_key" name="api_key" placeholder="" value="<?= $this -> settings['api_key'];?>">
|
||||||
|
<span class="input-group-addon cursor" field-id="api_key">
|
||||||
|
<i class="fa fa-save"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
.bulk-action-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-bar__info {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-col-bulk-check {
|
||||||
|
width: 36px;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.product-archive-thumb-wrap {
|
.product-archive-thumb-wrap {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -96,5 +118,119 @@
|
|||||||
$popup.removeClass('is-visible');
|
$popup.removeClass('is-visible');
|
||||||
$popupImage.attr('src', '');
|
$popupImage.attr('src', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Bulk select ---
|
||||||
|
var $table = $('.table-list-table');
|
||||||
|
var $bar = $('#js-bulk-action-bar');
|
||||||
|
var $label = $bar.find('.js-bulk-count-label');
|
||||||
|
|
||||||
|
// Inject select-all checkbox into _checkbox column header
|
||||||
|
$table.find('thead th.table-col-bulk-check').html(
|
||||||
|
'<input type="checkbox" id="js-bulk-select-all" title="Zaznacz wszystkie">'
|
||||||
|
);
|
||||||
|
|
||||||
|
function updateBar() {
|
||||||
|
var count = $table.find('.js-bulk-check:checked').length;
|
||||||
|
if (count > 0) {
|
||||||
|
$label.text('Zaznaczono: ' + count);
|
||||||
|
$bar.show();
|
||||||
|
} else {
|
||||||
|
$bar.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('change.bulkSelect', '#js-bulk-select-all', function() {
|
||||||
|
var checked = $(this).is(':checked');
|
||||||
|
$table.find('.js-bulk-check').prop('checked', checked);
|
||||||
|
updateBar();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('change.bulkSelect', '.js-bulk-check', function() {
|
||||||
|
var total = $table.find('.js-bulk-check').length;
|
||||||
|
var checked = $table.find('.js-bulk-check:checked').length;
|
||||||
|
$('#js-bulk-select-all').prop('indeterminate', checked > 0 && checked < total);
|
||||||
|
$('#js-bulk-select-all').prop('checked', checked === total && total > 0);
|
||||||
|
updateBar();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click.bulkDelete', '.js-bulk-delete-btn', function() {
|
||||||
|
var ids = [];
|
||||||
|
$table.find('.js-bulk-check:checked').each(function() {
|
||||||
|
ids.push($(this).val());
|
||||||
|
});
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmMsg = 'UWAGA! Operacja nieodwracalna!\n\n'
|
||||||
|
+ 'Wybrane produkty (' + ids.length + ' szt.) zostaną trwale usunięte razem ze wszystkimi zdjęciami i załącznikami z serwera.\n\n'
|
||||||
|
+ 'Czy na pewno chcesz usunąć zaznaczone produkty?';
|
||||||
|
|
||||||
|
var doDelete = function() {
|
||||||
|
var $btn = $('.js-bulk-delete-btn');
|
||||||
|
$btn.prop('disabled', true).text('Usuwanie…');
|
||||||
|
|
||||||
|
var formData = [];
|
||||||
|
for (var i = 0; i < ids.length; i++) {
|
||||||
|
formData.push('ids%5B%5D=' + encodeURIComponent(ids[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/admin/product_archive/bulk_delete_permanent/',
|
||||||
|
type: 'POST',
|
||||||
|
data: formData.join('&'),
|
||||||
|
contentType: 'application/x-www-form-urlencoded',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(resp) {
|
||||||
|
if (resp && resp.deleted > 0) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Nie udało się usunąć produktów. Spróbuj ponownie.');
|
||||||
|
$btn.prop('disabled', false).html('<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('Błąd podczas usuwania produktów. Spróbuj ponownie.');
|
||||||
|
$btn.prop('disabled', false).html('<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof $.confirm === 'function') {
|
||||||
|
$.confirm({
|
||||||
|
title: 'Potwierdzenie',
|
||||||
|
content: confirmMsg,
|
||||||
|
type: 'red',
|
||||||
|
boxWidth: '560px',
|
||||||
|
useBootstrap: false,
|
||||||
|
animation: 'scale',
|
||||||
|
closeAnimation: 'scale',
|
||||||
|
backgroundDismissAnimation: 'shake',
|
||||||
|
container: 'body',
|
||||||
|
theme: 'modern',
|
||||||
|
columnClass: '',
|
||||||
|
typeAnimated: true,
|
||||||
|
lazyOpen: false,
|
||||||
|
draggable: false,
|
||||||
|
closeIcon: true,
|
||||||
|
containerFluid: true,
|
||||||
|
escapeKey: true,
|
||||||
|
backgroundDismiss: true,
|
||||||
|
buttons: {
|
||||||
|
cancel: {
|
||||||
|
text: 'Anuluj',
|
||||||
|
btnClass: 'btn-default'
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
text: 'Tak, usuń trwale',
|
||||||
|
btnClass: 'btn-danger',
|
||||||
|
action: doDelete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (window.confirm(confirmMsg)) {
|
||||||
|
doDelete();
|
||||||
|
}
|
||||||
|
});
|
||||||
})(window.jQuery);
|
})(window.jQuery);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
<div id="js-bulk-action-bar" class="bulk-action-bar" style="display:none;">
|
||||||
|
<span class="bulk-action-bar__info js-bulk-count-label">Zaznaczono: 0</span>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm js-bulk-delete-btn">
|
||||||
|
<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
<?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
||||||
|
|
||||||
<?php if (!empty($this->viewModel->customScriptView)): ?>
|
<?php if (!empty($this->viewModel->customScriptView)): ?>
|
||||||
|
|||||||
@@ -1,3 +1,30 @@
|
|||||||
|
<style type="text/css">
|
||||||
|
.attr-copy-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 4px;
|
||||||
|
transition: background .12s, color .12s, border-color .12s;
|
||||||
|
}
|
||||||
|
.attr-copy-btn:hover {
|
||||||
|
background: #f4f4f4;
|
||||||
|
border-color: #aaa;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.attr-copy-btn--copied {
|
||||||
|
background: #d4edda !important;
|
||||||
|
border-color: #28a745 !important;
|
||||||
|
color: #28a745 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function() {
|
(function() {
|
||||||
var orderId = <?= (int)($this->order_id ?? 0);?>;
|
var orderId = <?= (int)($this->order_id ?? 0);?>;
|
||||||
@@ -378,6 +405,68 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
function fallbackCopy(text) {
|
||||||
|
var $tmp = $('<textarea>').css({position: 'fixed', top: 0, left: 0, opacity: 0}).val(text);
|
||||||
|
$('body').append($tmp);
|
||||||
|
$tmp[0].select();
|
||||||
|
try { document.execCommand('copy'); } catch (e) {}
|
||||||
|
$tmp.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.atributes').each(function() {
|
||||||
|
var $div = $(this);
|
||||||
|
var html = $.trim($div.html());
|
||||||
|
if (!html) { return; }
|
||||||
|
|
||||||
|
var parts = html.split(/<br\s*\/?>/i);
|
||||||
|
var newParts = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var part = $.trim(parts[i]);
|
||||||
|
if (!part) { continue; }
|
||||||
|
|
||||||
|
var match = part.match(/^(<b>[^<]*<\/b>\s*:\s*)(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
var labelHtml = match[1];
|
||||||
|
var value = $.trim(match[2]);
|
||||||
|
var escapedValue = $('<div>').text(value).html();
|
||||||
|
part = labelHtml + escapedValue
|
||||||
|
+ ' <button type="button" class="js-attr-copy-btn attr-copy-btn" data-value="'
|
||||||
|
+ escapedValue + '" title="Kopiuj: ' + escapedValue + '">'
|
||||||
|
+ '<i class="fa fa-copy"></i></button>';
|
||||||
|
}
|
||||||
|
newParts.push(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
$div.html(newParts.join('<br>'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.js-attr-copy-btn', function() {
|
||||||
|
var $btn = $(this);
|
||||||
|
var value = String($btn.data('value'));
|
||||||
|
|
||||||
|
function showCopied() {
|
||||||
|
$btn.addClass('attr-copy-btn--copied');
|
||||||
|
$btn.find('i').removeClass('fa-copy').addClass('fa-check');
|
||||||
|
setTimeout(function() {
|
||||||
|
$btn.removeClass('attr-copy-btn--copied');
|
||||||
|
$btn.find('i').removeClass('fa-check').addClass('fa-copy');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(value).then(showCopied, function() {
|
||||||
|
fallbackCopy(value);
|
||||||
|
showCopied();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy(value);
|
||||||
|
showCopied();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('body').on('click', '.btn-toggle-trustmate', function(e) {
|
$('body').on('click', '.btn-toggle-trustmate', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ $orderId = (int)($this -> order['id'] ?? 0);
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="site-title">Szczegóły zamówienia: <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?></div>
|
<div class="site-title">Szczegóły zamówienia: <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?></div>
|
||||||
|
<script>document.title = 'Zamówienie <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?> - shopPro';</script>
|
||||||
|
|
||||||
<div class="od-actions mb15">
|
<div class="od-actions mb15">
|
||||||
<a href="/admin/shop_order/list/" class="btn btn-dark btn-sm">
|
<a href="/admin/shop_order/list/" class="btn btn-dark btn-sm">
|
||||||
@@ -89,6 +90,19 @@ $orderId = (int)($this -> order['id'] ?? 0);
|
|||||||
<div>
|
<div>
|
||||||
<b><?= $this -> order[ 'payment_method' ];?> </b>
|
<b><?= $this -> order[ 'payment_method' ];?> </b>
|
||||||
</div>
|
</div>
|
||||||
|
<? if ( !empty($this -> order['apilo_order_id']) ):?>
|
||||||
|
<br/>
|
||||||
|
<div>
|
||||||
|
<i class="fa fa-cloud"></i> Apilo: <b style="color: #27ae60;">tak</b>
|
||||||
|
— ID: <b id="order-apilo-id"><?= htmlspecialchars((string)$this -> order['apilo_order_id'], ENT_QUOTES, 'UTF-8');?></b>
|
||||||
|
<i class="fa fa-copy" onclick="copyToClipboard( 'order-apilo-id' ); return false;"></i>
|
||||||
|
</div>
|
||||||
|
<? else:?>
|
||||||
|
<br/>
|
||||||
|
<div>
|
||||||
|
<i class="fa fa-cloud"></i> Apilo: <b style="color: #c0392b;">nie</b>
|
||||||
|
</div>
|
||||||
|
<? endif;?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="paid-status panel">
|
<div class="paid-status panel">
|
||||||
@@ -184,13 +198,14 @@ $orderId = (int)($this -> order['id'] ?? 0);
|
|||||||
<?= $product[ 'message' ] != '' ? '<strong>Wiadomość:</strong> ' . $product['message'] : '';?>
|
<?= $product[ 'message' ] != '' ? '<strong>Wiadomość:</strong> ' . $product['message'] : '';?>
|
||||||
</div>
|
</div>
|
||||||
<div class="od-mobile-price-line">
|
<div class="od-mobile-price-line">
|
||||||
<?= (int)$product['quantity'];?> × <?= \Shared\Helpers\Helpers::decimal( $product['price_brutto_promo'] );?> = <?= \Shared\Helpers\Helpers::decimal( $product['price_brutto_promo'] * $product['quantity'] );?> zł
|
<? $effective = ((float)$product['price_brutto_promo'] > 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?>
|
||||||
|
<?= (int)$product['quantity'];?> × <?= \Shared\Helpers\Helpers::decimal( $effective );?> = <?= \Shared\Helpers\Helpers::decimal( $effective * $product['quantity'] );?> zł
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="tab-center"><?= $product[ 'quantity' ];?></td>
|
<td class="tab-center"><?= $product[ 'quantity' ];?></td>
|
||||||
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto' ] );?> zł</td>
|
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto' ] );?> zł</td>
|
||||||
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto_promo' ] );?> zł</td>
|
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $effective );?> zł</td>
|
||||||
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto_promo' ] * $product[ 'quantity' ] );?> zł</td>
|
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $effective * $product[ 'quantity' ] );?> zł</td>
|
||||||
</tr>
|
</tr>
|
||||||
<? endforeach; endif;?>
|
<? endforeach; endif;?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -153,6 +153,11 @@
|
|||||||
<i class="fa fa-cogs" aria-hidden="true"></i>shopPRO
|
<i class="fa fa-cogs" aria-hidden="true"></i>shopPRO
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/integrations/logs/">
|
||||||
|
<i class="fa fa-list-alt" aria-hidden="true"></i>Logi
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview">
|
<div class="preview">
|
||||||
@@ -317,7 +322,7 @@
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/admin/settings/globalSearchAjax/',
|
url: '/admin/settings/globalSearchAjax/',
|
||||||
type: 'GET',
|
type: 'POST',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
data: { q: phrase },
|
data: { q: phrase },
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
@@ -328,8 +333,12 @@
|
|||||||
|
|
||||||
renderResults(response.items || []);
|
renderResults(response.items || []);
|
||||||
},
|
},
|
||||||
error: function() {
|
error: function(xhr) {
|
||||||
$results.html('<div class="admin-global-search-empty">Błąd połączenia</div>').addClass('open');
|
var msg = 'Błąd połączenia';
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
msg = 'Błąd parsowania odpowiedzi';
|
||||||
|
}
|
||||||
|
$results.html('<div class="admin-global-search-empty">' + msg + '</div>').addClass('open');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<span class="panel-title">Changelog</span>
|
<span class="panel-title">Changelog</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<?= @file_get_contents( 'https://shoppro.project-dc.pl/updates/changelog.php' ); ?>
|
<?= @file_get_contents( 'https://shoppro.project-dc.pl/updates/changelog.php?ver=' . $this->ver ); ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
45
api.php
45
api.php
@@ -47,6 +47,43 @@ if ( !$isApiRequest )
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- API routing (ordersPRO) ---
|
||||||
|
if ( $isApiRequest )
|
||||||
|
{
|
||||||
|
if ( !headers_sent() )
|
||||||
|
header( 'Content-Type: application/json; charset=utf-8' );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$mdb = new medoo( [
|
||||||
|
'database_type' => 'mysql',
|
||||||
|
'database_name' => $database[ 'name' ],
|
||||||
|
'server' => $database[ 'host' ],
|
||||||
|
'username' => $database[ 'user' ],
|
||||||
|
'password' => $database[ 'password' ],
|
||||||
|
'charset' => 'utf8'
|
||||||
|
] );
|
||||||
|
|
||||||
|
$settingsRepo = new \Domain\Settings\SettingsRepository( $mdb );
|
||||||
|
$router = new \api\ApiRouter( $mdb, $settingsRepo );
|
||||||
|
$router->handle();
|
||||||
|
}
|
||||||
|
catch ( \Throwable $e )
|
||||||
|
{
|
||||||
|
if ( !headers_sent() )
|
||||||
|
header( 'Content-Type: application/json; charset=utf-8' );
|
||||||
|
|
||||||
|
http_response_code( 500 );
|
||||||
|
echo json_encode( [
|
||||||
|
'status' => 'error',
|
||||||
|
'code' => 'INTERNAL_ERROR',
|
||||||
|
'message' => 'Internal server error'
|
||||||
|
], JSON_UNESCAPED_UNICODE );
|
||||||
|
}
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$mdb = new medoo( [
|
$mdb = new medoo( [
|
||||||
'database_type' => 'mysql',
|
'database_type' => 'mysql',
|
||||||
'database_name' => $database[ 'name' ],
|
'database_name' => $database[ 'name' ],
|
||||||
@@ -59,14 +96,6 @@ $mdb = new medoo( [
|
|||||||
$settingsRepo = new \Domain\Settings\SettingsRepository( $mdb );
|
$settingsRepo = new \Domain\Settings\SettingsRepository( $mdb );
|
||||||
$settings = $settingsRepo->allSettings();
|
$settings = $settingsRepo->allSettings();
|
||||||
|
|
||||||
// --- API routing (ordersPRO) ---
|
|
||||||
if ( $isApiRequest )
|
|
||||||
{
|
|
||||||
$router = new \api\ApiRouter( $mdb, $settingsRepo );
|
|
||||||
$router->handle();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Ekomi CSV export ---
|
// --- Ekomi CSV export ---
|
||||||
if ( \Shared\Helpers\Helpers::get( 'ekomi_csv' ) )
|
if ( \Shared\Helpers\Helpers::get( 'ekomi_csv' ) )
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -360,6 +360,9 @@ class ArticleRepository
|
|||||||
public function archive(int $articleId): bool
|
public function archive(int $articleId): bool
|
||||||
{
|
{
|
||||||
$result = $this->db->update('pp_articles', ['status' => -1], ['id' => $articleId]);
|
$result = $this->db->update('pp_articles', ['status' => -1], ['id' => $articleId]);
|
||||||
|
if ($result) {
|
||||||
|
$this->db->delete('pp_routes', ['article_id' => $articleId]);
|
||||||
|
}
|
||||||
return (bool)$result;
|
return (bool)$result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,6 +384,7 @@ class ArticleRepository
|
|||||||
$this->db->delete('pp_articles_langs', ['article_id' => $articleId]);
|
$this->db->delete('pp_articles_langs', ['article_id' => $articleId]);
|
||||||
$this->db->delete('pp_articles_images', ['article_id' => $articleId]);
|
$this->db->delete('pp_articles_images', ['article_id' => $articleId]);
|
||||||
$this->db->delete('pp_articles_files', ['article_id' => $articleId]);
|
$this->db->delete('pp_articles_files', ['article_id' => $articleId]);
|
||||||
|
$this->db->delete('pp_routes', ['article_id' => $articleId]);
|
||||||
$this->db->delete('pp_articles', ['id' => $articleId]);
|
$this->db->delete('pp_articles', ['id' => $articleId]);
|
||||||
|
|
||||||
\Shared\Helpers\Helpers::delete_dir('../upload/article_images/article_' . $articleId . '/');
|
\Shared\Helpers\Helpers::delete_dir('../upload/article_images/article_' . $articleId . '/');
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class AttributeRepository
|
|||||||
FROM pp_shop_attributes AS sa
|
FROM pp_shop_attributes AS sa
|
||||||
WHERE {$whereSql}
|
WHERE {$whereSql}
|
||||||
";
|
";
|
||||||
$stmtCount = $this->db->query($sqlCount, $params);
|
$stmtCount = $this->db->query($sqlCount, $whereData['params']);
|
||||||
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
|
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
|
||||||
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
|
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
|
||||||
|
|
||||||
@@ -655,6 +655,95 @@ class AttributeRepository
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find existing attribute by name/type or create a new one for API integration.
|
||||||
|
*
|
||||||
|
* @return array{id:int,created:bool}|null
|
||||||
|
*/
|
||||||
|
public function ensureAttributeForApi(string $name, int $type = 0, string $langId = 'pl'): ?array
|
||||||
|
{
|
||||||
|
$normalizedName = trim($name);
|
||||||
|
$normalizedLangId = trim($langId) !== '' ? trim($langId) : 'pl';
|
||||||
|
$normalizedType = $this->toTypeValue($type);
|
||||||
|
if ($normalizedName === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingId = $this->findAttributeIdByNameAndType($normalizedName, $normalizedType);
|
||||||
|
if ($existingId > 0) {
|
||||||
|
return ['id' => $existingId, 'created' => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->insert('pp_shop_attributes', [
|
||||||
|
'status' => 1,
|
||||||
|
'type' => $normalizedType,
|
||||||
|
'o' => $this->nextOrder(),
|
||||||
|
]);
|
||||||
|
$attributeId = (int) $this->db->id();
|
||||||
|
if ($attributeId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->insert('pp_shop_attributes_langs', [
|
||||||
|
'attribute_id' => $attributeId,
|
||||||
|
'lang_id' => $normalizedLangId,
|
||||||
|
'name' => $normalizedName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->clearTempAndCache();
|
||||||
|
$this->clearFrontCache($attributeId, 'frontAttributeDetails');
|
||||||
|
|
||||||
|
return ['id' => $attributeId, 'created' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find existing value by name within attribute or create a new one for API integration.
|
||||||
|
*
|
||||||
|
* @return array{id:int,created:bool}|null
|
||||||
|
*/
|
||||||
|
public function ensureAttributeValueForApi(int $attributeId, string $name, string $langId = 'pl'): ?array
|
||||||
|
{
|
||||||
|
$normalizedName = trim($name);
|
||||||
|
$normalizedLangId = trim($langId) !== '' ? trim($langId) : 'pl';
|
||||||
|
$attributeId = max(0, $attributeId);
|
||||||
|
|
||||||
|
if ($attributeId <= 0 || $normalizedName === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributeExists = (int) $this->db->count('pp_shop_attributes', ['id' => $attributeId]) > 0;
|
||||||
|
if (!$attributeExists) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingId = $this->findAttributeValueIdByName($attributeId, $normalizedName);
|
||||||
|
if ($existingId > 0) {
|
||||||
|
return ['id' => $existingId, 'created' => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->insert('pp_shop_attributes_values', [
|
||||||
|
'attribute_id' => $attributeId,
|
||||||
|
'impact_on_the_price' => null,
|
||||||
|
'is_default' => 0,
|
||||||
|
]);
|
||||||
|
$valueId = (int) $this->db->id();
|
||||||
|
if ($valueId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->insert('pp_shop_attributes_values_langs', [
|
||||||
|
'value_id' => $valueId,
|
||||||
|
'lang_id' => $normalizedLangId,
|
||||||
|
'name' => $normalizedName,
|
||||||
|
'value' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->clearTempAndCache();
|
||||||
|
$this->clearFrontCache($valueId, 'frontValueDetails');
|
||||||
|
|
||||||
|
return ['id' => $valueId, 'created' => true];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{sql: string, params: array<string, mixed>}
|
* @return array{sql: string, params: array<string, mixed>}
|
||||||
*/
|
*/
|
||||||
@@ -972,6 +1061,52 @@ class AttributeRepository
|
|||||||
return $this->defaultLangId;
|
return $this->defaultLangId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function findAttributeIdByNameAndType(string $name, int $type): int
|
||||||
|
{
|
||||||
|
$statement = $this->db->query(
|
||||||
|
'SELECT sa.id
|
||||||
|
FROM pp_shop_attributes sa
|
||||||
|
INNER JOIN pp_shop_attributes_langs sal ON sal.attribute_id = sa.id
|
||||||
|
WHERE sa.type = :type
|
||||||
|
AND LOWER(TRIM(sal.name)) = LOWER(TRIM(:name))
|
||||||
|
ORDER BY sa.id ASC
|
||||||
|
LIMIT 1',
|
||||||
|
[
|
||||||
|
':type' => $type,
|
||||||
|
':name' => $name,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (!$statement) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $statement->fetchColumn();
|
||||||
|
return $id === false ? 0 : (int) $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findAttributeValueIdByName(int $attributeId, string $name): int
|
||||||
|
{
|
||||||
|
$statement = $this->db->query(
|
||||||
|
'SELECT sav.id
|
||||||
|
FROM pp_shop_attributes_values sav
|
||||||
|
INNER JOIN pp_shop_attributes_values_langs savl ON savl.value_id = sav.id
|
||||||
|
WHERE sav.attribute_id = :attribute_id
|
||||||
|
AND LOWER(TRIM(savl.name)) = LOWER(TRIM(:name))
|
||||||
|
ORDER BY sav.id ASC
|
||||||
|
LIMIT 1',
|
||||||
|
[
|
||||||
|
':attribute_id' => $attributeId,
|
||||||
|
':name' => $name,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (!$statement) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $statement->fetchColumn();
|
||||||
|
return $id === false ? 0 : (int) $id;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Frontend methods ──────────────────────────────────────────
|
// ── Frontend methods ──────────────────────────────────────────
|
||||||
|
|
||||||
public function frontAttributeDetails(int $attributeId, string $langId): array
|
public function frontAttributeDetails(int $attributeId, string $langId): array
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ class CategoryRepository
|
|||||||
|
|
||||||
$deleted = (bool)$this->db->delete('pp_shop_categories', ['id' => $id]);
|
$deleted = (bool)$this->db->delete('pp_shop_categories', ['id' => $id]);
|
||||||
if ($deleted) {
|
if ($deleted) {
|
||||||
|
$this->db->delete('pp_routes', ['category_id' => $id]);
|
||||||
$this->refreshCategoryArtifacts();
|
$this->refreshCategoryArtifacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
140
autoload/Domain/CronJob/CronJobProcessor.php
Normal file
140
autoload/Domain/CronJob/CronJobProcessor.php
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domain\CronJob;
|
||||||
|
|
||||||
|
class CronJobProcessor
|
||||||
|
{
|
||||||
|
/** @var CronJobRepository */
|
||||||
|
private $cronRepo;
|
||||||
|
|
||||||
|
/** @var array<string, callable> */
|
||||||
|
private $handlers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param CronJobRepository $cronRepo
|
||||||
|
*/
|
||||||
|
public function __construct(CronJobRepository $cronRepo)
|
||||||
|
{
|
||||||
|
$this->cronRepo = $cronRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zarejestruj handler dla typu zadania
|
||||||
|
*
|
||||||
|
* @param string $jobType
|
||||||
|
* @param callable $handler fn($payload): bool|array — true/array = success, false/exception = fail
|
||||||
|
*/
|
||||||
|
public function registerHandler($jobType, callable $handler)
|
||||||
|
{
|
||||||
|
$this->handlers[$jobType] = $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utwórz zadania z harmonogramów, których next_run_at <= NOW
|
||||||
|
*
|
||||||
|
* @return int Liczba utworzonych zadań
|
||||||
|
*/
|
||||||
|
public function createScheduledJobs()
|
||||||
|
{
|
||||||
|
$schedules = $this->cronRepo->getDueSchedules();
|
||||||
|
$created = 0;
|
||||||
|
|
||||||
|
foreach ($schedules as $schedule) {
|
||||||
|
$jobType = $schedule['job_type'];
|
||||||
|
|
||||||
|
// Nie twórz duplikatów
|
||||||
|
if ($this->cronRepo->hasPendingJob($jobType)) {
|
||||||
|
// Mimo duplikatu, przesuń next_run_at żeby nie sprawdzać co sekundę
|
||||||
|
$this->cronRepo->touchSchedule($schedule['id'], (int) $schedule['interval_seconds']);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = null;
|
||||||
|
if (!empty($schedule['payload'])) {
|
||||||
|
$payload = json_decode($schedule['payload'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cronRepo->enqueue(
|
||||||
|
$jobType,
|
||||||
|
$payload,
|
||||||
|
(int) $schedule['priority'],
|
||||||
|
(int) $schedule['max_attempts']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->cronRepo->touchSchedule($schedule['id'], (int) $schedule['interval_seconds']);
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $created;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Przetwórz kolejkę zadań
|
||||||
|
*
|
||||||
|
* @param int $limit
|
||||||
|
* @return array Statystyki: ['processed' => int, 'succeeded' => int, 'failed' => int, 'skipped' => int]
|
||||||
|
*/
|
||||||
|
public function processQueue($limit = 10)
|
||||||
|
{
|
||||||
|
$stats = ['processed' => 0, 'succeeded' => 0, 'failed' => 0, 'skipped' => 0];
|
||||||
|
|
||||||
|
$jobs = $this->cronRepo->fetchNext($limit);
|
||||||
|
|
||||||
|
foreach ($jobs as $job) {
|
||||||
|
$jobType = $job['job_type'];
|
||||||
|
$jobId = (int) $job['id'];
|
||||||
|
$stats['processed']++;
|
||||||
|
|
||||||
|
if (!isset($this->handlers[$jobType])) {
|
||||||
|
$this->cronRepo->markFailed($jobId, 'No handler registered for job type: ' . $jobType, (int) $job['attempts']);
|
||||||
|
$stats['skipped']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = call_user_func($this->handlers[$jobType], $job['payload']);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
$this->cronRepo->markFailed($jobId, 'Handler returned false', (int) $job['attempts']);
|
||||||
|
$stats['failed']++;
|
||||||
|
} else {
|
||||||
|
$resultData = is_array($result) ? $result : null;
|
||||||
|
$this->cronRepo->markCompleted($jobId, $resultData);
|
||||||
|
$stats['succeeded']++;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->cronRepo->markFailed($jobId, $e->getMessage(), (int) $job['attempts']);
|
||||||
|
$stats['failed']++;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->cronRepo->markFailed($jobId, $e->getMessage(), (int) $job['attempts']);
|
||||||
|
$stats['failed']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Główna metoda: utwórz scheduled jobs + przetwórz kolejkę
|
||||||
|
*
|
||||||
|
* @param int $limit
|
||||||
|
* @return array ['scheduled' => int, 'processed' => int, 'succeeded' => int, 'failed' => int, 'skipped' => int]
|
||||||
|
*/
|
||||||
|
public function run($limit = 20)
|
||||||
|
{
|
||||||
|
// Odzyskaj stuck jobs
|
||||||
|
$this->cronRepo->recoverStuck(30);
|
||||||
|
|
||||||
|
// Utwórz zadania z harmonogramów
|
||||||
|
$scheduled = $this->createScheduledJobs();
|
||||||
|
|
||||||
|
// Przetwórz kolejkę
|
||||||
|
$stats = $this->processQueue($limit);
|
||||||
|
$stats['scheduled'] = $scheduled;
|
||||||
|
|
||||||
|
// Cleanup starych zadań (raz na uruchomienie)
|
||||||
|
$this->cronRepo->cleanup(30);
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
248
autoload/Domain/CronJob/CronJobRepository.php
Normal file
248
autoload/Domain/CronJob/CronJobRepository.php
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domain\CronJob;
|
||||||
|
|
||||||
|
class CronJobRepository
|
||||||
|
{
|
||||||
|
/** @var \medoo */
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \medoo $db
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dodaj zadanie do kolejki
|
||||||
|
*
|
||||||
|
* @param string $jobType
|
||||||
|
* @param array|null $payload
|
||||||
|
* @param int $priority
|
||||||
|
* @param int $maxAttempts
|
||||||
|
* @param string|null $scheduledAt
|
||||||
|
* @return int|null ID nowego zadania
|
||||||
|
*/
|
||||||
|
public function enqueue($jobType, $payload = null, $priority = CronJobType::PRIORITY_NORMAL, $maxAttempts = 10, $scheduledAt = null)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'job_type' => $jobType,
|
||||||
|
'status' => CronJobType::STATUS_PENDING,
|
||||||
|
'priority' => $priority,
|
||||||
|
'max_attempts' => $maxAttempts,
|
||||||
|
'scheduled_at' => $scheduledAt ? $scheduledAt : date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($payload !== null) {
|
||||||
|
$data['payload'] = json_encode($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->insert('pp_cron_jobs', $data);
|
||||||
|
$id = $this->db->id();
|
||||||
|
|
||||||
|
return $id ? (int) $id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomowe pobranie następnych zadań do przetworzenia.
|
||||||
|
*
|
||||||
|
* Uwaga: SELECT + UPDATE nie jest w pełni atomowe bez transakcji.
|
||||||
|
* Po UPDATE re-SELECT potwierdza, które joby zostały faktycznie przejęte
|
||||||
|
* (chroni przed race condition przy wielu workerach).
|
||||||
|
*
|
||||||
|
* @param int $limit
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function fetchNext($limit = 5)
|
||||||
|
{
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$jobs = $this->db->select('pp_cron_jobs', '*', [
|
||||||
|
'status' => CronJobType::STATUS_PENDING,
|
||||||
|
'scheduled_at[<=]' => $now,
|
||||||
|
'ORDER' => ['priority' => 'ASC', 'scheduled_at' => 'ASC'],
|
||||||
|
'LIMIT' => $limit,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($jobs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_column($jobs, 'id');
|
||||||
|
|
||||||
|
$this->db->update('pp_cron_jobs', [
|
||||||
|
'status' => CronJobType::STATUS_PROCESSING,
|
||||||
|
'started_at' => $now,
|
||||||
|
'attempts[+]' => 1,
|
||||||
|
], [
|
||||||
|
'id' => $ids,
|
||||||
|
'status' => CronJobType::STATUS_PENDING,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Re-SELECT: potwierdź, które joby zostały faktycznie przejęte
|
||||||
|
$claimed = $this->db->select('pp_cron_jobs', '*', [
|
||||||
|
'id' => $ids,
|
||||||
|
'status' => CronJobType::STATUS_PROCESSING,
|
||||||
|
'started_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($claimed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($claimed as &$job) {
|
||||||
|
if ($job['payload'] !== null) {
|
||||||
|
$job['payload'] = json_decode($job['payload'], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $claimed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oznacz zadanie jako zakończone
|
||||||
|
*
|
||||||
|
* @param int $jobId
|
||||||
|
* @param mixed $result
|
||||||
|
*/
|
||||||
|
public function markCompleted($jobId, $result = null)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'status' => CronJobType::STATUS_COMPLETED,
|
||||||
|
'completed_at' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($result !== null) {
|
||||||
|
$data['result'] = json_encode($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->update('pp_cron_jobs', $data, ['id' => $jobId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oznacz zadanie jako nieudane z backoffem
|
||||||
|
*
|
||||||
|
* @param int $jobId
|
||||||
|
* @param string $error
|
||||||
|
* @param int $attempt Numer próby (do obliczenia backoffu)
|
||||||
|
*/
|
||||||
|
public function markFailed($jobId, $error, $attempt = 1)
|
||||||
|
{
|
||||||
|
$job = $this->db->get('pp_cron_jobs', ['max_attempts', 'attempts'], ['id' => $jobId]);
|
||||||
|
|
||||||
|
$attempts = $job ? (int) $job['attempts'] : $attempt;
|
||||||
|
$maxAttempts = $job ? (int) $job['max_attempts'] : 10;
|
||||||
|
|
||||||
|
if ($attempts >= $maxAttempts) {
|
||||||
|
// Przekroczono limit prób — trwale failed
|
||||||
|
$this->db->update('pp_cron_jobs', [
|
||||||
|
'status' => CronJobType::STATUS_FAILED,
|
||||||
|
'last_error' => mb_substr($error, 0, 500),
|
||||||
|
'completed_at' => date('Y-m-d H:i:s'),
|
||||||
|
], ['id' => $jobId]);
|
||||||
|
} else {
|
||||||
|
// Wróć do pending z backoffem
|
||||||
|
$backoff = CronJobType::calculateBackoff($attempts);
|
||||||
|
$nextRun = date('Y-m-d H:i:s', time() + $backoff);
|
||||||
|
|
||||||
|
$this->db->update('pp_cron_jobs', [
|
||||||
|
'status' => CronJobType::STATUS_PENDING,
|
||||||
|
'last_error' => mb_substr($error, 0, 500),
|
||||||
|
'scheduled_at' => $nextRun,
|
||||||
|
], ['id' => $jobId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdź czy istnieje pending job danego typu z opcjonalnym payload match
|
||||||
|
*
|
||||||
|
* @param string $jobType
|
||||||
|
* @param array|null $payloadMatch
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasPendingJob($jobType, $payloadMatch = null)
|
||||||
|
{
|
||||||
|
$where = [
|
||||||
|
'job_type' => $jobType,
|
||||||
|
'status' => [CronJobType::STATUS_PENDING, CronJobType::STATUS_PROCESSING],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($payloadMatch !== null) {
|
||||||
|
$where['payload'] = json_encode($payloadMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = $this->db->count('pp_cron_jobs', $where);
|
||||||
|
return $count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wyczyść stare zakończone zadania
|
||||||
|
*
|
||||||
|
* @param int $olderThanDays
|
||||||
|
*/
|
||||||
|
public function cleanup($olderThanDays = 30)
|
||||||
|
{
|
||||||
|
$cutoff = date('Y-m-d H:i:s', time() - ($olderThanDays * 86400));
|
||||||
|
|
||||||
|
$this->db->delete('pp_cron_jobs', [
|
||||||
|
'status' => [CronJobType::STATUS_COMPLETED, CronJobType::STATUS_FAILED, CronJobType::STATUS_CANCELLED],
|
||||||
|
'updated_at[<]' => $cutoff,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Odzyskaj zablokowane zadania (stuck w processing)
|
||||||
|
*
|
||||||
|
* @param int $olderThanMinutes
|
||||||
|
*/
|
||||||
|
public function recoverStuck($olderThanMinutes = 30)
|
||||||
|
{
|
||||||
|
$cutoff = date('Y-m-d H:i:s', time() - ($olderThanMinutes * 60));
|
||||||
|
|
||||||
|
$this->db->update('pp_cron_jobs', [
|
||||||
|
'status' => CronJobType::STATUS_PENDING,
|
||||||
|
'started_at' => null,
|
||||||
|
], [
|
||||||
|
'status' => CronJobType::STATUS_PROCESSING,
|
||||||
|
'started_at[<]' => $cutoff,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pobierz harmonogramy gotowe do uruchomienia
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getDueSchedules()
|
||||||
|
{
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
return $this->db->select('pp_cron_schedules', '*', [
|
||||||
|
'enabled' => 1,
|
||||||
|
'OR' => [
|
||||||
|
'next_run_at' => null,
|
||||||
|
'next_run_at[<=]' => $now,
|
||||||
|
],
|
||||||
|
'ORDER' => ['priority' => 'ASC'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualizuj harmonogram po uruchomieniu
|
||||||
|
*
|
||||||
|
* @param int $scheduleId
|
||||||
|
* @param int $intervalSeconds
|
||||||
|
*/
|
||||||
|
public function touchSchedule($scheduleId, $intervalSeconds)
|
||||||
|
{
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$nextRun = date('Y-m-d H:i:s', time() + $intervalSeconds);
|
||||||
|
|
||||||
|
$this->db->update('pp_cron_schedules', [
|
||||||
|
'last_run_at' => $now,
|
||||||
|
'next_run_at' => $nextRun,
|
||||||
|
], ['id' => $scheduleId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
autoload/Domain/CronJob/CronJobType.php
Normal file
81
autoload/Domain/CronJob/CronJobType.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domain\CronJob;
|
||||||
|
|
||||||
|
class CronJobType
|
||||||
|
{
|
||||||
|
// Job types
|
||||||
|
const APILO_TOKEN_KEEPALIVE = 'apilo_token_keepalive';
|
||||||
|
const APILO_SEND_ORDER = 'apilo_send_order';
|
||||||
|
const APILO_SYNC_PAYMENT = 'apilo_sync_payment';
|
||||||
|
const APILO_SYNC_STATUS = 'apilo_sync_status';
|
||||||
|
const APILO_PRODUCT_SYNC = 'apilo_product_sync';
|
||||||
|
const APILO_PRICELIST_SYNC = 'apilo_pricelist_sync';
|
||||||
|
const APILO_STATUS_POLL = 'apilo_status_poll';
|
||||||
|
const PRICE_HISTORY = 'price_history';
|
||||||
|
const ORDER_ANALYSIS = 'order_analysis';
|
||||||
|
const TRUSTMATE_INVITATION = 'trustmate_invitation';
|
||||||
|
const GOOGLE_XML_FEED = 'google_xml_feed';
|
||||||
|
|
||||||
|
// Priorities (lower = more important)
|
||||||
|
const PRIORITY_CRITICAL = 10;
|
||||||
|
const PRIORITY_SEND_ORDER = 40; // apilo_send_order musi być PRZED sync payment/status
|
||||||
|
const PRIORITY_HIGH = 50;
|
||||||
|
const PRIORITY_NORMAL = 100;
|
||||||
|
const PRIORITY_LOW = 200;
|
||||||
|
|
||||||
|
// Statuses
|
||||||
|
const STATUS_PENDING = 'pending';
|
||||||
|
const STATUS_PROCESSING = 'processing';
|
||||||
|
const STATUS_COMPLETED = 'completed';
|
||||||
|
const STATUS_FAILED = 'failed';
|
||||||
|
const STATUS_CANCELLED = 'cancelled';
|
||||||
|
|
||||||
|
// Backoff
|
||||||
|
const BASE_BACKOFF_SECONDS = 60;
|
||||||
|
const MAX_BACKOFF_SECONDS = 3600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function allTypes()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::APILO_TOKEN_KEEPALIVE,
|
||||||
|
self::APILO_SEND_ORDER,
|
||||||
|
self::APILO_SYNC_PAYMENT,
|
||||||
|
self::APILO_SYNC_STATUS,
|
||||||
|
self::APILO_PRODUCT_SYNC,
|
||||||
|
self::APILO_PRICELIST_SYNC,
|
||||||
|
self::APILO_STATUS_POLL,
|
||||||
|
self::PRICE_HISTORY,
|
||||||
|
self::ORDER_ANALYSIS,
|
||||||
|
self::TRUSTMATE_INVITATION,
|
||||||
|
self::GOOGLE_XML_FEED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function allStatuses()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STATUS_PENDING,
|
||||||
|
self::STATUS_PROCESSING,
|
||||||
|
self::STATUS_COMPLETED,
|
||||||
|
self::STATUS_FAILED,
|
||||||
|
self::STATUS_CANCELLED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $attempt
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function calculateBackoff($attempt)
|
||||||
|
{
|
||||||
|
$backoff = self::BASE_BACKOFF_SECONDS * pow(2, $attempt - 1);
|
||||||
|
return min($backoff, self::MAX_BACKOFF_SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,63 @@ class IntegrationsRepository
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Logs ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pobiera logi z tabeli pp_log z paginacją, sortowaniem i filtrowaniem.
|
||||||
|
*
|
||||||
|
* @return array{items:array, total:int}
|
||||||
|
*/
|
||||||
|
public function getLogs( array $filters, string $sortColumn, string $sortDir, int $page, int $perPage ): array
|
||||||
|
{
|
||||||
|
$where = [];
|
||||||
|
|
||||||
|
if ( !empty( $filters['log_action'] ) ) {
|
||||||
|
$where['action[~]'] = '%' . $filters['log_action'] . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !empty( $filters['message'] ) ) {
|
||||||
|
$where['message[~]'] = '%' . $filters['message'] . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !empty( $filters['order_id'] ) ) {
|
||||||
|
$where['order_id'] = (int) $filters['order_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $this->db->count( 'pp_log', $where );
|
||||||
|
|
||||||
|
$where['ORDER'] = [ $sortColumn => $sortDir ];
|
||||||
|
$where['LIMIT'] = [ ( $page - 1 ) * $perPage, $perPage ];
|
||||||
|
|
||||||
|
$items = $this->db->select( 'pp_log', '*', $where );
|
||||||
|
if ( !is_array( $items ) ) {
|
||||||
|
$items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $items,
|
||||||
|
'total' => (int) $total,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usuwa wpis logu po ID.
|
||||||
|
*/
|
||||||
|
public function deleteLog( int $id ): bool
|
||||||
|
{
|
||||||
|
$this->db->delete( 'pp_log', [ 'id' => $id ] );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Czyści wszystkie logi z tabeli pp_log.
|
||||||
|
*/
|
||||||
|
public function clearLogs(): bool
|
||||||
|
{
|
||||||
|
$this->db->delete( 'pp_log', [] );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Product linking (Apilo) ─────────────────────────────────
|
// ── Product linking (Apilo) ─────────────────────────────────
|
||||||
|
|
||||||
public function linkProduct( int $productId, $externalId, $externalName ): bool
|
public function linkProduct( int $productId, $externalId, $externalName ): bool
|
||||||
@@ -611,15 +668,12 @@ class IntegrationsRepository
|
|||||||
public function shopproImportProduct( int $productId ): array
|
public function shopproImportProduct( int $productId ): array
|
||||||
{
|
{
|
||||||
$settings = $this->getSettings( 'shoppro' );
|
$settings = $this->getSettings( 'shoppro' );
|
||||||
|
$missingSetting = $this->missingShopproSetting( $settings, [ 'domain', 'db_name', 'db_host', 'db_user' ] );
|
||||||
|
if ( $missingSetting !== null ) {
|
||||||
|
return [ 'success' => false, 'message' => 'Brakuje konfiguracji shopPRO: ' . $missingSetting . '.' ];
|
||||||
|
}
|
||||||
|
|
||||||
$mdb2 = new \medoo( [
|
$mdb2 = $this->shopproDb( $settings );
|
||||||
'database_type' => 'mysql',
|
|
||||||
'database_name' => $settings['db_name'],
|
|
||||||
'server' => $settings['db_host'],
|
|
||||||
'username' => $settings['db_user'],
|
|
||||||
'password' => $settings['db_password'],
|
|
||||||
'charset' => 'utf8'
|
|
||||||
] );
|
|
||||||
|
|
||||||
$product = $mdb2->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
|
$product = $mdb2->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
|
||||||
if ( !$product )
|
if ( !$product )
|
||||||
@@ -643,6 +697,7 @@ class IntegrationsRepository
|
|||||||
'additional_message_text' => $product['additional_message_text'],
|
'additional_message_text' => $product['additional_message_text'],
|
||||||
'additional_message_required'=> $product['additional_message_required'],
|
'additional_message_required'=> $product['additional_message_required'],
|
||||||
'weight' => $product['weight'],
|
'weight' => $product['weight'],
|
||||||
|
'producer_id' => $product['producer_id'] ?? null,
|
||||||
] );
|
] );
|
||||||
|
|
||||||
$newProductId = $this->db->id();
|
$newProductId = $this->db->id();
|
||||||
@@ -672,41 +727,149 @@ class IntegrationsRepository
|
|||||||
'warehouse_message_nonzero'=> $lang['warehouse_message_nonzero'],
|
'warehouse_message_nonzero'=> $lang['warehouse_message_nonzero'],
|
||||||
'canonical' => $lang['canonical'],
|
'canonical' => $lang['canonical'],
|
||||||
'xml_name' => $lang['xml_name'],
|
'xml_name' => $lang['xml_name'],
|
||||||
|
'security_information' => $lang['security_information'] ?? null,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import custom fields
|
||||||
|
$customFields = $mdb2->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );
|
||||||
|
if ( is_array( $customFields ) ) {
|
||||||
|
foreach ( $customFields as $field ) {
|
||||||
|
$this->db->insert( 'pp_shop_products_custom_fields', [
|
||||||
|
'id_product' => $newProductId,
|
||||||
|
'name' => (string)($field['name'] ?? ''),
|
||||||
|
'type' => (string)($field['type'] ?? 'text'),
|
||||||
|
'is_required' => !empty( $field['is_required'] ) ? 1 : 0,
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import images
|
// Import images
|
||||||
$images = $mdb2->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId ] );
|
$images = $mdb2->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId ] );
|
||||||
|
$importLog = [];
|
||||||
|
$domainRaw = preg_replace( '#^https?://#', '', (string)($settings['domain'] ?? '') );
|
||||||
if ( is_array( $images ) ) {
|
if ( is_array( $images ) ) {
|
||||||
foreach ( $images as $image ) {
|
foreach ( $images as $image ) {
|
||||||
$imageUrl = 'https://' . $settings['domain'] . $image['src'];
|
$srcPath = (string)($image['src'] ?? '');
|
||||||
|
$imageUrl = 'https://' . rtrim( $domainRaw, '/' ) . '/' . ltrim( $srcPath, '/' );
|
||||||
|
$imageName = basename( $srcPath );
|
||||||
|
|
||||||
|
if ( $imageName === '' ) {
|
||||||
|
$importLog[] = '[SKIP] Pusta nazwa pliku dla src: ' . $srcPath;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$ch = curl_init( $imageUrl );
|
$ch = curl_init( $imageUrl );
|
||||||
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
|
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
|
||||||
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
|
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
|
||||||
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
|
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
|
||||||
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false );
|
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false );
|
||||||
|
curl_setopt( $ch, CURLOPT_TIMEOUT, 30 );
|
||||||
|
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
|
||||||
$imageData = curl_exec( $ch );
|
$imageData = curl_exec( $ch );
|
||||||
|
$httpCode = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||||
|
$curlErrno = curl_errno( $ch );
|
||||||
|
$curlError = curl_error( $ch );
|
||||||
curl_close( $ch );
|
curl_close( $ch );
|
||||||
|
|
||||||
$imageName = basename( $imageUrl );
|
if ( $curlErrno !== 0 || $imageData === false ) {
|
||||||
$imageDir = '../upload/product_images/product_' . $newProductId;
|
$importLog[] = '[ERROR] cURL: ' . $imageUrl . ' — błąd ' . $curlErrno . ': ' . $curlError;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $httpCode !== 200 ) {
|
||||||
|
$importLog[] = '[ERROR] HTTP ' . $httpCode . ': ' . $imageUrl;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageDir = dirname( __DIR__, 3 ) . '/upload/product_images/product_' . $newProductId;
|
||||||
$imagePath = $imageDir . '/' . $imageName;
|
$imagePath = $imageDir . '/' . $imageName;
|
||||||
|
|
||||||
if ( !file_exists( $imageDir ) )
|
if ( !file_exists( $imageDir ) && !mkdir( $imageDir, 0777, true ) && !file_exists( $imageDir ) ) {
|
||||||
mkdir( $imageDir, 0777, true );
|
$importLog[] = '[ERROR] Nie można utworzyć katalogu: ' . $imageDir;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
file_put_contents( $imagePath, $imageData );
|
$written = file_put_contents( $imagePath, $imageData );
|
||||||
|
if ( $written === false ) {
|
||||||
|
$importLog[] = '[ERROR] Zapis pliku nieudany: ' . $imagePath;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$this->db->insert( 'pp_shop_products_images', [
|
$this->db->insert( 'pp_shop_products_images', [
|
||||||
'product_id' => $newProductId,
|
'product_id' => $newProductId,
|
||||||
'src' => '/upload/product_images/product_' . $newProductId . '/' . $imageName,
|
'src' => '/upload/product_images/product_' . $newProductId . '/' . $imageName,
|
||||||
|
'alt' => $image['alt'] ?? '',
|
||||||
'o' => $image['o'],
|
'o' => $image['o'],
|
||||||
] );
|
] );
|
||||||
|
$importLog[] = '[OK] ' . $imageUrl . ' → ' . $imagePath . ' (' . $written . ' B)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ 'success' => true, 'message' => 'Produkt został zaimportowany.' ];
|
// Zapisz log importu zdjęć (ścieżka absolutna — niezależna od cwd)
|
||||||
|
$logDir = dirname( __DIR__, 3 ) . '/logs';
|
||||||
|
$logFile = $logDir . '/shoppro-import-debug.log';
|
||||||
|
$mkdirOk = file_exists( $logDir ) || mkdir( $logDir, 0755, true ) || file_exists( $logDir );
|
||||||
|
$logEntry = '[' . date( 'Y-m-d H:i:s' ) . '] Import produktu #' . $productId . ' → #' . $newProductId . "\n"
|
||||||
|
. ' Domain: ' . $domainRaw . "\n"
|
||||||
|
. ' Obrazy źródłowe: ' . count( $images ?: [] ) . "\n";
|
||||||
|
foreach ( $importLog as $line ) {
|
||||||
|
$logEntry .= ' ' . $line . "\n";
|
||||||
}
|
}
|
||||||
|
// Zawsze loguj do error_log (niezależnie od uprawnień do pliku)
|
||||||
|
error_log( '[shopPRO shoppro-import] ' . str_replace( "\n", ' | ', $logEntry ) );
|
||||||
|
|
||||||
|
if ( $mkdirOk && file_put_contents( $logFile, $logEntry, FILE_APPEND ) === false ) {
|
||||||
|
error_log( '[shopPRO shoppro-import] WARN: nie można zapisać logu do: ' . $logFile );
|
||||||
|
} elseif ( !$mkdirOk ) {
|
||||||
|
error_log( '[shopPRO shoppro-import] WARN: nie można utworzyć katalogu: ' . $logDir );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zbuduj czytelny komunikat z wynikiem importu zdjęć
|
||||||
|
$imgCount = count( $images ?: [] );
|
||||||
|
if ( $imgCount === 0 ) {
|
||||||
|
$imgSummary = 'Zdjęcia: brak w bazie źródłowej.';
|
||||||
|
} else {
|
||||||
|
$ok = 0;
|
||||||
|
$errors = [];
|
||||||
|
foreach ( $importLog as $line ) {
|
||||||
|
if ( strncmp( $line, '[OK]', 4 ) === 0 ) {
|
||||||
|
$ok++;
|
||||||
|
} else {
|
||||||
|
$errors[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$imgSummary = 'Zdjęcia: ' . $ok . '/' . $imgCount . ' zaimportowanych.';
|
||||||
|
if ( !empty( $errors ) ) {
|
||||||
|
$imgSummary .= ' Błędy: ' . implode( '; ', $errors );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ 'success' => true, 'message' => 'Produkt został zaimportowany. ' . $imgSummary ];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function missingShopproSetting( array $settings, array $requiredKeys ): ?string
|
||||||
|
{
|
||||||
|
foreach ( $requiredKeys as $requiredKey ) {
|
||||||
|
if ( trim( (string)($settings[$requiredKey] ?? '') ) === '' ) {
|
||||||
|
return $requiredKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shopproDb( array $settings ): \medoo
|
||||||
|
{
|
||||||
|
return new \medoo( [
|
||||||
|
'database_type' => 'mysql',
|
||||||
|
'database_name' => $settings['db_name'],
|
||||||
|
'server' => $settings['db_host'],
|
||||||
|
'username' => $settings['db_user'],
|
||||||
|
'password' => $settings['db_password'] ?? '',
|
||||||
|
'charset' => 'utf8'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ class LayoutsRepository
|
|||||||
if (is_array($layoutRows) && isset($layoutRows[0])) {
|
if (is_array($layoutRows) && isset($layoutRows[0])) {
|
||||||
$layout = $layoutRows[0];
|
$layout = $layoutRows[0];
|
||||||
} else {
|
} else {
|
||||||
$layout = $this->db->get('pp_layouts', '*', ['categories_default' => 1]);
|
$layout = $this->db->get('pp_layouts', '*', ['status' => 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,21 @@ class OrderAdminService
|
|||||||
private $productRepo;
|
private $productRepo;
|
||||||
private $settingsRepo;
|
private $settingsRepo;
|
||||||
private $transportRepo;
|
private $transportRepo;
|
||||||
|
/** @var \Domain\CronJob\CronJobRepository|null */
|
||||||
|
private $cronJobRepo;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
OrderRepository $orders,
|
OrderRepository $orders,
|
||||||
$productRepo = null,
|
$productRepo = null,
|
||||||
$settingsRepo = null,
|
$settingsRepo = null,
|
||||||
$transportRepo = null
|
$transportRepo = null,
|
||||||
|
$cronJobRepo = null
|
||||||
) {
|
) {
|
||||||
$this->orders = $orders;
|
$this->orders = $orders;
|
||||||
$this->productRepo = $productRepo;
|
$this->productRepo = $productRepo;
|
||||||
$this->settingsRepo = $settingsRepo;
|
$this->settingsRepo = $settingsRepo;
|
||||||
$this->transportRepo = $transportRepo;
|
$this->transportRepo = $transportRepo;
|
||||||
|
$this->cronJobRepo = $cronJobRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function details(int $orderId): array
|
public function details(int $orderId): array
|
||||||
@@ -393,17 +397,38 @@ class OrderAdminService
|
|||||||
global $mdb;
|
global $mdb;
|
||||||
|
|
||||||
if ($orderId <= 0) {
|
if ($orderId <= 0) {
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$mdb,
|
||||||
|
'resend_order',
|
||||||
|
$orderId,
|
||||||
|
'Nieprawidlowe ID zamowienia (orderId <= 0)',
|
||||||
|
['order_id' => $orderId]
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$order = $this->orders->findForAdmin($orderId);
|
$order = $this->orders->findForAdmin($orderId);
|
||||||
if (empty($order) || empty($order['apilo_order_id'])) {
|
if (empty($order) || empty($order['apilo_order_id'])) {
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$mdb,
|
||||||
|
'resend_order',
|
||||||
|
$orderId,
|
||||||
|
'Brak zamowienia lub brak apilo_order_id',
|
||||||
|
['order_found' => !empty($order), 'apilo_order_id' => $order['apilo_order_id'] ?? null]
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
|
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
|
||||||
$accessToken = $integrationsRepository -> apiloGetAccessToken();
|
$accessToken = $integrationsRepository -> apiloGetAccessToken();
|
||||||
if (!$accessToken) {
|
if (!$accessToken) {
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$mdb,
|
||||||
|
'resend_order',
|
||||||
|
$orderId,
|
||||||
|
'Nie udalo sie uzyskac tokenu Apilo (access token)',
|
||||||
|
['apilo_order_id' => $order['apilo_order_id']]
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,75 +523,6 @@ class OrderAdminService
|
|||||||
return $this->orders->deleteOrder($orderId);
|
return $this->orders->deleteOrder($orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Apilo sync queue (migrated from \shop\Order)
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
private const APILO_SYNC_QUEUE_FILE = '/temp/apilo-sync-queue.json';
|
|
||||||
|
|
||||||
public function processApiloSyncQueue(int $limit = 10): int
|
|
||||||
{
|
|
||||||
$queue = self::loadApiloSyncQueue();
|
|
||||||
if (!\Shared\Helpers\Helpers::is_array_fix($queue)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$processed = 0;
|
|
||||||
|
|
||||||
foreach ($queue as $key => $task)
|
|
||||||
{
|
|
||||||
if ($processed >= $limit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$order_id = (int)($task['order_id'] ?? 0);
|
|
||||||
if ($order_id <= 0) {
|
|
||||||
unset($queue[$key]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$order = $this->orders->findRawById($order_id);
|
|
||||||
if (!$order) {
|
|
||||||
unset($queue[$key]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$error = '';
|
|
||||||
$sync_failed = false;
|
|
||||||
|
|
||||||
$payment_pending = !empty($task['payment']) && (int)$order['paid'] === 1;
|
|
||||||
if ($payment_pending && (int)$order['apilo_order_id']) {
|
|
||||||
if (!$this->syncApiloPayment($order)) {
|
|
||||||
$sync_failed = true;
|
|
||||||
$error = 'payment_sync_failed';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$status_pending = isset($task['status']) && $task['status'] !== null && $task['status'] !== '';
|
|
||||||
if (!$sync_failed && $status_pending && (int)$order['apilo_order_id']) {
|
|
||||||
if (!$this->syncApiloStatus($order, (int)$task['status'])) {
|
|
||||||
$sync_failed = true;
|
|
||||||
$error = 'status_sync_failed';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sync_failed) {
|
|
||||||
$task['attempts'] = (int)($task['attempts'] ?? 0) + 1;
|
|
||||||
$task['last_error'] = $error;
|
|
||||||
$task['updated_at'] = date('Y-m-d H:i:s');
|
|
||||||
$queue[$key] = $task;
|
|
||||||
} else {
|
|
||||||
unset($queue[$key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$processed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
self::saveApiloSyncQueue($queue);
|
|
||||||
|
|
||||||
return $processed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Private: email
|
// Private: email
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -624,6 +580,17 @@ class OrderAdminService
|
|||||||
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||||
|
|
||||||
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
|
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$db,
|
||||||
|
'payment_sync',
|
||||||
|
(int)$order['id'],
|
||||||
|
'Pominięto sync płatności — Apilo wyłączone lub brak tokenu/sync_orders',
|
||||||
|
[
|
||||||
|
'enabled' => $apilo_settings['enabled'] ?? false,
|
||||||
|
'has_token' => !empty($apilo_settings['access-token']),
|
||||||
|
'sync_orders' => $apilo_settings['sync_orders'] ?? false,
|
||||||
|
]
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,8 +598,25 @@ class OrderAdminService
|
|||||||
self::appendApiloLog("SET AS PAID\n" . print_r($order, true));
|
self::appendApiloLog("SET AS PAID\n" . print_r($order, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($order['apilo_order_id'] && !$this->syncApiloPayment($order)) {
|
if (!$order['apilo_order_id']) {
|
||||||
self::queueApiloSync((int)$order['id'], true, null, 'payment_sync_failed');
|
// Zamówienie jeszcze nie wysłane do Apilo — kolejkuj sync płatności na później
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$db,
|
||||||
|
'payment_sync',
|
||||||
|
(int)$order['id'],
|
||||||
|
'Brak apilo_order_id — płatność zakolejkowana do sync',
|
||||||
|
['apilo_order_id' => $order['apilo_order_id'] ?? null]
|
||||||
|
);
|
||||||
|
$this->queueApiloSync((int)$order['id'], true, null, 'awaiting_apilo_order');
|
||||||
|
} elseif (!$this->syncApiloPayment($order)) {
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$db,
|
||||||
|
'payment_sync',
|
||||||
|
(int)$order['id'],
|
||||||
|
'Sync płatności nieudany — zakolejkowano ponowną próbę',
|
||||||
|
['apilo_order_id' => $order['apilo_order_id']]
|
||||||
|
);
|
||||||
|
$this->queueApiloSync((int)$order['id'], true, null, 'payment_sync_failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,6 +629,18 @@ class OrderAdminService
|
|||||||
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||||
|
|
||||||
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
|
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$db,
|
||||||
|
'status_sync',
|
||||||
|
(int)$order['id'],
|
||||||
|
'Pominięto sync statusu — Apilo wyłączone lub brak tokenu/sync_orders',
|
||||||
|
[
|
||||||
|
'target_status' => $status,
|
||||||
|
'enabled' => $apilo_settings['enabled'] ?? false,
|
||||||
|
'has_token' => !empty($apilo_settings['access-token']),
|
||||||
|
'sync_orders' => $apilo_settings['sync_orders'] ?? false,
|
||||||
|
]
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,19 +648,36 @@ class OrderAdminService
|
|||||||
self::appendApiloLog("UPDATE STATUS\n" . print_r($order, true));
|
self::appendApiloLog("UPDATE STATUS\n" . print_r($order, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($order['apilo_order_id'] && !$this->syncApiloStatus($order, $status)) {
|
if (!$order['apilo_order_id']) {
|
||||||
self::queueApiloSync((int)$order['id'], false, $status, 'status_sync_failed');
|
// Zamówienie jeszcze nie wysłane do Apilo — kolejkuj sync statusu na później
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$db,
|
||||||
|
'status_sync',
|
||||||
|
(int)$order['id'],
|
||||||
|
'Brak apilo_order_id — status zakolejkowany do sync',
|
||||||
|
['apilo_order_id' => $order['apilo_order_id'] ?? null, 'target_status' => $status]
|
||||||
|
);
|
||||||
|
$this->queueApiloSync((int)$order['id'], false, $status, 'awaiting_apilo_order');
|
||||||
|
} elseif (!$this->syncApiloStatus($order, $status)) {
|
||||||
|
\Domain\Integrations\ApiloLogger::log(
|
||||||
|
$db,
|
||||||
|
'status_sync',
|
||||||
|
(int)$order['id'],
|
||||||
|
'Sync statusu nieudany — zakolejkowano ponowną próbę',
|
||||||
|
['apilo_order_id' => $order['apilo_order_id'], 'target_status' => $status]
|
||||||
|
);
|
||||||
|
$this->queueApiloSync((int)$order['id'], false, $status, 'status_sync_failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncApiloPayment(array $order): bool
|
public function syncApiloPayment(array $order): bool
|
||||||
{
|
{
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
$db = $this->orders->getDb();
|
$db = $this->orders->getDb();
|
||||||
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
|
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
|
||||||
|
|
||||||
if (!(int)$order['apilo_order_id']) {
|
if (empty($order['apilo_order_id'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,14 +737,14 @@ class OrderAdminService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncApiloStatus(array $order, int $status): bool
|
public function syncApiloStatus(array $order, int $status): bool
|
||||||
{
|
{
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
$db = $this->orders->getDb();
|
$db = $this->orders->getDb();
|
||||||
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
|
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
|
||||||
|
|
||||||
if (!(int)$order['apilo_order_id']) {
|
if (empty($order['apilo_order_id'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,59 +800,42 @@ class OrderAdminService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Private: Apilo sync queue file helpers
|
// Private: Apilo sync queue (DB-based via CronJobRepository)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
private static function queueApiloSync(int $order_id, bool $payment, ?int $status, string $error): void
|
private function queueApiloSync(int $order_id, bool $payment, ?int $status, string $error): void
|
||||||
{
|
{
|
||||||
if ($order_id <= 0) return;
|
if ($order_id <= 0) return;
|
||||||
|
|
||||||
$queue = self::loadApiloSyncQueue();
|
if ($this->cronJobRepo === null) return;
|
||||||
$key = (string)$order_id;
|
|
||||||
$row = is_array($queue[$key] ?? null) ? $queue[$key] : [];
|
if ($payment) {
|
||||||
|
$jobType = \Domain\CronJob\CronJobType::APILO_SYNC_PAYMENT;
|
||||||
|
$payload = ['order_id' => $order_id];
|
||||||
|
|
||||||
|
if (!$this->cronJobRepo->hasPendingJob($jobType, $payload)) {
|
||||||
|
$this->cronJobRepo->enqueue(
|
||||||
|
$jobType,
|
||||||
|
$payload,
|
||||||
|
\Domain\CronJob\CronJobType::PRIORITY_HIGH,
|
||||||
|
50
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$row['order_id'] = $order_id;
|
|
||||||
$row['payment'] = !empty($row['payment']) || $payment ? 1 : 0;
|
|
||||||
if ($status !== null) {
|
if ($status !== null) {
|
||||||
$row['status'] = $status;
|
$jobType = \Domain\CronJob\CronJobType::APILO_SYNC_STATUS;
|
||||||
|
$payload = ['order_id' => $order_id, 'status' => $status];
|
||||||
|
|
||||||
|
if (!$this->cronJobRepo->hasPendingJob($jobType, $payload)) {
|
||||||
|
$this->cronJobRepo->enqueue(
|
||||||
|
$jobType,
|
||||||
|
$payload,
|
||||||
|
\Domain\CronJob\CronJobType::PRIORITY_HIGH,
|
||||||
|
50
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$row['attempts'] = (int)($row['attempts'] ?? 0) + 1;
|
|
||||||
$row['last_error'] = $error;
|
|
||||||
$row['updated_at'] = date('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
$queue[$key] = $row;
|
|
||||||
self::saveApiloSyncQueue($queue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function apiloSyncQueuePath(): string
|
|
||||||
{
|
|
||||||
return dirname(__DIR__, 2) . self::APILO_SYNC_QUEUE_FILE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function loadApiloSyncQueue(): array
|
|
||||||
{
|
|
||||||
$path = self::apiloSyncQueuePath();
|
|
||||||
if (!file_exists($path)) return [];
|
|
||||||
|
|
||||||
$content = file_get_contents($path);
|
|
||||||
if (!$content) return [];
|
|
||||||
|
|
||||||
$decoded = json_decode($content, true);
|
|
||||||
if (!is_array($decoded)) return [];
|
|
||||||
|
|
||||||
return $decoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function saveApiloSyncQueue(array $queue): void
|
|
||||||
{
|
|
||||||
$path = self::apiloSyncQueuePath();
|
|
||||||
$dir = dirname($path);
|
|
||||||
if (!is_dir($dir)) {
|
|
||||||
mkdir($dir, 0777, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
file_put_contents($path, json_encode($queue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOCK_EX);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function appendApiloLog(string $message): void
|
private static function appendApiloLog(string $message): void
|
||||||
|
|||||||
@@ -134,7 +134,11 @@ class PagesRepository
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (bool)$this->db->delete('pp_pages', ['id' => $pageId]);
|
$deleted = (bool)$this->db->delete('pp_pages', ['id' => $pageId]);
|
||||||
|
if ($deleted) {
|
||||||
|
$this->db->delete('pp_routes', ['page_id' => $pageId]);
|
||||||
|
}
|
||||||
|
return $deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -357,4 +357,34 @@ class ProducerRepository
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Znajdź producenta po nazwie lub utwórz nowego (dla API).
|
||||||
|
*
|
||||||
|
* @return array{id: int, created: bool}
|
||||||
|
*/
|
||||||
|
public function ensureProducerForApi(string $name): array
|
||||||
|
{
|
||||||
|
$name = trim($name);
|
||||||
|
if ($name === '') {
|
||||||
|
return ['id' => 0, 'created' => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->db->get('pp_shop_producer', 'id', ['name' => $name]);
|
||||||
|
if (!empty($existing)) {
|
||||||
|
return ['id' => (int)$existing, 'created' => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->insert('pp_shop_producer', [
|
||||||
|
'name' => $name,
|
||||||
|
'status' => 1,
|
||||||
|
'img' => null,
|
||||||
|
]);
|
||||||
|
$id = (int)$this->db->id();
|
||||||
|
if ($id <= 0) {
|
||||||
|
return ['id' => 0, 'created' => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['id' => $id, 'created' => true];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -657,6 +657,7 @@ class ProductRepository
|
|||||||
'set_id' => $product['set_id'] !== null ? (int)$product['set_id'] : null,
|
'set_id' => $product['set_id'] !== null ? (int)$product['set_id'] : null,
|
||||||
'product_unit_id' => $product['product_unit_id'] !== null ? (int)$product['product_unit_id'] : null,
|
'product_unit_id' => $product['product_unit_id'] !== null ? (int)$product['product_unit_id'] : null,
|
||||||
'producer_id' => $product['producer_id'] !== null ? (int)$product['producer_id'] : null,
|
'producer_id' => $product['producer_id'] !== null ? (int)$product['producer_id'] : null,
|
||||||
|
'producer_name' => $this->resolveProducerName($product['producer_id']),
|
||||||
'date_add' => $product['date_add'],
|
'date_add' => $product['date_add'],
|
||||||
'date_modify' => $product['date_modify'],
|
'date_modify' => $product['date_modify'],
|
||||||
];
|
];
|
||||||
@@ -682,6 +683,7 @@ class ProductRepository
|
|||||||
'tab_name_2' => $lang['tab_name_2'],
|
'tab_name_2' => $lang['tab_name_2'],
|
||||||
'tab_description_2' => $lang['tab_description_2'],
|
'tab_description_2' => $lang['tab_description_2'],
|
||||||
'canonical' => $lang['canonical'],
|
'canonical' => $lang['canonical'],
|
||||||
|
'security_information' => $lang['security_information'] ?? null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,6 +735,19 @@ class ProductRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom fields (Dodatkowe pola)
|
||||||
|
$customFields = $this->db->select('pp_shop_products_custom_fields', ['name', 'type', 'is_required'], ['id_product' => $id]);
|
||||||
|
$result['custom_fields'] = [];
|
||||||
|
if (is_array($customFields)) {
|
||||||
|
foreach ($customFields as $cf) {
|
||||||
|
$result['custom_fields'][] = [
|
||||||
|
'name' => $cf['name'],
|
||||||
|
'type' => !empty($cf['type']) ? $cf['type'] : 'text',
|
||||||
|
'is_required' => $cf['is_required'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Variants (only for parent products)
|
// Variants (only for parent products)
|
||||||
if (empty($product['parent_id'])) {
|
if (empty($product['parent_id'])) {
|
||||||
$result['variants'] = $this->findVariantsForApi($id);
|
$result['variants'] = $this->findVariantsForApi($id);
|
||||||
@@ -1116,6 +1131,21 @@ class ProductRepository
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca nazwę producenta po ID (null jeśli brak).
|
||||||
|
*
|
||||||
|
* @param mixed $producerId
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private function resolveProducerName($producerId): ?string
|
||||||
|
{
|
||||||
|
if (empty($producerId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$name = $this->db->get('pp_shop_producer', 'name', ['id' => (int)$producerId]);
|
||||||
|
return ($name !== false && $name !== null) ? (string)$name : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Szczegóły produktu (admin) — zastępuje factory product_details().
|
* Szczegóły produktu (admin) — zastępuje factory product_details().
|
||||||
*/
|
*/
|
||||||
@@ -1239,7 +1269,7 @@ class ProductRepository
|
|||||||
|
|
||||||
$productData = [
|
$productData = [
|
||||||
'date_modify' => date( 'Y-m-d H:i:s' ),
|
'date_modify' => date( 'Y-m-d H:i:s' ),
|
||||||
'modify_by' => $userId,
|
'modify_by' => $userId !== null ? (int) $userId : 0,
|
||||||
'status' => ( $d['status'] ?? '' ) === 'on' ? 1 : 0,
|
'status' => ( $d['status'] ?? '' ) === 'on' ? 1 : 0,
|
||||||
'price_netto' => $this->nullIfEmpty( $d['price_netto'] ?? null ),
|
'price_netto' => $this->nullIfEmpty( $d['price_netto'] ?? null ),
|
||||||
'price_brutto' => $this->nullIfEmpty( $d['price_brutto'] ?? null ),
|
'price_brutto' => $this->nullIfEmpty( $d['price_brutto'] ?? null ),
|
||||||
@@ -1301,7 +1331,10 @@ class ProductRepository
|
|||||||
$this->saveImagesOrder( $productId, $d['gallery_order'] );
|
$this->saveImagesOrder( $productId, $d['gallery_order'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zapisz custom fields tylko gdy jawnie podane (partial update przez API może nie zawierać tego klucza)
|
||||||
|
if ( array_key_exists( 'custom_field_name', $d ) ) {
|
||||||
$this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] );
|
$this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] );
|
||||||
|
}
|
||||||
|
|
||||||
if ( !$isNew ) {
|
if ( !$isNew ) {
|
||||||
$this->cleanupDeletedFiles( $productId );
|
$this->cleanupDeletedFiles( $productId );
|
||||||
@@ -1615,6 +1648,7 @@ class ProductRepository
|
|||||||
$this->db->delete( 'pp_shop_products_langs', [ 'product_id' => $productId ] );
|
$this->db->delete( 'pp_shop_products_langs', [ 'product_id' => $productId ] );
|
||||||
$this->db->delete( 'pp_shop_products_images', [ 'product_id' => $productId ] );
|
$this->db->delete( 'pp_shop_products_images', [ 'product_id' => $productId ] );
|
||||||
$this->db->delete( 'pp_shop_products_files', [ 'product_id' => $productId ] );
|
$this->db->delete( 'pp_shop_products_files', [ 'product_id' => $productId ] );
|
||||||
|
$this->db->delete( 'pp_shop_products_custom_fields', [ 'id_product' => $productId ] );
|
||||||
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $productId ] );
|
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $productId ] );
|
||||||
$this->db->delete( 'pp_shop_products', [ 'id' => $productId ] );
|
$this->db->delete( 'pp_shop_products', [ 'id' => $productId ] );
|
||||||
$this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] );
|
$this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] );
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class SettingsRepository
|
|||||||
'infinitescroll' => $this->isEnabled($values['infinitescroll'] ?? null) ? 1 : 0,
|
'infinitescroll' => $this->isEnabled($values['infinitescroll'] ?? null) ? 1 : 0,
|
||||||
'own_gtm_js' => $values['own_gtm_js'] ?? '',
|
'own_gtm_js' => $values['own_gtm_js'] ?? '',
|
||||||
'own_gtm_html' => $values['own_gtm_html'] ?? '',
|
'own_gtm_html' => $values['own_gtm_html'] ?? '',
|
||||||
|
'api_key' => $values['api_key'] ?? '',
|
||||||
];
|
];
|
||||||
|
|
||||||
$warehouseMessageZero = $values['warehouse_message_zero'] ?? [];
|
$warehouseMessageZero = $values['warehouse_message_zero'] ?? [];
|
||||||
|
|||||||
@@ -172,13 +172,19 @@ class UpdateRepository
|
|||||||
|
|
||||||
foreach ( $manifest['sql'] as $query ) {
|
foreach ( $manifest['sql'] as $query ) {
|
||||||
$query = trim( $query );
|
$query = trim( $query );
|
||||||
if ( $query !== '' ) {
|
if ( $query === '' || strpos( $query, '--' ) === 0 ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
if ( $this->db->query( $query ) ) {
|
if ( $this->db->query( $query ) ) {
|
||||||
$success++;
|
$success++;
|
||||||
} else {
|
} else {
|
||||||
$errors++;
|
$errors++;
|
||||||
$log[] = '[WARNING] Błąd SQL: ' . $query;
|
$log[] = '[WARNING] Błąd SQL: ' . $query;
|
||||||
}
|
}
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$errors++;
|
||||||
|
$log[] = '[WARNING] Wyjątek SQL: ' . $e->getMessage() . ' | Query: ' . substr( $query, 0, 200 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -425,42 +425,206 @@ class Helpers
|
|||||||
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
||||||
$site_map .= '</url>' . PHP_EOL;
|
$site_map .= '</url>' . PHP_EOL;
|
||||||
|
|
||||||
$htaccess_data = file_get_contents( $dir . 'libraries/htaccess.conf' );
|
//
|
||||||
$htaccess_data = str_replace( '{PAGE}', $url, $htaccess_data );
|
// SYSTEM ROUTES — delete all and reinsert
|
||||||
|
//
|
||||||
|
$mdb->delete( 'pp_routes', [ 'type' => 'system' ] );
|
||||||
|
|
||||||
$results = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
// Static system routes (hardcoded, never change)
|
||||||
|
$systemRoutes = [
|
||||||
|
// Wyszukiwarka
|
||||||
|
[ 'pattern' => '^wyszukiwarka/([^/]+)/([0-9]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=$2' ],
|
||||||
|
[ 'pattern' => '^wyszukiwarka/([^/]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=1' ],
|
||||||
|
// Zamowienia
|
||||||
|
[ 'pattern' => '^zamowienie/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=order_details&order_hash=$1' ],
|
||||||
|
[ 'pattern' => '^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=payment_confirmation&order_hash=$1' ],
|
||||||
|
// Platnosci
|
||||||
|
[ 'pattern' => '^tpay-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_tpay' ],
|
||||||
|
[ 'pattern' => '^platnosc-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_hotpay' ],
|
||||||
|
[ 'pattern' => '^przelewy24-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_przelewy24pl' ],
|
||||||
|
// Koszyk
|
||||||
|
[ 'pattern' => '^koszyk$', 'destination' => 'index.php?module=shop_basket&action=main_view' ],
|
||||||
|
[ 'pattern' => '^koszyk-podsumowanie$', 'destination' => 'index.php?module=shop_basket&action=summary_view' ],
|
||||||
|
[ 'pattern' => '^zloz-zamowienie$', 'destination' => 'index.php?module=shop_basket&action=basket_save' ],
|
||||||
|
// Klient
|
||||||
|
[ 'pattern' => '^rejestracja$', 'destination' => 'index.php?module=shop_client&action=register_form' ],
|
||||||
|
[ 'pattern' => '^logowanie$', 'destination' => 'index.php?module=shop_client&action=login_form' ],
|
||||||
|
[ 'pattern' => '^wylogowanie$', 'destination' => 'index.php?module=shop_client&action=logout' ],
|
||||||
|
[ 'pattern' => '^odzyskiwanie-hasla$', 'destination' => 'index.php?module=shop_client&action=recover_password' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/zamowienia$', 'destination' => 'index.php?module=shop_client&action=client_orders' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/adresy$', 'destination' => 'index.php?module=shop_client&action=client_addresses' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/nowy-adres$', 'destination' => 'index.php?module=shop_client&action=address_edit' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/edytuj-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_edit&id=$1' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/usun-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_delete&id=$1' ],
|
||||||
|
// Newsletter
|
||||||
|
[ 'pattern' => '^newsletter/signin$', 'destination' => 'index.php?module=newsletter&action=signin' ],
|
||||||
|
[ 'pattern' => '^newsletter/confirm/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=confirm&hash=$1' ],
|
||||||
|
[ 'pattern' => '^newsletter/unsubscribe/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=unsubscribe&hash=$1' ],
|
||||||
|
// Moduły AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search)
|
||||||
|
[ 'pattern' => '^shopBasket/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopBasket&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^shopBasket/([^/]+)$', 'destination' => 'index.php?module=shopBasket&action=$1' ],
|
||||||
|
[ 'pattern' => '^shopClient/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopClient&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^shopClient/([^/]+)$', 'destination' => 'index.php?module=shopClient&action=$1' ],
|
||||||
|
[ 'pattern' => '^shopProduct/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopProduct&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^shopProduct/([^/]+)$', 'destination' => 'index.php?module=shopProduct&action=$1' ],
|
||||||
|
[ 'pattern' => '^shopCoupon/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopCoupon&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^shopCoupon/([^/]+)$', 'destination' => 'index.php?module=shopCoupon&action=$1' ],
|
||||||
|
[ 'pattern' => '^search/([^/]+)/(.+)$', 'destination' => 'index.php?module=search&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^search/([^/]+)$', 'destination' => 'index.php?module=search&action=$1' ],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ( $systemRoutes as $route )
|
||||||
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => $route['pattern'],
|
||||||
|
'destination' => $route['destination'],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic system routes — languages
|
||||||
|
$results = $mdb->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||||
{
|
{
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $row['id'] . '/$ index.php?a=change_language&id=' . $row['id'] . ' [L]';
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => '^' . $row['id'] . '$',
|
||||||
|
'destination' => 'index.php?a=change_language&id=' . $row['id'],
|
||||||
|
] );
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
// Dynamic system routes — producenci
|
||||||
// INNE
|
|
||||||
//
|
|
||||||
$htaccess_data .= PHP_EOL;
|
|
||||||
$htaccess_data .= 'RewriteRule ^newsletter/signin/$ index.php?module=newsletter&action=signin [L]' . PHP_EOL;
|
|
||||||
$htaccess_data .= 'RewriteRule ^newsletter/confirm/hash=(.*)$ index.php?module=newsletter&action=confirm&hash=$1 [L]' . PHP_EOL;
|
|
||||||
$htaccess_data .= 'RewriteRule ^newsletter/unsubscribe/hash=(.*)$ index.php?module=newsletter&action=unsubscribe&hash=$1 [L]' . PHP_EOL;
|
|
||||||
|
|
||||||
//
|
|
||||||
// PRODUCENCI
|
|
||||||
//
|
|
||||||
$categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId();
|
$categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId();
|
||||||
$htaccess_data .= 'RewriteRule ^producenci$ index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId . '&%{QUERY_STRING} [L]' . PHP_EOL;
|
|
||||||
|
|
||||||
$rows = $mdb -> select( 'pp_shop_producer', '*', [ 'status' => 1 ] );
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => '^producenci$',
|
||||||
|
'destination' => 'index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$rows = $mdb->select( 'pp_shop_producer', '*', [ 'status' => 1 ] );
|
||||||
if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row )
|
if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row )
|
||||||
{
|
{
|
||||||
$htaccess_data .= 'RewriteRule ^producent/' . self::seo( $row['name'] ) . '$ index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&%{QUERY_STRING} [L]' . PHP_EOL;
|
$mdb->insert( 'pp_routes', [
|
||||||
$htaccess_data .= 'RewriteRule ^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$ index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1&%{QUERY_STRING} [L]' . PHP_EOL;
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => '^producent/' . self::seo( $row['name'] ) . '$',
|
||||||
|
'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId,
|
||||||
|
] );
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => '^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$',
|
||||||
|
'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1',
|
||||||
|
] );
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = $mdb -> select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
//
|
||||||
|
// HTACCESS — generuj z PHP (bez szablonu htaccess.conf)
|
||||||
|
//
|
||||||
|
$htaccess_data = 'RewriteEngine On' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteBase /' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'Options +FollowSymlinks' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'Options -Indexes' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= '# Przekierowanie z www na bez www i z http na https w jednym kroku' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= '# Przekierowanie z http na https, jesli nie zawiera www' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{HTTPS} off' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= '# Usuwanie koncowego slasha dla niekatalogów' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} (.+)/$' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ %1 [R=301,L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^admin/$ admin/index.php [L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ /%1 [R=301,L]' . PHP_EOL;
|
||||||
|
|
||||||
|
/* cache block */
|
||||||
|
if ( $settings['htaccess_cache'] )
|
||||||
|
{
|
||||||
|
$htaccess_data .= '<IfModule mod_deflate.c>' . PHP_EOL
|
||||||
|
. 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL
|
||||||
|
. '</IfModule>' . PHP_EOL
|
||||||
|
. '<IfModule mod_headers.c>' . PHP_EOL
|
||||||
|
. 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL
|
||||||
|
. '</IfModule>' . PHP_EOL
|
||||||
|
. '<IfModule mod_expires.c>' . PHP_EOL
|
||||||
|
. 'ExpiresActive on' . PHP_EOL
|
||||||
|
. 'ExpiresDefault "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL
|
||||||
|
. '</IfModule>' . PHP_EOL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$htaccess_data .= '<IfModule mod_headers.c>' . PHP_EOL
|
||||||
|
. 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL
|
||||||
|
. 'Header set Pragma "no-cache"' . PHP_EOL
|
||||||
|
. 'Header set Expires 0' . PHP_EOL
|
||||||
|
. '</IfModule>' . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$htaccess_data .= '<Files *.conf>' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Deny from all' . PHP_EOL;
|
||||||
|
$htaccess_data .= '</Files>' . PHP_EOL;
|
||||||
|
$htaccess_data .= '<Files *.log>' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Deny from all' . PHP_EOL;
|
||||||
|
$htaccess_data .= '</Files>' . PHP_EOL;
|
||||||
|
$htaccess_data .= '<Files *.ini>' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Deny from all' . PHP_EOL;
|
||||||
|
$htaccess_data .= '</Files>' . PHP_EOL;
|
||||||
|
|
||||||
|
//
|
||||||
|
// KATEGORIE — sitemap + pp_routes (bez zmian)
|
||||||
|
//
|
||||||
|
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||||
{
|
{
|
||||||
!$row['start'] ? $language_link = $row['id'] . '/' : $language_link = '';
|
!$row['start'] ? $language_link = $row['id'] . '/' : $language_link = '';
|
||||||
|
|
||||||
$results2 = $mdb -> select( 'pp_shop_categories_langs', [ '[><]pp_shop_categories' => [ 'category_id' => 'id' ] ], [ 'seo_link', 'title', 'category_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'o' => 'ASC' ] ] );
|
$results2 = $mdb->select( 'pp_shop_categories_langs', [ '[><]pp_shop_categories' => [ 'category_id' => 'id' ] ], [ 'seo_link', 'title', 'category_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
if ( is_array( $results2 ) ) foreach ( $results2 as $row2 )
|
if ( is_array( $results2 ) ) foreach ( $results2 as $row2 )
|
||||||
{
|
{
|
||||||
if ( $row2['title'] )
|
if ( $row2['title'] )
|
||||||
@@ -475,35 +639,42 @@ class Helpers
|
|||||||
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
||||||
$site_map .= '</url>' . PHP_EOL;
|
$site_map .= '</url>' . PHP_EOL;
|
||||||
|
|
||||||
if ( $row2['seo_link'] )
|
$seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] );
|
||||||
{
|
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]';
|
$mdb->delete( 'pp_routes', [ 'AND' => [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'] ] ] );
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9]+)$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]';
|
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/1$ ' . $language_link . self::seo( $row2['seo_link'] ) . ' [R=301,L]';
|
$mdb->insert( 'pp_routes', [
|
||||||
}
|
'category_id' => $row2['category_id'],
|
||||||
else
|
'lang_id' => $row['id'],
|
||||||
{
|
'pattern' => '^' . $language_link . $seoSlug . '$',
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]';
|
'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'],
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '/([0-9]+)$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]';
|
] );
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '/1$ ' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . ' [R=301,L]';
|
$mdb->insert( 'pp_routes', [
|
||||||
}
|
'category_id' => $row2['category_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $language_link . $seoSlug . '/([0-9]+)$',
|
||||||
|
'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1',
|
||||||
|
] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = $mdb -> select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
//
|
||||||
|
// PRODUKTY — sitemap + pp_routes (bez zmian)
|
||||||
|
//
|
||||||
|
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
if ( is_array( $results ) )
|
if ( is_array( $results ) )
|
||||||
{
|
{
|
||||||
foreach ( $results as $row )
|
foreach ( $results as $row )
|
||||||
{
|
{
|
||||||
!$row['start'] ? $language_link = $row['id'] . '/' : $language_link = '';
|
!$row['start'] ? $language_link = $row['id'] . '/' : $language_link = '';
|
||||||
|
|
||||||
$results2 = $mdb -> select( 'pp_shop_products_langs', [ '[><]pp_shop_products' => [ 'product_id' => 'id' ] ], [ 'seo_link', 'name', 'product_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'name' => 'ASC' ] ] );
|
$results2 = $mdb->select( 'pp_shop_products_langs', [ '[><]pp_shop_products' => [ 'product_id' => 'id' ] ], [ 'seo_link', 'name', 'product_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||||
if ( is_array( $results2 ) )
|
if ( is_array( $results2 ) )
|
||||||
{
|
{
|
||||||
foreach ( $results2 as $row2 )
|
foreach ( $results2 as $row2 )
|
||||||
{
|
{
|
||||||
$mdb -> delete( 'pp_routes', [ 'AND' => [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'] ] ] );
|
$mdb->delete( 'pp_routes', [ 'AND' => [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'] ] ] );
|
||||||
|
|
||||||
if ( $row2['name'] )
|
if ( $row2['name'] )
|
||||||
{
|
{
|
||||||
@@ -519,27 +690,13 @@ class Helpers
|
|||||||
|
|
||||||
if ( $row2['seo_link'] )
|
if ( $row2['seo_link'] )
|
||||||
{
|
{
|
||||||
$pattern = '^' . $language_link . self::seo( $row2['seo_link'] ) . '$';
|
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
|
||||||
$destination = 'index.php?product=' . $row2['product_id'];
|
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
|
||||||
|
|
||||||
$mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] );
|
|
||||||
|
|
||||||
$pattern = '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$';
|
|
||||||
$destination = 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1';
|
|
||||||
|
|
||||||
$mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] );
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$pattern = '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$';
|
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
|
||||||
$destination = 'index.php?product=' . $row2['product_id'];
|
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
|
||||||
|
|
||||||
$mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] );
|
|
||||||
|
|
||||||
$pattern = '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$';
|
|
||||||
$destination = 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1';
|
|
||||||
|
|
||||||
$mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,13 +704,16 @@ class Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = $mdb -> select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
//
|
||||||
|
// STRONY + ARTYKULY — sitemap + pp_routes (bez zmian)
|
||||||
|
//
|
||||||
|
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
if ( is_array( $results ) )
|
if ( is_array( $results ) )
|
||||||
foreach ( $results as $row )
|
foreach ( $results as $row )
|
||||||
{
|
{
|
||||||
( !$row['start'] and count( $results ) > 1 ) ? $language_link = $row['id'] . '/' : $language_link = '';
|
( !$row['start'] and count( $results ) > 1 ) ? $language_link = $row['id'] . '/' : $language_link = '';
|
||||||
|
|
||||||
$results2 = $mdb -> select( 'pp_pages_langs', [ '[><]pp_pages' => [ 'page_id' => 'id' ] ], [ 'seo_link', 'title', 'page_id', 'noindex', 'start', 'link', 'page_type' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'start' => 'DESC', 'o' => 'ASC' ] ] );
|
$results2 = $mdb->select( 'pp_pages_langs', [ '[><]pp_pages' => [ 'page_id' => 'id' ] ], [ 'seo_link', 'title', 'page_id', 'noindex', 'start', 'link', 'page_type' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'start' => 'DESC', 'o' => 'ASC' ] ] );
|
||||||
if ( is_array( $results2 ) )
|
if ( is_array( $results2 ) )
|
||||||
foreach ( $results2 as $row2 )
|
foreach ( $results2 as $row2 )
|
||||||
{
|
{
|
||||||
@@ -590,44 +750,39 @@ class Helpers
|
|||||||
{
|
{
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$';
|
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$';
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
|
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
|
||||||
|
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$';
|
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$';
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
|
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
|
||||||
}
|
}
|
||||||
|
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]';
|
$htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $row2['seo_link'] )
|
$seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] );
|
||||||
{
|
$langPrefix = $row2['start'] ? '' : $language_link;
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]';
|
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9]+)$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]';
|
$mdb->delete( 'pp_routes', [ 'AND' => [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'] ] ] );
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/1$ ' . $language_link . self::seo( $row2['seo_link'] ) . ' [R=301,L]';
|
|
||||||
}
|
$mdb->insert( 'pp_routes', [
|
||||||
else
|
'page_id' => $row2['page_id'],
|
||||||
{
|
'lang_id' => $row['id'],
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]';
|
'pattern' => '^' . $langPrefix . $seoSlug . '$',
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '/([0-9]+)$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]';
|
'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'],
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '/1$ ' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . ' [R=301,L]';
|
] );
|
||||||
}
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'page_id' => $row2['page_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $langPrefix . $seoSlug . '/([0-9]+)$',
|
||||||
|
'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1',
|
||||||
|
] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$results2 = $mdb -> select( 'pp_articles_langs', [ '[><]pp_articles' => [ 'article_id' => 'id' ] ], [ 'seo_link', 'title', 'article_id', 'noindex', 'copy_from' ], [ 'AND' => [ 'status' => 1, 'lang_id' => $row['id'], 'block_direct_access' => 0 ] ] );
|
$results2 = $mdb->select( 'pp_articles_langs', [ '[><]pp_articles' => [ 'article_id' => 'id' ] ], [ 'seo_link', 'title', 'article_id', 'noindex', 'copy_from' ], [ 'AND' => [ 'status' => 1, 'lang_id' => $row['id'], 'block_direct_access' => 0 ] ] );
|
||||||
if ( is_array( $results2 ) )
|
if ( is_array( $results2 ) )
|
||||||
foreach ( $results2 as $row2 )
|
foreach ( $results2 as $row2 )
|
||||||
{
|
{
|
||||||
if ( $row2['copy_from'] != null )
|
if ( $row2['copy_from'] != null )
|
||||||
{
|
{
|
||||||
$results_tmp = $mdb -> get( 'pp_articles_langs', [
|
$results_tmp = $mdb->get( 'pp_articles_langs', [ 'seo_link', 'title' ], [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row2['copy_from'] ] ] );
|
||||||
'seo_link',
|
|
||||||
'title'
|
|
||||||
], [
|
|
||||||
'AND' => [
|
|
||||||
'article_id' => $row2['article_id'],
|
|
||||||
'lang_id' => $row2['copy_from']
|
|
||||||
]
|
|
||||||
] );
|
|
||||||
$row2['seo_link'] = $results_tmp['seo_link'];
|
$row2['seo_link'] = $results_tmp['seo_link'];
|
||||||
$row2['title'] = $results_tmp['title'];
|
$row2['title'] = $results_tmp['title'];
|
||||||
}
|
}
|
||||||
@@ -650,81 +805,52 @@ class Helpers
|
|||||||
$robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL;
|
$robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$mdb->delete( 'pp_routes', [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'] ] ] );
|
||||||
|
|
||||||
if ( $row2['seo_link'] )
|
if ( $row2['seo_link'] )
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]';
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'article_id' => $row2['article_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$',
|
||||||
|
'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'],
|
||||||
|
] );
|
||||||
|
}
|
||||||
else if ( $row2['title'] != null )
|
else if ( $row2['title'] != null )
|
||||||
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]';
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'article_id' => $row2['article_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$',
|
||||||
|
'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'],
|
||||||
|
] );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = $mdb -> get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] );
|
// Invalidacja cache tras
|
||||||
|
try {
|
||||||
|
( new \Shared\Cache\CacheHandler() )->delete( 'pp_routes:all' );
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
// Redis niedostepny — ignorujemy
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] );
|
||||||
if ( $results )
|
if ( $results )
|
||||||
$htaccess_data .= PHP_EOL . $results;
|
$htaccess_data .= PHP_EOL . $results;
|
||||||
|
|
||||||
$results = $mdb -> get( 'pp_settings', 'value', [ 'param' => 'robots' ] );
|
$results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'robots' ] );
|
||||||
if ( $results )
|
if ( $results )
|
||||||
$robots .= PHP_EOL . $results;
|
$robots .= PHP_EOL . $results;
|
||||||
|
|
||||||
$site_map .= '</urlset>';
|
$site_map .= '</urlset>';
|
||||||
|
|
||||||
/* cache */
|
|
||||||
if ( $settings['htaccess_cache'] )
|
|
||||||
{
|
|
||||||
$htaccess_data = str_replace( '{HTACCESS_CACHE}',
|
|
||||||
'<IfModule mod_deflate.c>' . PHP_EOL
|
|
||||||
. 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL
|
|
||||||
. '</IfModule>' . PHP_EOL
|
|
||||||
. '<IfModule mod_headers.c>' . PHP_EOL
|
|
||||||
. 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL
|
|
||||||
. '</IfModule>' . PHP_EOL
|
|
||||||
. '<IfModule mod_expires.c>' . PHP_EOL
|
|
||||||
. 'ExpiresActive on' . PHP_EOL
|
|
||||||
. 'ExpiresDefault "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL
|
|
||||||
. 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL
|
|
||||||
. 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL
|
|
||||||
. 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL
|
|
||||||
. 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL
|
|
||||||
. 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL
|
|
||||||
. 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL
|
|
||||||
. '</IfModule>'
|
|
||||||
, $htaccess_data );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$htaccess_data = str_replace( '{HTACCESS_CACHE}',
|
|
||||||
'<IfModule mod_headers.c>' . PHP_EOL
|
|
||||||
. 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL
|
|
||||||
. 'Header set Pragma "no-cache"' . PHP_EOL
|
|
||||||
. 'Header set Expires 0' . PHP_EOL
|
|
||||||
. '</IfModule>',
|
|
||||||
$htaccess_data );
|
|
||||||
}
|
|
||||||
|
|
||||||
$htaccess_data .= PHP_EOL;
|
$htaccess_data .= PHP_EOL;
|
||||||
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL;
|
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL;
|
||||||
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL;
|
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL;
|
||||||
$htaccess_data .= 'RewriteRule ^ index.php [L]';
|
$htaccess_data .= 'RewriteRule ^ index.php [L]';
|
||||||
|
|
||||||
// Niektore hostingi blokuja zmiane wersji PHP przez .htaccess.
|
// Niektore hostingi blokuja zmiane wersji PHP przez .htaccess.
|
||||||
// Automatycznie komentujemy niedozwolone dyrektywy, aby generowany plik byl kompatybilny.
|
|
||||||
$htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data );
|
$htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data );
|
||||||
|
|
||||||
$fp = fopen( $dir . '.htaccess', 'w' );
|
$fp = fopen( $dir . '.htaccess', 'w' );
|
||||||
|
|||||||
@@ -423,7 +423,8 @@ class App
|
|||||||
new \Domain\Order\OrderRepository( $mdb ),
|
new \Domain\Order\OrderRepository( $mdb ),
|
||||||
$productRepo,
|
$productRepo,
|
||||||
new \Domain\Settings\SettingsRepository( $mdb ),
|
new \Domain\Settings\SettingsRepository( $mdb ),
|
||||||
new \Domain\Transport\TransportRepository( $mdb )
|
new \Domain\Transport\TransportRepository( $mdb ),
|
||||||
|
new \Domain\CronJob\CronJobRepository( $mdb )
|
||||||
),
|
),
|
||||||
$productRepo
|
$productRepo
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
namespace admin\Controllers;
|
namespace admin\Controllers;
|
||||||
|
|
||||||
use Domain\Integrations\IntegrationsRepository;
|
use Domain\Integrations\IntegrationsRepository;
|
||||||
|
use admin\ViewModels\Common\PaginatedTableViewModel;
|
||||||
|
|
||||||
class IntegrationsController
|
class IntegrationsController
|
||||||
{
|
{
|
||||||
@@ -12,6 +13,114 @@ class IntegrationsController
|
|||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function logs(): string
|
||||||
|
{
|
||||||
|
$sortableColumns = ['id', 'action', 'order_id', 'message', 'date'];
|
||||||
|
|
||||||
|
$filterDefinitions = [
|
||||||
|
[
|
||||||
|
'key' => 'log_action',
|
||||||
|
'label' => 'Akcja',
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'message',
|
||||||
|
'label' => 'Wiadomosc',
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'order_id',
|
||||||
|
'label' => 'ID zamowienia',
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$listRequest = \admin\Support\TableListRequestFactory::fromRequest(
|
||||||
|
$filterDefinitions,
|
||||||
|
$sortableColumns,
|
||||||
|
'id'
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->repository->getLogs(
|
||||||
|
$listRequest['filters'],
|
||||||
|
$listRequest['sortColumn'],
|
||||||
|
$listRequest['sortDir'],
|
||||||
|
$listRequest['page'],
|
||||||
|
$listRequest['perPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1;
|
||||||
|
|
||||||
|
foreach ( $result['items'] as $item ) {
|
||||||
|
$id = (int)($item['id'] ?? 0);
|
||||||
|
$context = trim( (string)($item['context'] ?? '') );
|
||||||
|
$contextHtml = '';
|
||||||
|
if ( $context !== '' ) {
|
||||||
|
$contextHtml = '<button class="btn btn-xs btn-default log-context-btn" data-id="' . $id . '">Pokaz</button>'
|
||||||
|
. '<pre class="log-context-pre" id="log-context-' . $id . '" style="display:none;max-height:300px;overflow:auto;margin-top:5px;font-size:11px;white-space:pre-wrap;">'
|
||||||
|
. htmlspecialchars( $context, ENT_QUOTES, 'UTF-8' )
|
||||||
|
. '</pre>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'lp' => $lp++ . '.',
|
||||||
|
'action' => htmlspecialchars( (string)($item['action'] ?? ''), ENT_QUOTES, 'UTF-8' ),
|
||||||
|
'order_id' => $item['order_id'] ? (int)$item['order_id'] : '-',
|
||||||
|
'message' => htmlspecialchars( (string)($item['message'] ?? ''), ENT_QUOTES, 'UTF-8' ),
|
||||||
|
'context' => $contextHtml,
|
||||||
|
'date' => !empty( $item['date'] ) ? date( 'Y-m-d H:i:s', strtotime( (string)$item['date'] ) ) : '-',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = (int)$result['total'];
|
||||||
|
$totalPages = max( 1, (int)ceil( $total / $listRequest['perPage'] ) );
|
||||||
|
|
||||||
|
$viewModel = new PaginatedTableViewModel(
|
||||||
|
[
|
||||||
|
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
||||||
|
['key' => 'date', 'sort_key' => 'date', 'label' => 'Data', 'class' => 'text-center', 'sortable' => true],
|
||||||
|
['key' => 'action', 'sort_key' => 'action', 'label' => 'Akcja', 'sortable' => true],
|
||||||
|
['key' => 'order_id', 'sort_key' => 'order_id', 'label' => 'Zamowienie', 'class' => 'text-center', 'sortable' => true],
|
||||||
|
['key' => 'message', 'sort_key' => 'message', 'label' => 'Wiadomosc', 'sortable' => true],
|
||||||
|
['key' => 'context', 'label' => 'Kontekst', 'sortable' => false, 'raw' => true],
|
||||||
|
],
|
||||||
|
$rows,
|
||||||
|
$listRequest['viewFilters'],
|
||||||
|
[
|
||||||
|
'column' => $listRequest['sortColumn'],
|
||||||
|
'dir' => $listRequest['sortDir'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'page' => $listRequest['page'],
|
||||||
|
'per_page' => $listRequest['perPage'],
|
||||||
|
'total' => $total,
|
||||||
|
'total_pages' => $totalPages,
|
||||||
|
],
|
||||||
|
array_merge( $listRequest['queryFilters'], [
|
||||||
|
'sort' => $listRequest['sortColumn'],
|
||||||
|
'dir' => $listRequest['sortDir'],
|
||||||
|
'per_page' => $listRequest['perPage'],
|
||||||
|
] ),
|
||||||
|
$listRequest['perPageOptions'],
|
||||||
|
$sortableColumns,
|
||||||
|
'/admin/integrations/logs/',
|
||||||
|
'Brak wpisow w logach.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return \Shared\Tpl\Tpl::view( 'integrations/logs', [
|
||||||
|
'viewModel' => $viewModel,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logs_clear(): void
|
||||||
|
{
|
||||||
|
$this->repository->clearLogs();
|
||||||
|
\Shared\Helpers\Helpers::alert( 'Logi zostaly wyczyszczone.' );
|
||||||
|
header( 'Location: /admin/integrations/logs/' );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
public function apilo_settings(): string
|
public function apilo_settings(): string
|
||||||
{
|
{
|
||||||
return \Shared\Tpl\Tpl::view( 'integrations/apilo-settings', [
|
return \Shared\Tpl\Tpl::view( 'integrations/apilo-settings', [
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class ProductArchiveController
|
|||||||
. $skuEanHtml;
|
. $skuEanHtml;
|
||||||
|
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
|
'_checkbox' => '<input type="checkbox" class="js-bulk-check" value="' . $id . '" aria-label="Zaznacz produkt">',
|
||||||
'lp' => $lp++ . '.',
|
'lp' => $lp++ . '.',
|
||||||
'product' => $productCell,
|
'product' => $productCell,
|
||||||
'price_brutto' => $priceBrutto !== '' ? $priceBrutto : '-',
|
'price_brutto' => $priceBrutto !== '' ? $priceBrutto : '-',
|
||||||
@@ -106,6 +107,14 @@ class ProductArchiveController
|
|||||||
'confirm_ok' => 'Przywroc',
|
'confirm_ok' => 'Przywroc',
|
||||||
'confirm_cancel' => 'Anuluj',
|
'confirm_cancel' => 'Anuluj',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Usun trwale',
|
||||||
|
'url' => '/admin/product_archive/delete_permanent/product_id=' . $id,
|
||||||
|
'class' => 'btn btn-xs btn-danger',
|
||||||
|
'confirm' => 'UWAGA! Operacja nieodwracalna!' . "\n\n" . 'Produkt "' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '" zostanie trwale usuniety razem ze wszystkimi zdjeciami i zalacznikami z serwera.' . "\n\n" . 'Czy na pewno chcesz usunac ten produkt?',
|
||||||
|
'confirm_ok' => 'Tak, usun trwale',
|
||||||
|
'confirm_cancel' => 'Anuluj',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -115,6 +124,7 @@ class ProductArchiveController
|
|||||||
|
|
||||||
$viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel(
|
$viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel(
|
||||||
[
|
[
|
||||||
|
['key' => '_checkbox', 'label' => '', 'class' => 'text-center table-col-bulk-check', 'sortable' => false, 'raw' => true],
|
||||||
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
||||||
['key' => 'product', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true],
|
['key' => 'product', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true],
|
||||||
['key' => 'price_brutto', 'sort_key' => 'price_brutto', 'label' => 'Cena', 'class' => 'text-center', 'sortable' => true],
|
['key' => 'price_brutto', 'sort_key' => 'price_brutto', 'label' => 'Cena', 'class' => 'text-center', 'sortable' => true],
|
||||||
@@ -162,4 +172,60 @@ class ProductArchiveController
|
|||||||
header( 'Location: /admin/product_archive/list/' );
|
header( 'Location: /admin/product_archive/list/' );
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete_permanent(): void
|
||||||
|
{
|
||||||
|
$productId = (int) \Shared\Helpers\Helpers::get( 'product_id' );
|
||||||
|
|
||||||
|
if ( $productId <= 0 ) {
|
||||||
|
\Shared\Helpers\Helpers::alert( 'Nieprawidłowe ID produktu.' );
|
||||||
|
header( 'Location: /admin/product_archive/list/' );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $this->productRepository->delete( $productId ) ) {
|
||||||
|
\Shared\Helpers\Helpers::set_message( 'Produkt został trwale usunięty wraz ze zdjęciami i załącznikami.' );
|
||||||
|
} else {
|
||||||
|
\Shared\Helpers\Helpers::alert( 'Podczas usuwania produktu wystąpił błąd. Proszę spróbować ponownie.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
header( 'Location: /admin/product_archive/list/' );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bulk_delete_permanent(): void
|
||||||
|
{
|
||||||
|
header( 'Content-Type: application/json; charset=utf-8' );
|
||||||
|
|
||||||
|
$rawIds = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? $_POST['ids'] : [];
|
||||||
|
$ids = [];
|
||||||
|
foreach ( $rawIds as $raw ) {
|
||||||
|
$id = (int) $raw;
|
||||||
|
if ( $id > 0 ) {
|
||||||
|
$ids[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $ids ) ) {
|
||||||
|
echo json_encode( ['success' => false, 'message' => 'Nie wybrano żadnych produktów.'] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = 0;
|
||||||
|
$errors = [];
|
||||||
|
foreach ( $ids as $id ) {
|
||||||
|
if ( $this->productRepository->delete( $id ) ) {
|
||||||
|
$deleted++;
|
||||||
|
} else {
|
||||||
|
$errors[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode( [
|
||||||
|
'success' => empty( $errors ),
|
||||||
|
'deleted' => $deleted,
|
||||||
|
'errors' => $errors,
|
||||||
|
] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,26 +73,45 @@ class SettingsController
|
|||||||
*/
|
*/
|
||||||
public function globalSearchAjax(): void
|
public function globalSearchAjax(): void
|
||||||
{
|
{
|
||||||
global $mdb;
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-store');
|
||||||
|
|
||||||
$phrase = trim((string)\Shared\Helpers\Helpers::get('q'));
|
try {
|
||||||
if ($phrase === '' || mb_strlen($phrase) < 2) {
|
$this->executeGlobalSearch();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'status' => 'ok',
|
'status' => 'error',
|
||||||
'items' => [],
|
'items' => [],
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function executeGlobalSearch(): void
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
$phrase = isset($_REQUEST['q']) ? trim((string)$_REQUEST['q']) : '';
|
||||||
|
if ($phrase === '' || mb_strlen($phrase) < 2) {
|
||||||
|
echo json_encode(['status' => 'ok', 'items' => []]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$phrase = mb_substr($phrase, 0, 120);
|
$phrase = mb_substr($phrase, 0, 120);
|
||||||
$phraseNormalized = preg_replace('/\s+/', ' ', $phrase);
|
$phraseNormalized = trim((string)preg_replace('/\s+/', ' ', $phrase));
|
||||||
$phraseNormalized = trim((string)$phraseNormalized);
|
|
||||||
$like = '%' . $phrase . '%';
|
$like = '%' . $phrase . '%';
|
||||||
$likeNormalized = '%' . $phraseNormalized . '%';
|
$likeNormalized = '%' . $phraseNormalized . '%';
|
||||||
|
|
||||||
$items = [];
|
$items = [];
|
||||||
$defaultLang = (string)$this->languagesRepository->defaultLanguage();
|
|
||||||
|
|
||||||
|
$defaultLang = '1';
|
||||||
|
try {
|
||||||
|
$defaultLang = (string)$this->languagesRepository->defaultLanguage();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// fallback to '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Produkty ---
|
||||||
try {
|
try {
|
||||||
$productStmt = $mdb->query(
|
$productStmt = $mdb->query(
|
||||||
'SELECT '
|
'SELECT '
|
||||||
@@ -115,7 +134,10 @@ class SettingsController
|
|||||||
$productStmt = false;
|
$productStmt = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$productRows = $productStmt ? $productStmt->fetchAll() : [];
|
$productRows = ($productStmt && method_exists($productStmt, 'fetchAll'))
|
||||||
|
? $productStmt->fetchAll(\PDO::FETCH_ASSOC)
|
||||||
|
: [];
|
||||||
|
|
||||||
if (is_array($productRows)) {
|
if (is_array($productRows)) {
|
||||||
foreach ($productRows as $row) {
|
foreach ($productRows as $row) {
|
||||||
$productId = (int)($row['id'] ?? 0);
|
$productId = (int)($row['id'] ?? 0);
|
||||||
@@ -147,6 +169,7 @@ class SettingsController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Zamowienia ---
|
||||||
try {
|
try {
|
||||||
$orderStmt = $mdb->query(
|
$orderStmt = $mdb->query(
|
||||||
'SELECT '
|
'SELECT '
|
||||||
@@ -178,7 +201,10 @@ class SettingsController
|
|||||||
$orderStmt = false;
|
$orderStmt = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$orderRows = $orderStmt ? $orderStmt->fetchAll() : [];
|
$orderRows = ($orderStmt && method_exists($orderStmt, 'fetchAll'))
|
||||||
|
? $orderStmt->fetchAll(\PDO::FETCH_ASSOC)
|
||||||
|
: [];
|
||||||
|
|
||||||
if (is_array($orderRows)) {
|
if (is_array($orderRows)) {
|
||||||
foreach ($orderRows as $row) {
|
foreach ($orderRows as $row) {
|
||||||
$orderId = (int)($row['id'] ?? 0);
|
$orderId = (int)($row['id'] ?? 0);
|
||||||
@@ -214,11 +240,12 @@ class SettingsController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode([
|
$json = json_encode(['status' => 'ok', 'items' => array_slice($items, 0, 20)]);
|
||||||
'status' => 'ok',
|
if ($json === false) {
|
||||||
'items' => array_slice($items, 0, 20),
|
echo json_encode(['status' => 'ok', 'items' => []], JSON_UNESCAPED_UNICODE);
|
||||||
]);
|
return;
|
||||||
exit;
|
}
|
||||||
|
echo $json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -444,8 +471,7 @@ class SettingsController
|
|||||||
'label' => 'Htaccess cache',
|
'label' => 'Htaccess cache',
|
||||||
'tab' => 'system',
|
'tab' => 'system',
|
||||||
]),
|
]),
|
||||||
FormField::text('api_key', [
|
FormField::custom('api_key', $this->renderApiKeyField($data['api_key'] ?? ''), [
|
||||||
'label' => 'Klucz API (ordersPRO)',
|
|
||||||
'tab' => 'system',
|
'tab' => 'system',
|
||||||
]),
|
]),
|
||||||
|
|
||||||
@@ -533,4 +559,23 @@ class SettingsController
|
|||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function renderApiKeyField(string $value): string
|
||||||
|
{
|
||||||
|
$escaped = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
$js = "var c='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',"
|
||||||
|
. "k='';for(var i=0;i<32;i++){k+=c.charAt(Math.floor(Math.random()*c.length));}"
|
||||||
|
. "document.getElementById('api_key').value=k;";
|
||||||
|
|
||||||
|
return '<div class="form-group row">'
|
||||||
|
. '<label class="col-lg-4 control-label">Klucz API:</label>'
|
||||||
|
. '<div class="col-lg-8">'
|
||||||
|
. '<div class="input-group">'
|
||||||
|
. '<input type="text" id="api_key" class="form-control" name="api_key" value="' . $escaped . '" />'
|
||||||
|
. '<span class="input-group-addon btn btn-info" onclick="' . htmlspecialchars($js, ENT_QUOTES, 'UTF-8') . '">Generuj</span>'
|
||||||
|
. '</div>'
|
||||||
|
. '</div>'
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class ShopProductController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class ApiRouter
|
|||||||
}
|
}
|
||||||
|
|
||||||
$controller->$action();
|
$controller->$action();
|
||||||
} catch (\Exception $e) {
|
} catch (\Throwable $e) {
|
||||||
self::sendError('INTERNAL_ERROR', 'Internal server error', 500);
|
self::sendError('INTERNAL_ERROR', 'Internal server error', 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,8 @@ class ApiRouter
|
|||||||
$settingsRepo = new \Domain\Settings\SettingsRepository($db);
|
$settingsRepo = new \Domain\Settings\SettingsRepository($db);
|
||||||
$productRepo = new \Domain\Product\ProductRepository($db);
|
$productRepo = new \Domain\Product\ProductRepository($db);
|
||||||
$transportRepo = new \Domain\Transport\TransportRepository($db);
|
$transportRepo = new \Domain\Transport\TransportRepository($db);
|
||||||
$service = new \Domain\Order\OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo);
|
$cronJobRepo = new \Domain\CronJob\CronJobRepository($db);
|
||||||
|
$service = new \Domain\Order\OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo, $cronJobRepo);
|
||||||
return new Controllers\OrdersApiController($service, $orderRepo);
|
return new Controllers\OrdersApiController($service, $orderRepo);
|
||||||
},
|
},
|
||||||
'products' => function () use ($db) {
|
'products' => function () use ($db) {
|
||||||
@@ -100,7 +101,11 @@ class ApiRouter
|
|||||||
$transportRepo = new \Domain\Transport\TransportRepository($db);
|
$transportRepo = new \Domain\Transport\TransportRepository($db);
|
||||||
$paymentRepo = new \Domain\PaymentMethod\PaymentMethodRepository($db);
|
$paymentRepo = new \Domain\PaymentMethod\PaymentMethodRepository($db);
|
||||||
$attrRepo = new \Domain\Attribute\AttributeRepository($db);
|
$attrRepo = new \Domain\Attribute\AttributeRepository($db);
|
||||||
return new Controllers\DictionariesApiController($statusRepo, $transportRepo, $paymentRepo, $attrRepo);
|
$producerRepo = new \Domain\Producer\ProducerRepository($db);
|
||||||
|
return new Controllers\DictionariesApiController($statusRepo, $transportRepo, $paymentRepo, $attrRepo, $producerRepo);
|
||||||
|
},
|
||||||
|
'categories' => function () use ($db) {
|
||||||
|
return new Controllers\CategoriesApiController();
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
104
autoload/api/Controllers/CategoriesApiController.php
Normal file
104
autoload/api/Controllers/CategoriesApiController.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\Controllers;
|
||||||
|
|
||||||
|
use api\ApiRouter;
|
||||||
|
|
||||||
|
class CategoriesApiController
|
||||||
|
{
|
||||||
|
public function list(): void
|
||||||
|
{
|
||||||
|
if (!ApiRouter::requireMethod('GET')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = $GLOBALS['mdb'] ?? null;
|
||||||
|
if (!$db) {
|
||||||
|
ApiRouter::sendError('INTERNAL_ERROR', 'Database not available', 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default shop language
|
||||||
|
$defaultLang = $db->get('pp_langs', 'id', ['start' => 1]);
|
||||||
|
if (!$defaultLang) {
|
||||||
|
$defaultLang = 'pl';
|
||||||
|
}
|
||||||
|
$defaultLang = (string)$defaultLang;
|
||||||
|
|
||||||
|
// All active categories, ordered by display order
|
||||||
|
$rows = $db->select(
|
||||||
|
'pp_shop_categories',
|
||||||
|
['id', 'parent_id'],
|
||||||
|
[
|
||||||
|
'status' => 1,
|
||||||
|
'ORDER' => ['o' => 'ASC'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!is_array($rows) || empty($rows)) {
|
||||||
|
ApiRouter::sendSuccess(['categories' => []]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$categoryIds = array_values(array_filter(
|
||||||
|
array_map(fn($row) => (int)($row['id'] ?? 0), $rows),
|
||||||
|
fn($id) => $id > 0
|
||||||
|
));
|
||||||
|
|
||||||
|
// Bulk fetch titles for default language
|
||||||
|
$titlesByCategory = [];
|
||||||
|
$titleRows = $db->select('pp_shop_categories_langs', ['category_id', 'title'], [
|
||||||
|
'AND' => [
|
||||||
|
'category_id' => $categoryIds,
|
||||||
|
'lang_id' => $defaultLang,
|
||||||
|
'title[!]' => '',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
if (is_array($titleRows)) {
|
||||||
|
foreach ($titleRows as $tr) {
|
||||||
|
$tid = (int)($tr['category_id'] ?? 0);
|
||||||
|
if ($tid > 0 && !isset($titlesByCategory[$tid])) {
|
||||||
|
$titlesByCategory[$tid] = (string)($tr['title'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk fetch fallback titles for categories without a title in default language
|
||||||
|
$missingIds = array_values(array_filter($categoryIds, fn($id) => !isset($titlesByCategory[$id])));
|
||||||
|
if (!empty($missingIds)) {
|
||||||
|
$fallbackRows = $db->select('pp_shop_categories_langs', ['category_id', 'title'], [
|
||||||
|
'AND' => [
|
||||||
|
'category_id' => $missingIds,
|
||||||
|
'title[!]' => '',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
if (is_array($fallbackRows)) {
|
||||||
|
foreach ($fallbackRows as $fr) {
|
||||||
|
$fid = (int)($fr['category_id'] ?? 0);
|
||||||
|
if ($fid > 0 && !isset($titlesByCategory[$fid])) {
|
||||||
|
$titlesByCategory[$fid] = (string)($fr['title'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build flat category list
|
||||||
|
$categories = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$categoryId = (int)($row['id'] ?? 0);
|
||||||
|
if ($categoryId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentId = $row['parent_id'] !== null ? (int)$row['parent_id'] : null;
|
||||||
|
$title = $titlesByCategory[$categoryId] ?? ('Kategoria #' . $categoryId);
|
||||||
|
|
||||||
|
$categories[] = [
|
||||||
|
'id' => $categoryId,
|
||||||
|
'parent_id' => $parentId,
|
||||||
|
'title' => $title,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiRouter::sendSuccess(['categories' => $categories]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ namespace api\Controllers;
|
|||||||
|
|
||||||
use api\ApiRouter;
|
use api\ApiRouter;
|
||||||
use Domain\Attribute\AttributeRepository;
|
use Domain\Attribute\AttributeRepository;
|
||||||
|
use Domain\Producer\ProducerRepository;
|
||||||
use Domain\ShopStatus\ShopStatusRepository;
|
use Domain\ShopStatus\ShopStatusRepository;
|
||||||
use Domain\Transport\TransportRepository;
|
use Domain\Transport\TransportRepository;
|
||||||
use Domain\PaymentMethod\PaymentMethodRepository;
|
use Domain\PaymentMethod\PaymentMethodRepository;
|
||||||
@@ -13,17 +14,20 @@ class DictionariesApiController
|
|||||||
private $transportRepo;
|
private $transportRepo;
|
||||||
private $paymentRepo;
|
private $paymentRepo;
|
||||||
private $attrRepo;
|
private $attrRepo;
|
||||||
|
private $producerRepo;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ShopStatusRepository $statusRepo,
|
ShopStatusRepository $statusRepo,
|
||||||
TransportRepository $transportRepo,
|
TransportRepository $transportRepo,
|
||||||
PaymentMethodRepository $paymentRepo,
|
PaymentMethodRepository $paymentRepo,
|
||||||
AttributeRepository $attrRepo
|
AttributeRepository $attrRepo,
|
||||||
|
ProducerRepository $producerRepo
|
||||||
) {
|
) {
|
||||||
$this->statusRepo = $statusRepo;
|
$this->statusRepo = $statusRepo;
|
||||||
$this->transportRepo = $transportRepo;
|
$this->transportRepo = $transportRepo;
|
||||||
$this->paymentRepo = $paymentRepo;
|
$this->paymentRepo = $paymentRepo;
|
||||||
$this->attrRepo = $attrRepo;
|
$this->attrRepo = $attrRepo;
|
||||||
|
$this->producerRepo = $producerRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function statuses(): void
|
public function statuses(): void
|
||||||
@@ -94,4 +98,111 @@ class DictionariesApiController
|
|||||||
|
|
||||||
ApiRouter::sendSuccess($attributes);
|
ApiRouter::sendSuccess($attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ensure_attribute(): void
|
||||||
|
{
|
||||||
|
if (!ApiRouter::requireMethod('POST')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = ApiRouter::getJsonBody();
|
||||||
|
if (!is_array($body)) {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim((string) ($body['name'] ?? ''));
|
||||||
|
if ($name === '') {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing name', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = (int) ($body['type'] ?? 0);
|
||||||
|
$lang = trim((string) ($body['lang'] ?? 'pl'));
|
||||||
|
if ($lang === '') {
|
||||||
|
$lang = 'pl';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->attrRepo->ensureAttributeForApi($name, $type, $lang);
|
||||||
|
if (!is_array($result) || (int) ($result['id'] ?? 0) <= 0) {
|
||||||
|
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to ensure attribute', 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiRouter::sendSuccess([
|
||||||
|
'id' => (int) ($result['id'] ?? 0),
|
||||||
|
'created' => !empty($result['created']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ensure_attribute_value(): void
|
||||||
|
{
|
||||||
|
if (!ApiRouter::requireMethod('POST')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = ApiRouter::getJsonBody();
|
||||||
|
if (!is_array($body)) {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributeId = (int) ($body['attribute_id'] ?? 0);
|
||||||
|
if ($attributeId <= 0) {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid attribute_id', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim((string) ($body['name'] ?? ''));
|
||||||
|
if ($name === '') {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing name', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lang = trim((string) ($body['lang'] ?? 'pl'));
|
||||||
|
if ($lang === '') {
|
||||||
|
$lang = 'pl';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->attrRepo->ensureAttributeValueForApi($attributeId, $name, $lang);
|
||||||
|
if (!is_array($result) || (int) ($result['id'] ?? 0) <= 0) {
|
||||||
|
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to ensure attribute value', 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiRouter::sendSuccess([
|
||||||
|
'id' => (int) ($result['id'] ?? 0),
|
||||||
|
'created' => !empty($result['created']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ensure_producer(): void
|
||||||
|
{
|
||||||
|
if (!ApiRouter::requireMethod('POST')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = ApiRouter::getJsonBody();
|
||||||
|
if (!is_array($body)) {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim((string) ($body['name'] ?? ''));
|
||||||
|
if ($name === '') {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing name', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->producerRepo->ensureProducerForApi($name);
|
||||||
|
if ((int) ($result['id'] ?? 0) <= 0) {
|
||||||
|
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to ensure producer', 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiRouter::sendSuccess([
|
||||||
|
'id' => (int) ($result['id'] ?? 0),
|
||||||
|
'created' => !empty($result['created']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,6 +296,96 @@ class ProductsApiController
|
|||||||
ApiRouter::sendSuccess(['id' => $variantId, 'deleted' => true]);
|
ApiRouter::sendSuccess(['id' => $variantId, 'deleted' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function upload_image(): void
|
||||||
|
{
|
||||||
|
if (!ApiRouter::requireMethod('POST')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = ApiRouter::getJsonBody();
|
||||||
|
if ($body === null) {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$productId = (int)($body['id'] ?? 0);
|
||||||
|
if ($productId <= 0) {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid product id', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = $this->productRepo->find($productId);
|
||||||
|
if ($product === null) {
|
||||||
|
ApiRouter::sendError('NOT_FOUND', 'Product not found', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = trim((string)($body['file_name'] ?? ''));
|
||||||
|
$base64 = (string)($body['content_base64'] ?? '');
|
||||||
|
if ($fileName === '' || $base64 === '') {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Missing file_name or content_base64', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$binary = base64_decode($base64, true);
|
||||||
|
if ($binary === false) {
|
||||||
|
ApiRouter::sendError('BAD_REQUEST', 'Invalid content_base64 payload', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($fileName));
|
||||||
|
if ($safeName === '' || $safeName === null) {
|
||||||
|
$safeName = 'image_' . md5((string)microtime(true)) . '.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
// api.php działa z rootu projektu (nie z admin/), więc ścieżka bez ../
|
||||||
|
$baseDir = 'upload/product_images/product_' . $productId;
|
||||||
|
if (!is_dir($baseDir) && !mkdir($baseDir, 0775, true) && !is_dir($baseDir)) {
|
||||||
|
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to create target directory', 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetPath = $baseDir . '/' . $safeName;
|
||||||
|
if (is_file($targetPath)) {
|
||||||
|
$name = pathinfo($safeName, PATHINFO_FILENAME);
|
||||||
|
$ext = pathinfo($safeName, PATHINFO_EXTENSION);
|
||||||
|
$targetPath = $baseDir . '/' . $name . '_' . substr(md5($safeName . microtime(true)), 0, 8) . ($ext !== '' ? '.' . $ext : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($targetPath, $binary) === false) {
|
||||||
|
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to save image file', 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$src = '/upload/product_images/product_' . $productId . '/' . basename($targetPath);
|
||||||
|
$alt = (string)($body['alt'] ?? '');
|
||||||
|
$position = isset($body['o']) ? (int)$body['o'] : null;
|
||||||
|
|
||||||
|
$db = $GLOBALS['mdb'] ?? null;
|
||||||
|
if (!$db) {
|
||||||
|
ApiRouter::sendError('INTERNAL_ERROR', 'Database not available', 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($position === null) {
|
||||||
|
$max = $db->max('pp_shop_products_images', 'o', ['product_id' => $productId]);
|
||||||
|
$position = (int)$max + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->insert('pp_shop_products_images', [
|
||||||
|
'product_id' => $productId,
|
||||||
|
'src' => $src,
|
||||||
|
'alt' => $alt,
|
||||||
|
'o' => $position,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ApiRouter::sendSuccess([
|
||||||
|
'src' => $src,
|
||||||
|
'alt' => $alt,
|
||||||
|
'o' => $position,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapuje dane z JSON API na format oczekiwany przez saveProduct().
|
* Mapuje dane z JSON API na format oczekiwany przez saveProduct().
|
||||||
*
|
*
|
||||||
@@ -339,6 +429,11 @@ class ProductsApiController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveProduct() traktuje float 0.00 jako "puste", ale cena 0 musi pozostać jawnie ustawiona.
|
||||||
|
if (isset($d['price_brutto']) && is_numeric($d['price_brutto']) && (float)$d['price_brutto'] === 0.0) {
|
||||||
|
$d['price_brutto'] = '0';
|
||||||
|
}
|
||||||
|
|
||||||
// String fields — direct mapping
|
// String fields — direct mapping
|
||||||
$stringFields = [
|
$stringFields = [
|
||||||
'sku', 'ean', 'custom_label_0', 'custom_label_1', 'custom_label_2',
|
'sku', 'ean', 'custom_label_0', 'custom_label_1', 'custom_label_2',
|
||||||
@@ -403,6 +498,21 @@ class ProductsApiController
|
|||||||
$d['products_related'] = $body['products_related'];
|
$d['products_related'] = $body['products_related'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom fields (Dodatkowe pola)
|
||||||
|
if (isset($body['custom_fields']) && is_array($body['custom_fields'])) {
|
||||||
|
$d['custom_field_name'] = [];
|
||||||
|
$d['custom_field_type'] = [];
|
||||||
|
$d['custom_field_required'] = [];
|
||||||
|
foreach ($body['custom_fields'] as $cf) {
|
||||||
|
if (!is_array($cf) || empty($cf['name'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$d['custom_field_name'][] = (string)$cf['name'];
|
||||||
|
$d['custom_field_type'][] = !empty($cf['type']) ? (string)$cf['type'] : 'text';
|
||||||
|
$d['custom_field_required'][] = !empty($cf['is_required']) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $d;
|
return $d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,9 +177,10 @@ class App
|
|||||||
'ShopOrder' => function() {
|
'ShopOrder' => function() {
|
||||||
global $mdb;
|
global $mdb;
|
||||||
$orderRepo = new \Domain\Order\OrderRepository( $mdb );
|
$orderRepo = new \Domain\Order\OrderRepository( $mdb );
|
||||||
|
$cronJobRepo = new \Domain\CronJob\CronJobRepository( $mdb );
|
||||||
return new \front\Controllers\ShopOrderController(
|
return new \front\Controllers\ShopOrderController(
|
||||||
$orderRepo,
|
$orderRepo,
|
||||||
new \Domain\Order\OrderAdminService( $orderRepo )
|
new \Domain\Order\OrderAdminService( $orderRepo, null, null, null, $cronJobRepo )
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
'ShopProducer' => function() {
|
'ShopProducer' => function() {
|
||||||
|
|||||||
@@ -215,7 +215,10 @@ $sqlQueries = @()
|
|||||||
$migrationFile = "migrations/$versionNumber.sql"
|
$migrationFile = "migrations/$versionNumber.sql"
|
||||||
|
|
||||||
if (Test-Path $migrationFile) {
|
if (Test-Path $migrationFile) {
|
||||||
$sqlQueries = @(Get-Content $migrationFile | Where-Object { $_.Trim() -ne '' } | ForEach-Object { $_.ToString() })
|
# Read entire file, strip comment lines, split by semicolons to get complete SQL statements
|
||||||
|
$rawLines = Get-Content $migrationFile | Where-Object { $_.Trim() -ne '' -and $_.Trim() -notmatch '^\s*--' }
|
||||||
|
$rawSql = ($rawLines -join "`n").Trim()
|
||||||
|
$sqlQueries = @($rawSql -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } | ForEach-Object { $_.ToString() })
|
||||||
Write-Step "Znaleziono migracje SQL: $migrationFile ($($sqlQueries.Count) zapytan)"
|
Write-Step "Znaleziono migracje SQL: $migrationFile ($($sqlQueries.Count) zapytan)"
|
||||||
} else {
|
} else {
|
||||||
Write-Step "Brak migracji SQL ($migrationFile nie istnieje)"
|
Write-Step "Brak migracji SQL ($migrationFile nie istnieje)"
|
||||||
@@ -262,6 +265,10 @@ if (Test-Path $tempDir) {
|
|||||||
Remove-Item -Recurse -Force $tempDir
|
Remove-Item -Recurse -Force $tempDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $tempDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($f in $filesToPack) {
|
foreach ($f in $filesToPack) {
|
||||||
$destPath = Join-Path $tempDir $f
|
$destPath = Join-Path $tempDir $f
|
||||||
$destDir = Split-Path $destPath -Parent
|
$destDir = Split-Path $destPath -Parent
|
||||||
@@ -295,7 +302,16 @@ if (Test-Path $zipPath) {
|
|||||||
# Pakuj zawartosc temp dir (bez folderu temp/)
|
# Pakuj zawartosc temp dir (bez folderu temp/)
|
||||||
$originalLocation = Get-Location
|
$originalLocation = Get-Location
|
||||||
Set-Location $tempDir
|
Set-Location $tempDir
|
||||||
Compress-Archive -Path '*' -DestinationPath "../../$zipPath" -Force
|
$tempItems = Get-ChildItem -Force
|
||||||
|
if ($tempItems) {
|
||||||
|
Compress-Archive -Path '*' -DestinationPath "../../$zipPath" -Force
|
||||||
|
} else {
|
||||||
|
# SQL-only update: create minimal ZIP with empty placeholder
|
||||||
|
$placeholderPath = "_sql_only_update.txt"
|
||||||
|
Set-Content -Path $placeholderPath -Value "SQL-only update $versionNumber"
|
||||||
|
Compress-Archive -Path $placeholderPath -DestinationPath "../../$zipPath" -Force
|
||||||
|
Remove-Item $placeholderPath -Force
|
||||||
|
}
|
||||||
Set-Location $originalLocation
|
Set-Location $originalLocation
|
||||||
|
|
||||||
Write-Ok "Utworzono ZIP: $zipPath"
|
Write-Ok "Utworzono ZIP: $zipPath"
|
||||||
@@ -355,9 +371,9 @@ if (Test-Path $versionsFile) {
|
|||||||
Write-Ok "Zaktualizowano versions.php: `$current_ver = $versionInt"
|
Write-Ok "Zaktualizowano versions.php: `$current_ver = $versionInt"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 14. Aktualizacja changelog.php ---
|
# --- 14. Aktualizacja changelog-data.html ---
|
||||||
|
|
||||||
$changelogFile = "updates/changelog.php"
|
$changelogFile = "updates/changelog-data.html"
|
||||||
if (Test-Path $changelogFile) {
|
if (Test-Path $changelogFile) {
|
||||||
$dateStr = Get-Date -Format "dd.MM.yyyy"
|
$dateStr = Get-Date -Format "dd.MM.yyyy"
|
||||||
$newEntry = "<b>ver. $versionNumber - $dateStr</b><br />`n$ChangelogEntry`n<hr>`n"
|
$newEntry = "<b>ver. $versionNumber - $dateStr</b><br />`n$ChangelogEntry`n<hr>`n"
|
||||||
@@ -365,7 +381,7 @@ if (Test-Path $changelogFile) {
|
|||||||
$changelogContent = Get-Content $changelogFile -Raw
|
$changelogContent = Get-Content $changelogFile -Raw
|
||||||
$changelogContent = $newEntry + $changelogContent
|
$changelogContent = $newEntry + $changelogContent
|
||||||
[System.IO.File]::WriteAllText($changelogFile, $changelogContent, $Utf8NoBom)
|
[System.IO.File]::WriteAllText($changelogFile, $changelogContent, $Utf8NoBom)
|
||||||
Write-Ok "Zaktualizowano changelog.php"
|
Write-Ok "Zaktualizowano changelog-data.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 15. Cleanup ---
|
# --- 15. Cleanup ---
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
$database['host'] = 'localhost';
|
$database['host'] = 'localhost';
|
||||||
$database['remote_host'] = 'host700513.hostido.net.pl';
|
$database['remote_host'] = 'host117523.hostido.net.pl';
|
||||||
$database['user'] = 'host117523_shoppro';
|
$database['user'] = 'host117523_shoppro';
|
||||||
$database['password'] = 'mhA9WCEXEnRfTtbN33hL';
|
$database['password'] = 'mhA9WCEXEnRfTtbN33hL';
|
||||||
$database['name'] = 'host117523_shoppro';
|
$database['name'] = 'host117523_shoppro';
|
||||||
@@ -13,8 +13,10 @@ $config['redis']['host'] = '127.0.0.1';
|
|||||||
$config['redis']['port'] = 20470;
|
$config['redis']['port'] = 20470;
|
||||||
$config['redis']['password'] = 'Gi7FzWtkry19hZ1BqT1LKEWfwokQpigh';
|
$config['redis']['password'] = 'Gi7FzWtkry19hZ1BqT1LKEWfwokQpigh';
|
||||||
|
|
||||||
$config['debug']['apilo'] = false;
|
$config['debug']['apilo'] = true;
|
||||||
|
|
||||||
$config['trustmate']['enabled'] = true;
|
$config['trustmate']['enabled'] = true;
|
||||||
$config['trustmate']['uid'] = '34eb36ba-c715-4cdc-8707-22376c9f14c7';
|
$config['trustmate']['uid'] = '34eb36ba-c715-4cdc-8707-22376c9f14c7';
|
||||||
|
|
||||||
|
$config['cron_key'] = 'Gi7FzWtkry19hZ1BqT1LKEWfwokQpigh';
|
||||||
?>
|
?>
|
||||||
|
|||||||
454
cron.php
454
cron.php
@@ -50,19 +50,26 @@ $mdb = new medoo( [
|
|||||||
'charset' => 'utf8'
|
'charset' => 'utf8'
|
||||||
] );
|
] );
|
||||||
|
|
||||||
$settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings();
|
// =========================================================================
|
||||||
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
|
// Auth: cron endpoint protection
|
||||||
$apilo_settings = $integrationsRepository -> getSettings( 'apilo' );
|
// =========================================================================
|
||||||
|
|
||||||
// Keepalive tokenu Apilo: odswiezaj token przed wygasnieciem, zeby integracja byla stale aktywna.
|
if ( php_sapi_name() !== 'cli' )
|
||||||
if ( (int)($apilo_settings['enabled'] ?? 0) === 1 ) {
|
{
|
||||||
$integrationsRepository -> apiloKeepalive( 300 );
|
$cron_key = isset( $config['cron_key'] ) ? $config['cron_key'] : '';
|
||||||
$apilo_settings = $integrationsRepository -> getSettings( 'apilo' );
|
$provided_key = isset( $_GET['key'] ) ? $_GET['key'] : '';
|
||||||
$orderRepo = new \Domain\Order\OrderRepository( $mdb );
|
|
||||||
$orderAdminService = new \Domain\Order\OrderAdminService( $orderRepo );
|
if ( $cron_key === '' || $provided_key !== $cron_key )
|
||||||
$orderAdminService->processApiloSyncQueue( 10 );
|
{
|
||||||
|
http_response_code( 403 );
|
||||||
|
exit( 'Forbidden' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper functions (used by handlers)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
function parsePaczkomatAddress($input)
|
function parsePaczkomatAddress($input)
|
||||||
{
|
{
|
||||||
$pattern = '/^([\w-]+)\s+\|\s+([^,]+),\s+(\d{2}-\d{3})\s+(.+)$/';
|
$pattern = '/^([\w-]+)\s+\|\s+([^,]+),\s+(\d{2}-\d{3})\s+(.+)$/';
|
||||||
@@ -118,92 +125,90 @@ function getImageUrlById($id) {
|
|||||||
return isset($data['img']) ? $data['img'] : null;
|
return isset($data['img']) ? $data['img'] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pobieranie informacji o produkcie z apilo.com
|
// =========================================================================
|
||||||
if ( $apilo_settings['enabled'] and $apilo_settings['sync_products'] and $apilo_settings['access-token'] )
|
// Shared dependencies
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
$settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings();
|
||||||
|
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
|
||||||
|
$orderRepo = new \Domain\Order\OrderRepository( $mdb );
|
||||||
|
$cronRepo = new \Domain\CronJob\CronJobRepository( $mdb );
|
||||||
|
$orderAdminService = new \Domain\Order\OrderAdminService( $orderRepo, null, null, null, $cronRepo );
|
||||||
|
|
||||||
|
$processor = new \Domain\CronJob\CronJobProcessor( $cronRepo );
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// One-time migration: JSON queue → DB
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
$json_queue_path = __DIR__ . '/temp/apilo-sync-queue.json';
|
||||||
|
if ( file_exists( $json_queue_path ) )
|
||||||
{
|
{
|
||||||
if ( $result = $mdb -> query( 'SELECT id, apilo_product_id, apilo_get_data_date, apilo_product_name FROM pp_shop_products WHERE apilo_product_id IS NOT NULL AND apilo_product_id != 0 AND ( apilo_get_data_date IS NULL OR apilo_get_data_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_get_data_date ASC LIMIT 1' ) -> fetch( \PDO::FETCH_ASSOC ) )
|
$json_content = file_get_contents( $json_queue_path );
|
||||||
|
$json_queue = $json_content ? json_decode( $json_content, true ) : [];
|
||||||
|
|
||||||
|
if ( is_array( $json_queue ) )
|
||||||
{
|
{
|
||||||
$access_token = $integrationsRepository -> apiloGetAccessToken();
|
foreach ( $json_queue as $task )
|
||||||
$url = 'https://projectpro.apilo.com/rest/api/warehouse/product/' . $result['apilo_product_id'] . '/';
|
{
|
||||||
$curl = curl_init( $url );
|
$order_id = (int)($task['order_id'] ?? 0);
|
||||||
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
|
if ( $order_id <= 0 ) continue;
|
||||||
curl_setopt( $curl, CURLOPT_HTTPHEADER, [
|
|
||||||
"Authorization: Bearer " . $access_token,
|
|
||||||
"Accept: application/json"
|
|
||||||
] );
|
|
||||||
|
|
||||||
$response = curl_exec( $curl );
|
if ( !empty($task['payment']) )
|
||||||
$responseData = json_decode( $response, true );
|
{
|
||||||
|
$cronRepo->enqueue(
|
||||||
// aktualizowanie stanu magazynowego
|
\Domain\CronJob\CronJobType::APILO_SYNC_PAYMENT,
|
||||||
$mdb -> update( 'pp_shop_products', [ 'quantity' => $responseData['quantity'] ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
['order_id' => $order_id],
|
||||||
// aktualizowanie ceny
|
\Domain\CronJob\CronJobType::PRIORITY_HIGH,
|
||||||
$mdb -> update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithoutTax'], 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithTax'], 2 ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
50
|
||||||
|
);
|
||||||
$mdb -> update( 'pp_shop_products', [ 'apilo_get_data_date' => date( 'Y-m-d H:i:s' ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
|
||||||
|
|
||||||
// Czyszczenie cache produktu
|
|
||||||
\Shared\Helpers\Helpers::clear_product_cache( (int)$result['id'] );
|
|
||||||
|
|
||||||
echo '<p>Zaktualizowałem dane produktu (APILO) <b>' . $result['apilo_product_name'] . ' #' . $result['id'] . '</b></p>';
|
|
||||||
}
|
}
|
||||||
|
if ( isset($task['status']) && $task['status'] !== null && $task['status'] !== '' )
|
||||||
|
{
|
||||||
|
$cronRepo->enqueue(
|
||||||
|
\Domain\CronJob\CronJobType::APILO_SYNC_STATUS,
|
||||||
|
['order_id' => $order_id, 'status' => (int)$task['status']],
|
||||||
|
\Domain\CronJob\CronJobType::PRIORITY_HIGH,
|
||||||
|
50
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink( $json_queue_path );
|
||||||
|
echo '<p>Migracja kolejki JSON → DB zakończona</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// synchronizacja cen apilo.com
|
// =========================================================================
|
||||||
if ( $apilo_settings['enabled'] and $apilo_settings['access-token'] and ( !$apilo_settings['pricelist_update_date'] or $apilo_settings['pricelist_update_date'] <= date( 'Y-m-d H:i:s', strtotime( '-1 hour', time() ) ) ) )
|
// Handler registration
|
||||||
{
|
// =========================================================================
|
||||||
$access_token = $integrationsRepository -> apiloGetAccessToken();
|
|
||||||
|
|
||||||
$url = 'https://projectpro.apilo.com/rest/api/warehouse/price-calculated/?price=' . $apilo_settings['pricelist_id'];
|
// 1. Apilo token keepalive (priorytet: krytyczny)
|
||||||
|
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_TOKEN_KEEPALIVE, function($payload) use ($integrationsRepository) {
|
||||||
|
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||||
|
if ( !(int)($apilo_settings['enabled'] ?? 0) ) return true; // skip if disabled
|
||||||
|
|
||||||
$curl = curl_init( $url );
|
$integrationsRepository->apiloKeepalive( 300 );
|
||||||
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
|
echo '<p>Apilo token keepalive</p>';
|
||||||
curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, "GET" );
|
return true;
|
||||||
curl_setopt( $curl, CURLOPT_HTTPHEADER, [
|
});
|
||||||
"Authorization: Bearer " . $access_token,
|
|
||||||
"Accept: application/json",
|
|
||||||
"Content-Type: application/json"
|
|
||||||
] );
|
|
||||||
|
|
||||||
$response = curl_exec( $curl );
|
// 2. Apilo send order (priorytet: wysoki)
|
||||||
$responseData = json_decode( $response, true );
|
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SEND_ORDER, function($payload) use ($mdb, $integrationsRepository, $orderAdminService, $config) {
|
||||||
|
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||||
|
if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_orders'] || !$apilo_settings['access-token'] || $apilo_settings['sync_orders_date_start'] > date('Y-m-d H:i:s') ) return true;
|
||||||
|
|
||||||
if ( $responseData['list'] )
|
$orders = $mdb->select( 'pp_shop_orders', '*', [ 'AND' => [ 'apilo_order_id' => null, 'date_order[>=]' => $apilo_settings['sync_orders_date_start'] ], 'ORDER' => [ 'date_order' => 'ASC' ], 'LIMIT' => 1 ] );
|
||||||
{
|
if ( empty($orders) ) return true;
|
||||||
foreach ( $responseData['list'] as $product_price )
|
|
||||||
{
|
|
||||||
//aktualizowanie ceny
|
|
||||||
if ( $product_price['customPriceWithTax'] )
|
|
||||||
{
|
|
||||||
$price_brutto = $product_price['customPriceWithTax'];
|
|
||||||
$vat = $vat = $mdb -> get( 'pp_shop_products', 'vat', [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
|
||||||
$price_netto = $price_brutto / ( ( 100 + $vat ) / 100 );
|
|
||||||
|
|
||||||
$mdb -> update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $price_netto, 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $price_brutto, 2 ) ], [ 'apilo_product_id' => $product_price['product'] ] );
|
|
||||||
$product_id = $mdb -> get( 'pp_shop_products', 'id', [ 'apilo_product_id' => $product_price['product'] ] );
|
|
||||||
|
|
||||||
( new \Domain\Product\ProductRepository( $mdb ) )->updateCombinationPricesFromBase( (int)$product_id, $price_brutto, $vat, null );
|
|
||||||
|
|
||||||
// Czyszczenie cache produktu
|
|
||||||
\Shared\Helpers\Helpers::clear_product_cache( (int)$product_id );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$integrationsRepository -> saveSetting( 'apilo', 'pricelist_update_date', date( 'Y-m-d H:i:s' ) );
|
|
||||||
echo '<p>Zaktualizowałem ceny produktów (APILO)</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// wysyłanie zamówień do apilo
|
|
||||||
if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_settings['access-token'] and $apilo_settings['sync_orders_date_start'] <= date( 'Y-m-d H:i:s' ) )
|
|
||||||
{
|
|
||||||
$orders = $mdb -> select( 'pp_shop_orders', '*', [ 'AND' => [ 'apilo_order_id' => null, 'date_order[>=]' => $apilo_settings['sync_orders_date_start'] ], 'ORDER' => [ 'date_order' => 'ASC' ], 'LIMIT' => 1 ] );
|
|
||||||
foreach ( $orders as $order )
|
foreach ( $orders as $order )
|
||||||
{
|
{
|
||||||
$products = $mdb -> select( 'pp_shop_order_products', '*', [ 'order_id' => $order['id'] ] );
|
$products = $mdb->select( 'pp_shop_order_products', '*', [ 'order_id' => $order['id'] ] );
|
||||||
|
$productRepo = new \Domain\Product\ProductRepository( $mdb );
|
||||||
$products_array = [];
|
$products_array = [];
|
||||||
|
$order_message = '';
|
||||||
foreach ( $products as $product )
|
foreach ( $products as $product )
|
||||||
{
|
{
|
||||||
$productRepo = new \Domain\Product\ProductRepository( $mdb );
|
|
||||||
$sku = $productRepo->getSkuWithFallback( (int)$product['product_id'], true );
|
$sku = $productRepo->getSkuWithFallback( (int)$product['product_id'], true );
|
||||||
|
|
||||||
$products_array[] = [
|
$products_array[] = [
|
||||||
@@ -211,8 +216,8 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
'ean' => $productRepo->getEanWithFallback( (int)$product['product_id'], true ),
|
'ean' => $productRepo->getEanWithFallback( (int)$product['product_id'], true ),
|
||||||
'sku' => $sku ? $sku : md5( $product['product_id'] ),
|
'sku' => $sku ? $sku : md5( $product['product_id'] ),
|
||||||
'originalName' => $product['name'],
|
'originalName' => $product['name'],
|
||||||
'originalPriceWithTax' => $product['price_brutto_promo'] ? str_replace( ',', '.', $product['price_brutto_promo'] ) : str_replace( ',', '.', $product['price_brutto'] ),
|
'originalPriceWithTax' => (float)$product['price_brutto_promo'] > 0 ? str_replace( ',', '.', $product['price_brutto_promo'] ) : str_replace( ',', '.', $product['price_brutto'] ),
|
||||||
'originalPriceWithoutTax' => $product['price_brutto_promo'] ? str_replace( ',', '.', round( $product['price_brutto_promo'] / ( 1 + $product['vat']/100 ), 2 ) ) : str_replace( ',', '.', round( $product['price_brutto'] / ( 1 + $product['vat']/100 ), 2 ) ),
|
'originalPriceWithoutTax' => (float)$product['price_brutto_promo'] > 0 ? str_replace( ',', '.', round( $product['price_brutto_promo'] / ( 1 + $product['vat']/100 ), 2 ) ) : str_replace( ',', '.', round( $product['price_brutto'] / ( 1 + $product['vat']/100 ), 2 ) ),
|
||||||
'quantity' => $product['quantity'],
|
'quantity' => $product['quantity'],
|
||||||
'tax' => number_format( $product['vat'], 2, '.', '' ),
|
'tax' => number_format( $product['vat'], 2, '.', '' ),
|
||||||
'status' => 1,
|
'status' => 1,
|
||||||
@@ -236,11 +241,9 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
$order_message .= '<hr>';
|
$order_message .= '<hr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: ostatnio był problem kiedy wiadomość miała mniej 1024 znaki ale zawierała przeniesienie tekstu '<br>' i do tego jeszcze miała emoji. Wtedy APILO tego nie przepuszczał.
|
|
||||||
if ( strlen( $order_message ) > 850 )
|
if ( strlen( $order_message ) > 850 )
|
||||||
$order_message = '<p><strong>Wiadomość do zamówienia była zbyt długa. Sprawdź szczegóły w panelu sklepu</strong></p>';
|
$order_message = '<p><strong>Wiadomość do zamówienia była zbyt długa. Sprawdź szczegóły w panelu sklepu</strong></p>';
|
||||||
|
|
||||||
// add transport as product
|
|
||||||
$products_array[] = [
|
$products_array[] = [
|
||||||
'idExternal' => '',
|
'idExternal' => '',
|
||||||
'ean' => null,
|
'ean' => null,
|
||||||
@@ -255,10 +258,26 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
'media' => null
|
'media' => null
|
||||||
];
|
];
|
||||||
|
|
||||||
$access_token = $integrationsRepository -> apiloGetAccessToken();
|
$has_priced_products = false;
|
||||||
|
foreach ( $products_array as $pa )
|
||||||
|
{
|
||||||
|
if ( $pa['type'] == 1 && (float)$pa['originalPriceWithTax'] > 0 )
|
||||||
|
{
|
||||||
|
$has_priced_products = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( !$has_priced_products )
|
||||||
|
{
|
||||||
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Pominięto zamówienie - wszystkie produkty mają cenę 0.00', [ 'products' => $products_array ] );
|
||||||
|
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Apilo: zamówienie #' . $order['id'] . ' ma zerowe ceny produktów', 'Zamówienie #' . $order['id'] . ' nie zostało wysłane do Apilo, ponieważ wszystkie produkty mają cenę 0.00 PLN. Sprawdź zamówienie w panelu sklepu.' );
|
||||||
|
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => -2 ], [ 'id' => $order['id'] ] );
|
||||||
|
echo '<p>Pominięto zamówienie #' . $order['id'] . ' - zerowe ceny produktów</p>';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$access_token = $integrationsRepository->apiloGetAccessToken();
|
||||||
$order_date = new DateTime( $order['date_order'] );
|
$order_date = new DateTime( $order['date_order'] );
|
||||||
|
|
||||||
$paczkomatData = parsePaczkomatAddress( $order['inpost_paczkomat'] );
|
$paczkomatData = parsePaczkomatAddress( $order['inpost_paczkomat'] );
|
||||||
$orlenPointData = parseOrlenAddress( $order['orlen_point'] );
|
$orlenPointData = parseOrlenAddress( $order['orlen_point'] );
|
||||||
|
|
||||||
@@ -306,7 +325,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
'originalCurrency' => 'PLN',
|
'originalCurrency' => 'PLN',
|
||||||
'originalAmountTotalWithTax' => str_replace( ',', '.', $order['summary'] ),
|
'originalAmountTotalWithTax' => str_replace( ',', '.', $order['summary'] ),
|
||||||
'orderItems' => $products_array,
|
'orderItems' => $products_array,
|
||||||
'orderedAt' => $order_date -> format('Y-m-d\TH:i:s\Z'),
|
'orderedAt' => $order_date->format('Y-m-d\TH:i:s\Z'),
|
||||||
'addressCustomer' => [
|
'addressCustomer' => [
|
||||||
'name' => $order['client_name'] . ' ' . $order['client_surname'],
|
'name' => $order['client_name'] . ' ' . $order['client_surname'],
|
||||||
'phone' => $order['client_phone'],
|
'phone' => $order['client_phone'],
|
||||||
@@ -341,7 +360,6 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
$postData['addressInvoice']['companyTaxNumber'] = $order['firm_nip'];
|
$postData['addressInvoice']['companyTaxNumber'] = $order['firm_nip'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// jeżeli paczkomat
|
|
||||||
if ( $order['inpost_paczkomat'] )
|
if ( $order['inpost_paczkomat'] )
|
||||||
{
|
{
|
||||||
$postData['addressDelivery']['parcelName'] = $order['inpost_paczkomat'] ? 'Paczkomat: ' . $order['inpost_paczkomat'] : null;
|
$postData['addressDelivery']['parcelName'] = $order['inpost_paczkomat'] ? 'Paczkomat: ' . $order['inpost_paczkomat'] : null;
|
||||||
@@ -361,7 +379,6 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// jeżeli orlen paczka
|
|
||||||
if ( $order['orlen_point'] )
|
if ( $order['orlen_point'] )
|
||||||
{
|
{
|
||||||
$postData['addressDelivery']['parcelName'] = $order['orlen_point'] ? 'Automat ORLEN ' . $order['orlen_point'] : null;
|
$postData['addressDelivery']['parcelName'] = $order['orlen_point'] ? 'Automat ORLEN ' . $order['orlen_point'] : null;
|
||||||
@@ -379,16 +396,14 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
$postData['addressDelivery']['zipCode'] = $postalCode;
|
$postData['addressDelivery']['zipCode'] = $postalCode;
|
||||||
$postData['addressDelivery']['city'] = $city;
|
$postData['addressDelivery']['city'] = $city;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $order['paid'] )
|
if ( $order['paid'] )
|
||||||
{
|
{
|
||||||
$payment_date = new DateTime( $order['date_order'] );
|
$payment_date = new DateTime( $order['date_order'] );
|
||||||
|
|
||||||
$postData['orderPayments'][] = [
|
$postData['orderPayments'][] = [
|
||||||
'amount' => str_replace( ',', '.', $order['summary'] ),
|
'amount' => str_replace( ',', '.', $order['summary'] ),
|
||||||
'paymentDate' => $payment_date -> format('Y-m-d\TH:i:s\Z'),
|
'paymentDate' => $payment_date->format('Y-m-d\TH:i:s\Z'),
|
||||||
'type' => ( new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) )->getApiloPaymentTypeId( (int)$order['payment_method_id'] )
|
'type' => ( new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) )->getApiloPaymentTypeId( (int)$order['payment_method_id'] )
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -410,35 +425,34 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd cURL przy wysyłaniu zamówienia: ' . $curl_error_send, [ 'curl_error' => $curl_error_send ] );
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd cURL przy wysyłaniu zamówienia: ' . $curl_error_send, [ 'curl_error' => $curl_error_send ] );
|
||||||
echo 'Błąd cURL: ' . $curl_error_send;
|
echo 'Błąd cURL: ' . $curl_error_send;
|
||||||
}
|
}
|
||||||
|
$http_code_send = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||||
curl_close( $ch );
|
curl_close( $ch );
|
||||||
|
|
||||||
$http_code_send = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
|
||||||
$response = json_decode( $response, true );
|
$response = json_decode( $response, true );
|
||||||
|
|
||||||
if ( $config['debug']['apilo'] )
|
if ( isset($config['debug']['apilo']) && $config['debug']['apilo'] )
|
||||||
{
|
{
|
||||||
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', date( 'Y-m-d H:i:s' ) . " --- SEND ORDER TO APILO\n\n", FILE_APPEND );
|
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', date( 'Y-m-d H:i:s' ) . " --- SEND ORDER TO APILO\n\n", FILE_APPEND );
|
||||||
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $postData, true ) . "\n\n", FILE_APPEND );
|
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $postData, true ) . "\n\n", FILE_APPEND );
|
||||||
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $response, true ) . "\n\n", FILE_APPEND );
|
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $response, true ) . "\n\n", FILE_APPEND );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $response['message'] == 'Order already exists' )
|
if ( isset($response['message']) && $response['message'] == 'Order already exists' )
|
||||||
{
|
{
|
||||||
$apilo_order_id = str_replace( 'Order id: ', '', $response['description'] );
|
$apilo_order_id = str_replace( 'Order id: ', '', $response['description'] );
|
||||||
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
|
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
|
||||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie już istnieje w Apilo (apilo_order_id: ' . $apilo_order_id . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie już istnieje w Apilo (apilo_order_id: ' . $apilo_order_id . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
||||||
echo '<p>Zaktualizowałem id zamówienia na podstawie zamówienia apilo.com</p>';
|
echo '<p>Zaktualizowałem id zamówienia na podstawie zamówienia apilo.com</p>';
|
||||||
}
|
}
|
||||||
elseif ( $response['message'] == 'Validation error' )
|
elseif ( isset($response['message']) && $response['message'] == 'Validation error' )
|
||||||
{
|
{
|
||||||
// sprawdzanie czy błąd dotyczy duplikatu idExternal
|
|
||||||
$is_duplicate_idexternal = false;
|
$is_duplicate_idexternal = false;
|
||||||
if ( isset( $response['errors'] ) and is_array( $response['errors'] ) )
|
if ( isset( $response['errors'] ) && is_array( $response['errors'] ) )
|
||||||
{
|
{
|
||||||
foreach ( $response['errors'] as $error )
|
foreach ( $response['errors'] as $error )
|
||||||
{
|
{
|
||||||
if ( isset( $error['field'] ) and $error['field'] == 'idExternal' and
|
if ( isset( $error['field'] ) && $error['field'] == 'idExternal' &&
|
||||||
( strpos( $error['message'], 'już wykorzystywana' ) !== false or
|
( strpos( $error['message'], 'już wykorzystywana' ) !== false ||
|
||||||
strpos( $error['message'], 'already' ) !== false ) )
|
strpos( $error['message'], 'already' ) !== false ) )
|
||||||
{
|
{
|
||||||
$is_duplicate_idexternal = true;
|
$is_duplicate_idexternal = true;
|
||||||
@@ -449,7 +463,6 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
|
|
||||||
if ( $is_duplicate_idexternal )
|
if ( $is_duplicate_idexternal )
|
||||||
{
|
{
|
||||||
// próba pobrania zamówienia z Apilo na podstawie idExternal
|
|
||||||
$ch_get = curl_init();
|
$ch_get = curl_init();
|
||||||
curl_setopt( $ch_get, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/?idExternal=" . $order['id'] );
|
curl_setopt( $ch_get, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/?idExternal=" . $order['id'] );
|
||||||
curl_setopt( $ch_get, CURLOPT_RETURNTRANSFER, true );
|
curl_setopt( $ch_get, CURLOPT_RETURNTRANSFER, true );
|
||||||
@@ -462,22 +475,16 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
|
|
||||||
$get_response_data = json_decode( $get_response, true );
|
$get_response_data = json_decode( $get_response, true );
|
||||||
|
|
||||||
if ( isset( $get_response_data['list'] ) and count( $get_response_data['list'] ) > 0 )
|
if ( isset( $get_response_data['list'] ) && count( $get_response_data['list'] ) > 0 )
|
||||||
{
|
{
|
||||||
$apilo_order_id = $get_response_data['list'][0]['id'];
|
$apilo_order_id = $get_response_data['list'][0]['id'];
|
||||||
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
|
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
|
||||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Duplikat idExternal - pobrano apilo_order_id: ' . $apilo_order_id, [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Duplikat idExternal - pobrano apilo_order_id: ' . $apilo_order_id, [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
|
||||||
echo '<p>Zamówienie już istnieje w Apilo. Zaktualizowano ID zamówienia: ' . $apilo_order_id . '</p>';
|
echo '<p>Zamówienie już istnieje w Apilo. Zaktualizowano ID zamówienia: ' . $apilo_order_id . '</p>';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
echo '<pre>';
|
|
||||||
echo print_r( $response, true );
|
|
||||||
echo print_r( $postData, true );
|
|
||||||
echo '</pre>';
|
|
||||||
|
|
||||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd: duplikat idExternal, ale nie znaleziono zamówienia w Apilo', [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd: duplikat idExternal, ale nie znaleziono zamówienia w Apilo', [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
|
||||||
|
|
||||||
$email_data = print_r( $response, true );
|
$email_data = print_r( $response, true );
|
||||||
$email_data .= print_r( $postData, true );
|
$email_data .= print_r( $postData, true );
|
||||||
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com - nie znaleziono zamówienia', $email_data );
|
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com - nie znaleziono zamówienia', $email_data );
|
||||||
@@ -485,36 +492,154 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
echo '<pre>';
|
|
||||||
echo print_r( $response, true );
|
|
||||||
echo print_r( $postData, true );
|
|
||||||
echo '</pre>';
|
|
||||||
|
|
||||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd walidacji wysyłania zamówienia do Apilo', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd walidacji wysyłania zamówienia do Apilo', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
||||||
|
|
||||||
$email_data = print_r( $response, true );
|
$email_data = print_r( $response, true );
|
||||||
$email_data .= print_r( $postData, true );
|
$email_data .= print_r( $postData, true );
|
||||||
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com', $email_data );
|
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com', $email_data );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
elseif ( $http_code_send >= 400 || !isset( $response['id'] ) )
|
||||||
|
{
|
||||||
|
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => -1 ], [ 'id' => $order['id'] ] );
|
||||||
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd wysyłania zamówienia do Apilo (HTTP ' . $http_code_send . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
||||||
|
$email_data = 'HTTP Code: ' . $http_code_send . "\n\n";
|
||||||
|
$email_data .= print_r( $response, true );
|
||||||
|
$email_data .= print_r( $postData, true );
|
||||||
|
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia #' . $order['id'] . ' do apilo.com (HTTP ' . $http_code_send . ')', $email_data );
|
||||||
|
echo '<p>Błąd wysyłania zamówienia do apilo.com: ID: ' . $order['id'] . ' (HTTP ' . $http_code_send . ')</p>';
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $response['id'] ], [ 'id' => $order['id'] ] );
|
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $response['id'] ], [ 'id' => $order['id'] ] );
|
||||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie wysłane do Apilo (apilo_order_id: ' . $response['id'] . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie wysłane do Apilo (apilo_order_id: ' . $response['id'] . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
||||||
echo '<p>Wysłałem zamówienie do apilo.com: ID: ' . $order['id'] . ' - ' . $response['id'] . '</p>';
|
echo '<p>Wysłałem zamówienie do apilo.com: ID: ' . $order['id'] . ' - ' . $response['id'] . '</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// sprawdzanie statusów zamówień w apilo.com jeżeli zamówienie nie jest zrealizowane, anulowane lub nieodebrane
|
return true;
|
||||||
if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_settings['access-token'] and $apilo_settings['sync_orders_date_start'] <= date( 'Y-m-d H:i:s' ) )
|
});
|
||||||
{
|
|
||||||
$orders = $mdb -> query( 'SELECT id, apilo_order_id, apilo_order_status_date, number FROM pp_shop_orders WHERE apilo_order_id IS NOT NULL AND ( status != 6 AND status != 8 AND status != 9 ) AND ( apilo_order_status_date IS NULL OR apilo_order_status_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_order_status_date ASC LIMIT 5' ) -> fetchAll( \PDO::FETCH_ASSOC );
|
// 3. Apilo sync payment (event-driven — enqueued by OrderAdminService)
|
||||||
|
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SYNC_PAYMENT, function($payload) use ($mdb, $orderRepo, $orderAdminService) {
|
||||||
|
$order_id = (int)($payload['order_id'] ?? 0);
|
||||||
|
if ( $order_id <= 0 ) return true;
|
||||||
|
|
||||||
|
$order = $orderRepo->findRawById( $order_id );
|
||||||
|
if ( !$order ) return true;
|
||||||
|
|
||||||
|
if ( empty($order['apilo_order_id']) ) return false; // retry — awaiting apilo_order_id
|
||||||
|
|
||||||
|
if ( (int)$order['paid'] !== 1 ) return true; // not paid — nothing to sync
|
||||||
|
|
||||||
|
return $orderAdminService->syncApiloPayment( $order );
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Apilo sync status (event-driven — enqueued by OrderAdminService)
|
||||||
|
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SYNC_STATUS, function($payload) use ($mdb, $orderRepo, $orderAdminService) {
|
||||||
|
$order_id = (int)($payload['order_id'] ?? 0);
|
||||||
|
$status = isset($payload['status']) ? (int)$payload['status'] : null;
|
||||||
|
if ( $order_id <= 0 || $status === null ) return true;
|
||||||
|
|
||||||
|
$order = $orderRepo->findRawById( $order_id );
|
||||||
|
if ( !$order ) return true;
|
||||||
|
|
||||||
|
if ( empty($order['apilo_order_id']) ) return false; // retry — awaiting apilo_order_id
|
||||||
|
|
||||||
|
return $orderAdminService->syncApiloStatus( $order, $status );
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Apilo product sync
|
||||||
|
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_PRODUCT_SYNC, function($payload) use ($mdb, $integrationsRepository) {
|
||||||
|
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||||
|
if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_products'] || !$apilo_settings['access-token'] ) return true;
|
||||||
|
|
||||||
|
$stmt = $mdb->query( 'SELECT id, apilo_product_id, apilo_get_data_date, apilo_product_name FROM pp_shop_products WHERE apilo_product_id IS NOT NULL AND apilo_product_id != 0 AND ( apilo_get_data_date IS NULL OR apilo_get_data_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_get_data_date ASC LIMIT 1' );
|
||||||
|
$result = $stmt ? $stmt->fetch( \PDO::FETCH_ASSOC ) : null;
|
||||||
|
if ( !$result ) return true;
|
||||||
|
|
||||||
|
$access_token = $integrationsRepository->apiloGetAccessToken();
|
||||||
|
$url = 'https://projectpro.apilo.com/rest/api/warehouse/product/' . $result['apilo_product_id'] . '/';
|
||||||
|
$curl = curl_init( $url );
|
||||||
|
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
|
||||||
|
curl_setopt( $curl, CURLOPT_HTTPHEADER, [
|
||||||
|
"Authorization: Bearer " . $access_token,
|
||||||
|
"Accept: application/json"
|
||||||
|
] );
|
||||||
|
|
||||||
|
$response = curl_exec( $curl );
|
||||||
|
if ( $response === false ) return false;
|
||||||
|
|
||||||
|
$responseData = json_decode( $response, true );
|
||||||
|
if ( !is_array( $responseData ) || !isset( $responseData['quantity'] ) ) return false;
|
||||||
|
|
||||||
|
$mdb->update( 'pp_shop_products', [ 'quantity' => $responseData['quantity'] ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||||
|
$mdb->update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithoutTax'], 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithTax'], 2 ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||||
|
$mdb->update( 'pp_shop_products', [ 'apilo_get_data_date' => date( 'Y-m-d H:i:s' ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||||
|
\Shared\Helpers\Helpers::clear_product_cache( (int)$result['id'] );
|
||||||
|
|
||||||
|
echo '<p>Zaktualizowałem dane produktu (APILO) <b>' . $result['apilo_product_name'] . ' #' . $result['id'] . '</b></p>';
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Apilo pricelist sync
|
||||||
|
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_PRICELIST_SYNC, function($payload) use ($mdb, $integrationsRepository) {
|
||||||
|
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||||
|
if ( !$apilo_settings['enabled'] || !$apilo_settings['access-token'] ) return true;
|
||||||
|
|
||||||
|
$access_token = $integrationsRepository->apiloGetAccessToken();
|
||||||
|
$url = 'https://projectpro.apilo.com/rest/api/warehouse/price-calculated/?price=' . $apilo_settings['pricelist_id'];
|
||||||
|
|
||||||
|
$curl = curl_init( $url );
|
||||||
|
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
|
||||||
|
curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, "GET" );
|
||||||
|
curl_setopt( $curl, CURLOPT_HTTPHEADER, [
|
||||||
|
"Authorization: Bearer " . $access_token,
|
||||||
|
"Accept: application/json",
|
||||||
|
"Content-Type: application/json"
|
||||||
|
] );
|
||||||
|
|
||||||
|
$response = curl_exec( $curl );
|
||||||
|
if ( $response === false ) return false;
|
||||||
|
|
||||||
|
$responseData = json_decode( $response, true );
|
||||||
|
if ( !is_array( $responseData ) ) return false;
|
||||||
|
|
||||||
|
if ( isset($responseData['list']) && $responseData['list'] )
|
||||||
|
{
|
||||||
|
foreach ( $responseData['list'] as $product_price )
|
||||||
|
{
|
||||||
|
if ( $product_price['customPriceWithTax'] )
|
||||||
|
{
|
||||||
|
$price_brutto = $product_price['customPriceWithTax'];
|
||||||
|
$vat = $mdb->get( 'pp_shop_products', 'vat', [ 'apilo_product_id' => $product_price['product'] ] );
|
||||||
|
$price_netto = $price_brutto / ( ( 100 + $vat ) / 100 );
|
||||||
|
|
||||||
|
$mdb->update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $price_netto, 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $price_brutto, 2 ) ], [ 'apilo_product_id' => $product_price['product'] ] );
|
||||||
|
$product_id = $mdb->get( 'pp_shop_products', 'id', [ 'apilo_product_id' => $product_price['product'] ] );
|
||||||
|
|
||||||
|
( new \Domain\Product\ProductRepository( $mdb ) )->updateCombinationPricesFromBase( (int)$product_id, $price_brutto, $vat, null );
|
||||||
|
\Shared\Helpers\Helpers::clear_product_cache( (int)$product_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$integrationsRepository->saveSetting( 'apilo', 'pricelist_update_date', date( 'Y-m-d H:i:s' ) );
|
||||||
|
echo '<p>Zaktualizowałem ceny produktów (APILO)</p>';
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Apilo status poll
|
||||||
|
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_STATUS_POLL, function($payload) use ($mdb, $integrationsRepository, $orderRepo, $orderAdminService) {
|
||||||
|
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||||
|
if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_orders'] || !$apilo_settings['access-token'] ) return true;
|
||||||
|
|
||||||
|
$stmt = $mdb->query( 'SELECT id, apilo_order_id, apilo_order_status_date, number FROM pp_shop_orders WHERE apilo_order_id IS NOT NULL AND ( status != 6 AND status != 8 AND status != 9 ) AND ( apilo_order_status_date IS NULL OR apilo_order_status_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_order_status_date ASC LIMIT 5' );
|
||||||
|
$orders = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||||||
|
|
||||||
foreach ( $orders as $order )
|
foreach ( $orders as $order )
|
||||||
{
|
{
|
||||||
if ( $order['apilo_order_id'] )
|
if ( $order['apilo_order_id'] )
|
||||||
{
|
{
|
||||||
$access_token = $integrationsRepository -> apiloGetAccessToken();
|
$access_token = $integrationsRepository->apiloGetAccessToken();
|
||||||
$url = 'https://projectpro.apilo.com/rest/api/orders/' . $order['apilo_order_id'] . '/';
|
$url = 'https://projectpro.apilo.com/rest/api/orders/' . $order['apilo_order_id'] . '/';
|
||||||
|
|
||||||
$ch = curl_init( $url );
|
$ch = curl_init( $url );
|
||||||
@@ -528,70 +653,103 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
$http_code_poll = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
$http_code_poll = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||||
$responseData = json_decode( $response, true );
|
$responseData = json_decode( $response, true );
|
||||||
|
|
||||||
if ( $responseData['id'] and $responseData['status'] )
|
if ( isset($responseData['id']) && $responseData['id'] && isset($responseData['status']) && $responseData['status'] )
|
||||||
{
|
{
|
||||||
$shop_status_id = ( new \Domain\ShopStatus\ShopStatusRepository( $mdb ) )->getByIntegrationStatusId( 'apilo', (int)$responseData['status'] );
|
$shop_status_id = ( new \Domain\ShopStatus\ShopStatusRepository( $mdb ) )->getByIntegrationStatusId( 'apilo', (int)$responseData['status'] );
|
||||||
|
|
||||||
if ( $shop_status_id )
|
if ( $shop_status_id )
|
||||||
$orderAdminService->changeStatus( (int)$order['id'], $shop_status_id, false );
|
$orderAdminService->changeStatus( (int)$order['id'], $shop_status_id, false );
|
||||||
|
|
||||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'status_poll', (int)$order['id'], 'Status pobrany z Apilo (apilo_status: ' . $responseData['status'] . ', shop_status: ' . ($shop_status_id ?: 'brak mapowania') . ')', [ 'apilo_order_id' => $order['apilo_order_id'], 'http_code' => $http_code_poll, 'response' => $responseData ] );
|
\Domain\Integrations\ApiloLogger::log( $mdb, 'status_poll', (int)$order['id'], 'Status pobrany z Apilo (apilo_status: ' . $responseData['status'] . ', shop_status: ' . ($shop_status_id ? $shop_status_id : 'brak mapowania') . ')', [ 'apilo_order_id' => $order['apilo_order_id'], 'http_code' => $http_code_poll, 'response' => $responseData ] );
|
||||||
|
|
||||||
$orderRepo->updateApiloStatusDate( (int)$order['id'], date( 'Y-m-d H:i:s' ) );
|
$orderRepo->updateApiloStatusDate( (int)$order['id'], date( 'Y-m-d H:i:s' ) );
|
||||||
echo '<p>Zaktualizowałem status zamówienia <b>' . $order['number'] . '</b></p>';
|
echo '<p>Zaktualizowałem status zamówienia <b>' . $order['number'] . '</b></p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
/* zapisywanie historii cen produktów */
|
// 8. Price history
|
||||||
$results = $mdb -> select( 'pp_shop_products', [ 'id', 'price_brutto', 'price_brutto_promo' ], [ 'OR' => [ 'price_history_date[!]' => date( 'Y-m-d' ), 'price_history_date' => null ], 'ORDER' => [ 'price_history_date' => 'ASC' ], 'LIMIT' => 100 ] );
|
$processor->registerHandler( \Domain\CronJob\CronJobType::PRICE_HISTORY, function($payload) use ($mdb) {
|
||||||
foreach ( $results as $row )
|
$results = $mdb->select( 'pp_shop_products', [ 'id', 'price_brutto', 'price_brutto_promo' ], [ 'OR' => [ 'price_history_date[!]' => date( 'Y-m-d' ), 'price_history_date' => null ], 'ORDER' => [ 'price_history_date' => 'ASC' ], 'LIMIT' => 100 ] );
|
||||||
{
|
|
||||||
|
foreach ( $results as $row )
|
||||||
|
{
|
||||||
|
$price = $row['price_brutto_promo'] > 0 ? $row['price_brutto_promo'] : $row['price_brutto'];
|
||||||
if ( $price )
|
if ( $price )
|
||||||
{
|
{
|
||||||
$mdb -> insert( 'pp_shop_product_price_history', [
|
$mdb->insert( 'pp_shop_product_price_history', [
|
||||||
'id_product' => $row['id'],
|
'id_product' => $row['id'],
|
||||||
'price' => $row['price_brutto_promo'] > 0 ? $row['price_brutto_promo'] : $row['price_brutto'],
|
'price' => $price,
|
||||||
'date' => date( 'Y-m-d' )
|
'date' => date( 'Y-m-d' )
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
|
|
||||||
$mdb -> update( 'pp_shop_products', [ 'price_history_date' => date( 'Y-m-d' ) ], [ 'id' => $row['id'] ] );
|
$mdb->update( 'pp_shop_products', [ 'price_history_date' => date( 'Y-m-d' ) ], [ 'id' => $row['id'] ] );
|
||||||
|
$mdb->delete( 'pp_shop_product_price_history', [ 'date[<=]' => date( 'Y-m-d', strtotime( '-31 days', time() ) ) ] );
|
||||||
$mdb -> delete( 'pp_shop_product_price_history', [ 'date[<=]' => date( 'Y-m-d', strtotime( '-31 days', time() ) ) ] );
|
|
||||||
echo '<p>Zapisuję historyczną cenę dla produktu <b>#' . $row['id'] . '</b></p>';
|
echo '<p>Zapisuję historyczną cenę dla produktu <b>#' . $row['id'] . '</b></p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* parsowanie zamówień m.in. pod kątem najczęściej sprzedawanych razem produktów */
|
return true;
|
||||||
$orders = $mdb -> select( 'pp_shop_orders', 'id', [ 'parsed' => 0, 'LIMIT' => 1 ] );
|
});
|
||||||
foreach ( $orders as $order )
|
|
||||||
{
|
// 9. Order analysis
|
||||||
$products = $mdb -> select( 'pp_shop_order_products', 'product_id', [ 'order_id' => $order ] );
|
$processor->registerHandler( \Domain\CronJob\CronJobType::ORDER_ANALYSIS, function($payload) use ($mdb) {
|
||||||
|
$orders = $mdb->select( 'pp_shop_orders', 'id', [ 'parsed' => 0, 'LIMIT' => 1 ] );
|
||||||
|
foreach ( $orders as $order )
|
||||||
|
{
|
||||||
|
$products = $mdb->select( 'pp_shop_order_products', 'product_id', [ 'order_id' => $order ] );
|
||||||
foreach ( $products as $product1 )
|
foreach ( $products as $product1 )
|
||||||
{
|
{
|
||||||
if ( $parent_id = $mdb -> get( 'pp_shop_products', 'parent_id', [ 'id' => $product1 ] ) )
|
if ( $parent_id = $mdb->get( 'pp_shop_products', 'parent_id', [ 'id' => $product1 ] ) )
|
||||||
$product1 = $parent_id;
|
$product1 = $parent_id;
|
||||||
|
|
||||||
foreach ( $products as $product2 )
|
foreach ( $products as $product2 )
|
||||||
{
|
{
|
||||||
if ( $parent_id = $mdb -> get( 'pp_shop_products', 'parent_id', [ 'id' => $product2 ] ) )
|
if ( $parent_id = $mdb->get( 'pp_shop_products', 'parent_id', [ 'id' => $product2 ] ) )
|
||||||
$product2 = $parent_id;
|
$product2 = $parent_id;
|
||||||
|
|
||||||
if ( $product1 != $product2 )
|
if ( $product1 != $product2 )
|
||||||
{
|
{
|
||||||
$intersection_id = $mdb -> query( 'SELECT * FROM pp_shop_orders_products_intersection WHERE product_1_id = :product_1_id AND product_2_id = :product_2_id OR product_1_id = :product_2_id AND product_2_id = :product_1_id', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2 ] ) -> fetch( \PDO::FETCH_ASSOC );
|
$stmt = $mdb->query( 'SELECT * FROM pp_shop_orders_products_intersection WHERE product_1_id = :product_1_id AND product_2_id = :product_2_id OR product_1_id = :product_2_id AND product_2_id = :product_1_id', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2 ] );
|
||||||
|
$intersection_id = $stmt ? $stmt->fetch( \PDO::FETCH_ASSOC ) : null;
|
||||||
if ( $intersection_id )
|
if ( $intersection_id )
|
||||||
{
|
{
|
||||||
$mdb -> update( 'pp_shop_orders_products_intersection', [ 'count' => $intersection_id['count'] + 1 ], [ 'id' => $intersection_id['id'] ] );
|
$mdb->update( 'pp_shop_orders_products_intersection', [ 'count' => $intersection_id['count'] + 1 ], [ 'id' => $intersection_id['id'] ] );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$mdb -> insert( 'pp_shop_orders_products_intersection', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2, 'count' => 1 ] );
|
$mdb->insert( 'pp_shop_orders_products_intersection', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2, 'count' => 1 ] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$mdb -> update( 'pp_shop_orders', [ 'parsed' => 1 ], [ 'id' => $order ] );
|
$mdb->update( 'pp_shop_orders', [ 'parsed' => 1 ], [ 'id' => $order ] );
|
||||||
echo '<p>Parsuję zamówienie <b>#' . $order . '</b></p>';
|
echo '<p>Parsuję zamówienie <b>#' . $order . '</b></p>';
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 10. Google XML feed
|
||||||
|
$processor->registerHandler( \Domain\CronJob\CronJobType::GOOGLE_XML_FEED, function($payload) use ($mdb) {
|
||||||
|
( new \Domain\Product\ProductRepository( $mdb ) )->generateGoogleFeedXml();
|
||||||
|
echo '<p>Wygenerowano Google XML Feed</p>';
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 11. TrustMate invitation — handled by separate cron-turstmate.php (requires browser context)
|
||||||
|
$processor->registerHandler( \Domain\CronJob\CronJobType::TRUSTMATE_INVITATION, function($payload) use ($config) {
|
||||||
|
if ( !isset($config['trustmate']['enabled']) || !$config['trustmate']['enabled'] ) return true;
|
||||||
|
// TrustMate requires browser context (JavaScript). Handled by cron-turstmate.php.
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Run processor
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
$result = $processor->run( 20 );
|
||||||
|
|
||||||
|
echo '<hr>';
|
||||||
|
echo '<p><small>CronJob stats: scheduled=' . $result['scheduled'] . ', processed=' . $result['processed'] . ', succeeded=' . $result['succeeded'] . ', failed=' . $result['failed'] . ', skipped=' . $result['skipped'] . '</small></p>';
|
||||||
|
|||||||
73
docs/API.md
73
docs/API.md
@@ -219,6 +219,7 @@ Odpowiedz:
|
|||||||
"set_id": null,
|
"set_id": null,
|
||||||
"product_unit_id": 1,
|
"product_unit_id": 1,
|
||||||
"producer_id": 3,
|
"producer_id": 3,
|
||||||
|
"producer_name": "Nike",
|
||||||
"date_add": "2026-01-15 10:00:00",
|
"date_add": "2026-01-15 10:00:00",
|
||||||
"date_modify": "2026-02-19 12:00:00",
|
"date_modify": "2026-02-19 12:00:00",
|
||||||
"languages": {
|
"languages": {
|
||||||
@@ -237,7 +238,8 @@ Odpowiedz:
|
|||||||
"tab_description_1": null,
|
"tab_description_1": null,
|
||||||
"tab_name_2": null,
|
"tab_name_2": null,
|
||||||
"tab_description_2": null,
|
"tab_description_2": null,
|
||||||
"canonical": null
|
"canonical": null,
|
||||||
|
"security_information": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"images": [
|
"images": [
|
||||||
@@ -253,6 +255,9 @@ Odpowiedz:
|
|||||||
"value_names": {"pl": "Czerwony", "en": "Red"}
|
"value_names": {"pl": "Czerwony", "en": "Red"}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"custom_fields": [
|
||||||
|
{"name": "Napis na koszulce", "type": "text", "is_required": 1}
|
||||||
|
],
|
||||||
"variants": [
|
"variants": [
|
||||||
{
|
{
|
||||||
"id": 101,
|
"id": 101,
|
||||||
@@ -297,11 +302,15 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"categories": [1, 5],
|
"categories": [1, 5],
|
||||||
"products_related": [10, 20]
|
"products_related": [10, 20],
|
||||||
|
"custom_fields": [
|
||||||
|
{"name": "Napis na koszulce", "type": "text", "is_required": 1}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Wymagane: `languages` (min. 1 jezyk z `name`) oraz `price_brutto`.
|
Wymagane: `languages` (min. 1 jezyk z `name`) oraz `price_brutto`.
|
||||||
|
`custom_fields` — opcjonalne; kazdy element wymaga `name`, `type` (domyslnie `text`), `is_required` (0/1).
|
||||||
|
|
||||||
Odpowiedz (HTTP 201):
|
Odpowiedz (HTTP 201):
|
||||||
```json
|
```json
|
||||||
@@ -431,6 +440,38 @@ Odpowiedz:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Kategorie
|
||||||
|
|
||||||
|
#### Lista kategorii
|
||||||
|
```
|
||||||
|
GET api.php?endpoint=categories&action=list
|
||||||
|
```
|
||||||
|
|
||||||
|
Zwraca plaska liste wszystkich aktywnych kategorii w domyslnym jezyku sklepu.
|
||||||
|
|
||||||
|
Odpowiedz:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"data": {
|
||||||
|
"categories": [
|
||||||
|
{"id": 1, "parent_id": null, "title": "Kategoria glowna"},
|
||||||
|
{"id": 3, "parent_id": 1, "title": "Podkategoria A"},
|
||||||
|
{"id": 5, "parent_id": 1, "title": "Podkategoria B"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pola odpowiedzi:
|
||||||
|
| Pole | Typ | Opis |
|
||||||
|
|------|-----|------|
|
||||||
|
| `id` | int | ID kategorii |
|
||||||
|
| `parent_id` | int\|null | ID kategorii nadrzednej (null = kategoria glowna) |
|
||||||
|
| `title` | string | Nazwa w domyslnym jezyku; fallback na inny jezyk jesli brak tlumaczenia |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Slowniki
|
### Slowniki
|
||||||
|
|
||||||
#### Lista statusow zamowien
|
#### Lista statusow zamowien
|
||||||
@@ -468,6 +509,31 @@ GET api.php?endpoint=dictionaries&action=attributes
|
|||||||
|
|
||||||
Zwraca aktywne atrybuty z wartosciami i wielojezycznymi nazwami.
|
Zwraca aktywne atrybuty z wartosciami i wielojezycznymi nazwami.
|
||||||
|
|
||||||
|
#### Znajdz lub utworz producenta
|
||||||
|
```
|
||||||
|
POST api.php?endpoint=dictionaries&action=ensure_producer
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Nike"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Zwraca istniejacego producenta po nazwie lub tworzy nowego. Uzyc przed tworzeniem produktu, jesli producent moze nie istniec.
|
||||||
|
|
||||||
|
Odpowiedz:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"data": {
|
||||||
|
"id": 5,
|
||||||
|
"created": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`created: true` gdy producent zostal nowo dodany, `false` gdy juz istnial.
|
||||||
|
|
||||||
Odpowiedz:
|
Odpowiedz:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -514,4 +580,5 @@ UPDATE pp_settings SET value = 'twoj-klucz-api' WHERE param = 'api_key';
|
|||||||
- Kontrolery: `autoload/api/Controllers/`
|
- Kontrolery: `autoload/api/Controllers/`
|
||||||
- `OrdersApiController` — zamowienia (5 akcji)
|
- `OrdersApiController` — zamowienia (5 akcji)
|
||||||
- `ProductsApiController` — produkty (8 akcji: list, get, create, update, variants, create_variant, update_variant, delete_variant)
|
- `ProductsApiController` — produkty (8 akcji: list, get, create, update, variants, create_variant, update_variant, delete_variant)
|
||||||
- `DictionariesApiController` — slowniki (4 akcje: statuses, transports, payment_methods, attributes)
|
- `DictionariesApiController` — slowniki (5 akcji: statuses, transports, payment_methods, attributes, ensure_producer)
|
||||||
|
- `CategoriesApiController` — kategorie (1 akcja: list)
|
||||||
|
|||||||
@@ -4,6 +4,184 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ver. 0.331 (2026-03-01) - Bugfix: strona produktu używała layoutu kategorii zamiast domyślnego
|
||||||
|
|
||||||
|
- **FIX**: `LayoutsRepository::getProductLayout()` — fallback gdy produkt i jego kategorie nie mają przypisanego layoutu zmieniany z `categories_default = 1` na `status = 1`; wcześniej produkty bez layoutu pobierały szablon "Podstrony - kategorie" zamiast właściwego domyślnego
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.330 (2026-02-27) - Eliminacja htaccess.conf — wszystkie trasy URL w pp_routes
|
||||||
|
|
||||||
|
- **REFACTOR**: `Helpers::htacces()` — generowanie `.htaccess` w całości z PHP (usunięty `file_get_contents('htaccess.conf')` i placeholder `{HTACCESS_CACHE}`)
|
||||||
|
- **NEW**: 32 statyczne trasy systemowe wstawiane do `pp_routes` z `type='system'` przy każdym `htacces()` (koszyk, logowanie, wylogowanie, panel klienta, newsletter, zamówienia, płatności, moduły AJAX: shopBasket/shopClient/shopProduct/shopCoupon/search)
|
||||||
|
- **NEW**: Dynamiczne trasy językowe i producentów (producenci + per-producent z paginacją) przenoszone do `pp_routes` zamiast `.htaccess`
|
||||||
|
- **NEW**: Kolumna `type VARCHAR(20) NULL` w `pp_routes` — `NULL` dla encji, `'system'` dla tras systemowych
|
||||||
|
- **REMOVED**: `libraries/htaccess.conf` — plik szablonu usunięty, treść wbudowana w PHP
|
||||||
|
- **PERF**: Invalidacja cache Redis `pp_routes:all` po każdym `htacces()` — świeże trasy przy kolejnym żądaniu
|
||||||
|
- **MIGRATION**: `migrations/0.329.sql` (dodano `type` column)
|
||||||
|
- **DOCS**: `docs/DATABASE_STRUCTURE.md` — zaktualizowana sekcja `pp_routes` o kolumnę `type`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.329 (2026-02-27) - Routing kategorii, stron i artykułów przez pp_routes
|
||||||
|
|
||||||
|
- **REFACTOR**: `index.php` — blok routingu przez `pp_routes` przeniesiony PRZED `checkUrlParams()` (poprawna kolejność: lang/a=page dostępne w checkUrlParams)
|
||||||
|
- **PERF**: Cache Redis dla tras (`pp_routes:all`, TTL 86400s) w `index.php` — jeden SELECT na 24h zamiast przy każdym żądaniu
|
||||||
|
- **NEW**: Kategorie, strony i artykuły zapisywane do `pp_routes` zamiast `.htaccess` w `Helpers::htacces()`
|
||||||
|
- **NEW**: `CategoryRepository::categoryDelete()` — usuwa powiązane `pp_routes` przed odświeżeniem
|
||||||
|
- **NEW**: `PagesRepository::pageDelete()` — usuwa powiązane `pp_routes`
|
||||||
|
- **NEW**: `ArticleRepository::archive()` i `deletePermanently()` — usuwa powiązane `pp_routes`
|
||||||
|
- **MIGRATION**: `migrations/0.329.sql` — `ALTER TABLE pp_routes ADD COLUMN category_id, page_id, article_id`
|
||||||
|
- **TESTS**: Zaktualizowane `CategoryRepositoryTest` i `ArticleRepositoryTest` (nowe asercje na `pp_routes` delete)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.328 (2026-02-27) - Ikona kopiowania wartości atrybutów w szczegółach zamówienia
|
||||||
|
|
||||||
|
- **NEW**: `order-details-custom-script.php` — JS parsuje `.atributes` div i wstrzykuje przycisk `fa-copy` przy każdej wartości atrybutu
|
||||||
|
- **UX**: Kliknięcie kopiuje wartość do schowka (Clipboard API + fallback execCommand), ikona zmienia się na `fa-check` z zielonym tłem przez 1,5s
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.327 (2026-02-27) - Masowe usuwanie w archiwum produktów
|
||||||
|
|
||||||
|
- **NEW**: `ProductArchiveController::bulk_delete_permanent()` — endpoint POST `product_archive/bulk_delete_permanent/`, przyjmuje `ids[]`, usuwa każdy produkt przez `ProductRepository::delete()`, zwraca JSON `{success, deleted, errors[]}`
|
||||||
|
- **UX**: Kolumna checkboxów w liście archiwum produktów + pasek akcji masowych z licznikiem zaznaczonych
|
||||||
|
- **UX**: "Zaznacz wszystkie" w nagłówku tabeli (wstrzyknięty via JS), dialog potwierdzenia przed masowym usunięciem
|
||||||
|
- **TEST**: 2 nowe testy w `ProductArchiveControllerTest` — weryfikacja istnienia i sygnatury `bulk_delete_permanent`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.326 (2026-02-27) - API: endpoint categories/list
|
||||||
|
|
||||||
|
- **NEW**: `api\Controllers\CategoriesApiController` — nowy kontroler API z akcją `list`
|
||||||
|
- **NEW**: Endpoint `GET api.php?endpoint=categories&action=list` — zwraca płaską listę aktywnych kategorii (id, parent_id, title) w domyślnym języku sklepu
|
||||||
|
- **FIX**: Usunięto zbędny parametr w `CategoryRepository`, eliminacja N+1 queries w categories/list przez bulk-fetch tytułów
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.325 (2026-02-27) - Fix changelog encoding + limit wyświetlania
|
||||||
|
|
||||||
|
- **FIX**: `updates/changelog.php` — naprawione krzaczki (mojibake) w polskich znakach; dane odbudowane z plików manifest
|
||||||
|
- **NEW**: `updates/changelog-data.html` — czyste dane changelog oddzielone od logiki PHP
|
||||||
|
- **REFACTOR**: `updates/changelog.php` — konwersja ze statycznego HTML na skrypt PHP: `Content-Type: utf-8`, parsowanie wpisów, filtrowanie po wersji
|
||||||
|
- **NEW**: Parametr `?ver=X.XXX` — ogranicza changelog do 5 wersji wstecz od wersji instancji
|
||||||
|
- **UPDATE**: `admin/templates/update/main-view.php` — przekazuje `?ver=` do URL changelog
|
||||||
|
- **UPDATE**: `build-update.ps1` — nowe wpisy dopisywane do `changelog-data.html` zamiast `changelog.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.324 (2026-02-27) - System kolejki zadań cron
|
||||||
|
|
||||||
|
- **NEW**: `Domain\CronJob\CronJobType` — stałe typów zadań, priorytetów, statusów, exponential backoff
|
||||||
|
- **NEW**: `Domain\CronJob\CronJobRepository` — CRUD na `pp_cron_jobs` + `pp_cron_schedules` (enqueue, fetchNext, markCompleted, markFailed, hasPendingJob, cleanup, recoverStuck, getDueSchedules, touchSchedule)
|
||||||
|
- **NEW**: `Domain\CronJob\CronJobProcessor` — orkiestracja: rejestracja handlerów, tworzenie scheduled jobs, przetwarzanie kolejki z priorytetami i retry/backoff
|
||||||
|
- **NEW**: Tabele `pp_cron_jobs` i `pp_cron_schedules` — kolejka zadań z priorytetami, exponential backoff, harmonogram cykliczny
|
||||||
|
- **REFACTOR**: `cron.php` — zastąpienie monolitycznego ~550 linii orkiestratorem z CronJobProcessor i zarejestrowanymi handlerami
|
||||||
|
- **REFACTOR**: `OrderAdminService::queueApiloSync()` — kolejkowanie przez `CronJobRepository::enqueue()` zamiast pliku JSON
|
||||||
|
- **REFACTOR**: `OrderAdminService::syncApiloPayment()`, `syncApiloStatus()` — zmiana z private na public (używane przez handlery cron)
|
||||||
|
- **REMOVED**: `OrderAdminService::processApiloSyncQueue()`, `loadApiloSyncQueue()`, `saveApiloSyncQueue()`, `apiloSyncQueuePath()`, stała `APILO_SYNC_QUEUE_FILE`
|
||||||
|
- **NEW**: Jednorazowa migracja JSON queue → DB w cron.php (automatyczna przy pierwszym uruchomieniu)
|
||||||
|
- **SECURITY**: `cron.php` — ochrona endpointu: wymaga `$config['cron_key']` w URL (`?key=...`) lub trybu CLI
|
||||||
|
- **FIX**: `CronJobRepository::fetchNext()` — re-SELECT po UPDATE eliminuje race condition przy równoległych workerach
|
||||||
|
- **FIX**: `cron.php` — null check dla `$mdb->query()` przed `->fetch()` / `->fetchAll()` (3 miejsca)
|
||||||
|
- **FIX**: `cron.php` — walidacja odpowiedzi curl w APILO_PRODUCT_SYNC i APILO_PRICELIST_SYNC (zapobiega zapisaniu null do bazy)
|
||||||
|
- **FIX**: DI wiring — `CronJobRepository` przekazywany do `OrderAdminService` we wszystkich 4 punktach: `admin\App`, `api\ApiRouter`, `front\App`, `cron.php`
|
||||||
|
- **TESTS**: 41 nowych testów CronJob (CronJobTypeTest, CronJobRepositoryTest, CronJobProcessorTest)
|
||||||
|
- **MIGRATION**: `migrations/0.324.sql`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.323 (2026-02-24) - Import zdjęć, trwałe usuwanie, fix API upload
|
||||||
|
|
||||||
|
- **FIX**: `IntegrationsRepository::shopproImportProduct()` — kompletny refactor importu zdjęć: walidacja HTTP response, curl timeouty, bezpieczna budowa URL, szczegółowy log do `logs/shoppro-import-debug.log` i `error_log`, czytelny komunikat z wynikiem
|
||||||
|
- **FIX**: `ProductRepository::saveProduct()` — `saveCustomFields()` wywoływane tylko gdy klucz `custom_field_name` istnieje w danych (partial update przez API nie czyści custom fields)
|
||||||
|
- **FIX**: `ProductRepository::delete()` — usuwanie rekordów z `pp_shop_products_custom_fields` przy kasowaniu produktu
|
||||||
|
- **FIX**: `ProductsApiController::upload_image()` — poprawka ścieżki uploadu (`upload/` zamiast `../upload/` — api.php działa z rootu projektu)
|
||||||
|
- **NEW**: `ProductArchiveController::delete_permanent()` — trwałe usunięcie produktu z archiwum (wraz ze zdjęciami i załącznikami)
|
||||||
|
- **NEW**: Przycisk "Usuń trwale" w liście produktów archiwalnych z potwierdzeniem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.318 (2026-02-24) - ShopPRO export produktów + API endpoints
|
||||||
|
|
||||||
|
- **NEW**: `IntegrationsRepository::shopproExportProduct()` — eksport produktu do zdalnej instancji shopPRO: pola główne, tłumaczenia, custom fields, zdjęcia przez API (base64)
|
||||||
|
- **NEW**: `IntegrationsRepository::sendImageToShopproApi()` — wysyłka zdjęć do remote API shopPRO (endpoint `upload_image`) z base64
|
||||||
|
- **REFACTOR**: `shopproImportProduct()` — wydzielono `shopproDb()` i `missingShopproSetting()` jako prywatne helpery; dodano import `security_information`, `producer_id`, custom fields i `alt` zdjęcia
|
||||||
|
- **NEW**: `AttributeRepository::ensureAttributeForApi()` i `ensureAttributeValueForApi()` — idempotent find-or-create dla atrybutów i ich wartości (integracje API)
|
||||||
|
- **NEW**: API endpoint `POST /api.php?endpoint=dictionaries&action=ensure_attribute` — utwórz lub znajdź atrybut po nazwie i typie
|
||||||
|
- **NEW**: API endpoint `POST /api.php?endpoint=dictionaries&action=ensure_attribute_value` — utwórz lub znajdź wartość atrybutu po nazwie
|
||||||
|
- **NEW**: API endpoint `POST /api.php?endpoint=products&action=upload_image` — przyjmuje zdjęcie produktu jako base64 JSON, zapisuje plik i rekord w `pp_shop_products_images`
|
||||||
|
- **NEW**: `IntegrationsController::shoppro_product_export()` — akcja admina eksportująca produkt do shopPRO
|
||||||
|
- **NEW**: Przycisk "Eksportuj do shopPRO" w liście produktów (widoczny gdy shopPRO enabled)
|
||||||
|
- **NEW**: Pole "API key" w ustawieniach integracji shopPRO (`shoppro-settings.php`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.317 (2026-02-23) - Klucz API: przycisk generowania + fix zapisu
|
||||||
|
|
||||||
|
- **FIX**: `SettingsRepository::saveSettings()` — pole `api_key` brakowało w whiteliście zapisywanych pól, przez co wartość była tracona przy każdym zapisie (TRUNCATE + insert)
|
||||||
|
- **NEW**: Pole "Klucz API" w ustawieniach — przycisk "Generuj" do losowego 32-znakowego klucza alfanumerycznego, usunięto "(ordersPRO)" z nazwy
|
||||||
|
- **FIX**: `api.php` — routing API przeniesiony przed ładowanie globalnych settings (wczesne wyjście), obsługa błędów przez `\Throwable`
|
||||||
|
- **FIX**: `ApiRouter` — catch `\Throwable` zamiast `\Exception` dla pełniejszego łapania błędów
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.316 (2026-02-23) - Migracja brakującej kolumny type w custom fields
|
||||||
|
|
||||||
|
- **FIX**: Dodanie brakującej kolumny `type` w tabeli `pp_shop_products_custom_fields` — kolumna była używana w kodzie od v0.277 ale nigdy nie miała migracji ALTER TABLE, przez co instancje ze starszą bazą dostawały `PDOException: Column not found: 1054 Unknown column 'type'` przy zapisie produktu
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.315 (2026-02-23) - Fix listowania atrybutów w admin
|
||||||
|
|
||||||
|
- **FIX**: `AttributeRepository::listForAdmin()` — zapytanie COUNT dostawało parametr `:default_lang_id` którego nie miało w SQL, powodując `PDOException: SQLSTATE[HY093]: Invalid parameter number`. Parametr potrzebny tylko w głównym SELECT, nie w COUNT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.314 (2026-02-23) - Fix wyszukiwarki admin + title zamówienia
|
||||||
|
|
||||||
|
- **FIX**: Globalna wyszukiwarka w panelu admina przestała zwracać wyniki — dodano `Content-Type: application/json` i `Cache-Control: no-store` (zapobiega cache'owaniu przez proxy/CDN), zmiana AJAX z GET na POST, `fetchAll(PDO::FETCH_ASSOC)`, top-level try/catch z gwarantowaną odpowiedzią JSON
|
||||||
|
- **NEW**: `document.title` w widoku szczegółów zamówienia pokazuje numer zamówienia (np. "Zamówienie ZAM/123 - shopPro")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.313 (2026-02-23) - Fix sync płatności Apilo + logowanie
|
||||||
|
|
||||||
|
- **FIX**: `syncApiloPayment()` i `syncApiloStatus()` — `(int)` cast na `apilo_order_id` (format `"PPxxxxxx"`) dawał `0`, przez co metody pomijały sync z API Apilo. Zmiana na `empty()`
|
||||||
|
- **NEW**: Logowanie w `syncApiloPaymentIfNeeded()` i `syncApiloStatusIfNeeded()` — każda ścieżka decyzyjna (Apilo wyłączone, brak tokenu, brak `apilo_order_id`, sync nieudany) zapisuje wpis do `pp_log` z kontekstem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.312 (2026-02-23) - Fix krytycznych bugów integracji Apilo
|
||||||
|
|
||||||
|
- **FIX**: `curl_getinfo()` wywoływane po `curl_close()` — HTTP code zawsze wynosił 0, uniemożliwiając prawidłową obsługę odpowiedzi Apilo
|
||||||
|
- **FIX**: Nieskończona pętla wysyłania zamówienia — gdy Apilo zwracało błąd serwera, zamówienie nie dostawało `apilo_order_id` i było ponownie wybierane w każdym cyklu crona. Teraz błędne zamówienia oznaczane `apilo_order_id = -1` z powiadomieniem email
|
||||||
|
- **FIX**: Ceny produktów 0.00 PLN w Apilo — string `"0.00"` z MySQL jest truthy w PHP, więc ternary wybierał `price_brutto_promo` (0.00) zamiast `price_brutto`. Zmiana na `(float)... > 0`
|
||||||
|
- **FIX**: Walidacja cen przed wysyłką — zamówienia z zerowymi cenami produktów nie są wysyłane do Apilo (`apilo_order_id = -2`) z powiadomieniem email
|
||||||
|
- **FIX**: Niezainicjalizowana zmienna `$order_message` powodująca PHP warning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.311 (2026-02-23) - Fix race condition Apilo + persistence filtrów + poprawki cen
|
||||||
|
|
||||||
|
- **FIX**: Race condition — callback płatności przed wysłaniem zamówienia do Apilo nie synchronizował płatności (task trafiał w pustkę). Teraz `syncApiloPaymentIfNeeded` i `syncApiloStatusIfNeeded` kolejkują sync do retry gdy `apilo_order_id` jeszcze nie istnieje
|
||||||
|
- **FIX**: `processApiloSyncQueue` — zamówienia bez `apilo_order_id` były usuwane z kolejki bez synchronizacji. Teraz czekają (max 50 prób ~8h) aż cron wyśle zamówienie do Apilo
|
||||||
|
- **FIX**: Drugie wywołanie `processApiloSyncQueue` w cronie po wysyłce zamówień — sync płatności/statusów w tym samym cyklu
|
||||||
|
- **FIX**: Ceny w szczegółach zamówienia (admin + frontend) — gdy `price_brutto_promo` = 0 lub >= ceny regularnej, wyświetla cenę regularną zamiast 0 zł
|
||||||
|
- **NEW**: Persistence filtrów tabel w panelu admin — localStorage zapamiętuje ostatni widok (filtry, sortowanie, paginacja) i przywraca go przy powrocie do listy. Przycisk "Wyczyść" resetuje zapisany stan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ver. 0.310 (2026-02-23) - Logi integracji w panelu admin
|
||||||
|
|
||||||
|
- **NEW**: Zakładka "Logi" w sekcji Integracje — podgląd tabeli `pp_log` z paginacją, sortowaniem, filtrami (akcja, wiadomość, ID zamówienia) i rozwijalnym kontekstem JSON
|
||||||
|
- **NEW**: `IntegrationsRepository::getLogs()`, `deleteLog()`, `clearLogs()` — metody do obsługi logów
|
||||||
|
- **NEW**: `IntegrationsController::logs()`, `logs_clear()` — akcje kontrolera
|
||||||
|
- **NEW**: Przycisk "Wyczyść wszystkie logi" z potwierdzeniem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ver. 0.309 (2026-02-23) - ApiloLogger + cache-busting CSS/JS + poprawki UI
|
## ver. 0.309 (2026-02-23) - ApiloLogger + cache-busting CSS/JS + poprawki UI
|
||||||
|
|
||||||
- **NEW**: `ApiloLogger` — logowanie operacji Apilo do tabeli `pp_log` z kontekstem JSON (send_order, resend_order, payment_sync, status_sync, status_poll)
|
- **NEW**: `ApiloLogger` — logowanie operacji Apilo do tabeli `pp_log` z kontekstem JSON (send_order, resend_order, payment_sync, status_sync, status_poll)
|
||||||
|
|||||||
156
docs/CRON_QUEUE_PLAN.md
Normal file
156
docs/CRON_QUEUE_PLAN.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Plan: System kolejki zadań cron oparty o bazę danych
|
||||||
|
|
||||||
|
## Kontekst
|
||||||
|
|
||||||
|
Obecny system cron ma dwa problemy:
|
||||||
|
1. **Kolejka plikowa (JSON)** — sync płatności/statusów Apilo trzymany w `/temp/apilo-sync-queue.json` — kruchy, brak transakcji, ryzyko utraty danych
|
||||||
|
2. **Monolityczny cron.php** (~550 linii) — brak priorytetów, brak retry z backoff, brak centralnego zarządzania
|
||||||
|
|
||||||
|
Cel: Zastąpienie całego systemu cron tabelą `pp_cron_jobs` z priorytetami, retry/backoff i harmonogramem `pp_cron_schedules`.
|
||||||
|
|
||||||
|
## Nowe pliki
|
||||||
|
|
||||||
|
| Plik | Opis |
|
||||||
|
|------|------|
|
||||||
|
| `autoload/Domain/CronJob/CronJobType.php` | Stałe typów zadań i priorytetów |
|
||||||
|
| `autoload/Domain/CronJob/CronJobRepository.php` | CRUD na `pp_cron_jobs` + `pp_cron_schedules` |
|
||||||
|
| `autoload/Domain/CronJob/CronJobProcessor.php` | Orkiestracja: pobierz zadanie → wywołaj handler → obsłuż wynik |
|
||||||
|
| `tests/Unit/Domain/CronJob/CronJobTypeTest.php` | Testy stałych |
|
||||||
|
| `tests/Unit/Domain/CronJob/CronJobRepositoryTest.php` | Testy repozytorium |
|
||||||
|
| `tests/Unit/Domain/CronJob/CronJobProcessorTest.php` | Testy procesora |
|
||||||
|
| `migrations/0.315.sql` | CREATE TABLE + INSERT harmonogramów |
|
||||||
|
|
||||||
|
## Modyfikowane pliki
|
||||||
|
|
||||||
|
| Plik | Zmiana |
|
||||||
|
|------|--------|
|
||||||
|
| `cron.php` | Zastąpienie ~550 linii orchestratorem (~100 linii) z rejestracją handlerów |
|
||||||
|
| `cron/cron-xml.php` | Usunięcie — logika przeniesiona do handlera `google_xml_feed` |
|
||||||
|
| `cron-turstmate.php` | Usunięcie — logika przeniesiona do handlera `trustmate_invitation` |
|
||||||
|
| `autoload/Domain/Order/OrderAdminService.php` | `queueApiloSync()` → enqueue do DB; usunięcie metod plikowych; `syncApiloPayment()`/`syncApiloStatus()` → public |
|
||||||
|
| `tests/Unit/Domain/Order/OrderAdminServiceTest.php` | Refaktor testów kolejki: mock `CronJobRepository` zamiast pliku JSON |
|
||||||
|
| `docs/DATABASE_STRUCTURE.md` | Dodanie tabel `pp_cron_jobs`, `pp_cron_schedules` |
|
||||||
|
| `docs/CHANGELOG.md` | Wpis o nowym systemie |
|
||||||
|
|
||||||
|
## Schemat DB (`migrations/0.315.sql`)
|
||||||
|
|
||||||
|
### `pp_cron_jobs`
|
||||||
|
```sql
|
||||||
|
CREATE TABLE pp_cron_jobs (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
job_type VARCHAR(50) NOT NULL,
|
||||||
|
status ENUM('pending','processing','completed','failed','cancelled') NOT NULL DEFAULT 'pending',
|
||||||
|
priority TINYINT UNSIGNED NOT NULL DEFAULT 100, -- niższy = ważniejszy
|
||||||
|
payload TEXT NULL, -- JSON z danymi zadania
|
||||||
|
result TEXT NULL, -- JSON z wynikiem
|
||||||
|
attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
max_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 10,
|
||||||
|
last_error VARCHAR(500) NULL,
|
||||||
|
scheduled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
started_at DATETIME NULL,
|
||||||
|
completed_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_status_priority_scheduled (status, priority, scheduled_at),
|
||||||
|
INDEX idx_job_type (job_type),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `pp_cron_schedules`
|
||||||
|
```sql
|
||||||
|
CREATE TABLE pp_cron_schedules (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
job_type VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
interval_seconds INT UNSIGNED NOT NULL,
|
||||||
|
priority TINYINT UNSIGNED NOT NULL DEFAULT 100,
|
||||||
|
max_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 3,
|
||||||
|
payload TEXT NULL,
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
last_run_at DATETIME NULL,
|
||||||
|
next_run_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_enabled_next_run (enabled, next_run_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typy zadań i priorytety
|
||||||
|
|
||||||
|
| Typ | Priorytet | Harmonogram |
|
||||||
|
|-----|-----------|-------------|
|
||||||
|
| `apilo_token_keepalive` | 10 (krytyczny) | co 4 min |
|
||||||
|
| `apilo_send_order` | 50 (wysoki) | co 1 min |
|
||||||
|
| `apilo_sync_payment` | 50 (wysoki) | event-driven (enqueue przy zmianie) |
|
||||||
|
| `apilo_sync_status` | 50 (wysoki) | event-driven |
|
||||||
|
| `apilo_product_sync` | 100 (normalny) | co 10 min |
|
||||||
|
| `apilo_pricelist_sync` | 100 (normalny) | co 1h |
|
||||||
|
| `apilo_status_poll` | 100 (normalny) | co 10 min |
|
||||||
|
| `price_history` | 100 (normalny) | co 24h |
|
||||||
|
| `order_analysis` | 100 (normalny) | co 10 min |
|
||||||
|
| `trustmate_invitation` | 200 (niski) | co 10 min |
|
||||||
|
| `google_xml_feed` | 200 (niski) | co 1h |
|
||||||
|
|
||||||
|
## Architektura klas
|
||||||
|
|
||||||
|
### CronJobRepository — metody kluczowe
|
||||||
|
- `enqueue($jobType, $payload, $priority, $maxAttempts, $scheduledAt)` — dodaj do kolejki
|
||||||
|
- `fetchNext($limit)` — atomowe pobranie pending jobs (UPDATE WHERE status='pending')
|
||||||
|
- `markCompleted($jobId, $result)` / `markFailed($jobId, $error, $backoffSeconds)`
|
||||||
|
- `hasPendingJob($jobType, $payloadMatch)` — zapobiega duplikatom
|
||||||
|
- `cleanup($olderThanDays)` — GC starych wpisów
|
||||||
|
- `recoverStuck($olderThanMinutes)` — reset stuck "processing" jobs
|
||||||
|
- `getDueSchedules()` / `touchSchedule($id)` — harmonogram
|
||||||
|
|
||||||
|
### CronJobProcessor — orkiestracja
|
||||||
|
- `registerHandler($jobType, callable)` — rejestracja handlera
|
||||||
|
- `createScheduledJobs()` — tworzy jobs z harmonogramów których `next_run_at <= NOW`
|
||||||
|
- `processQueue($limit)` — pobierz + wywołaj handler + markCompleted/markFailed
|
||||||
|
- `run($limit)` — główna metoda: schedules + process
|
||||||
|
|
||||||
|
### Exponential backoff
|
||||||
|
```
|
||||||
|
Próba 1: 60s, Próba 2: 120s, Próba 3: 240s, ... max 3600s (1h)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zależność "order not yet in Apilo"
|
||||||
|
Handler `apilo_sync_payment`/`apilo_sync_status` sprawdza `apilo_order_id`. Jeśli brak → zwraca false → `markFailed()` z backoffem → zadanie wraca do kolejki. Max 50 prób.
|
||||||
|
|
||||||
|
## Nowy cron.php (schemat)
|
||||||
|
|
||||||
|
```php
|
||||||
|
$cronRepo = new \Domain\CronJob\CronJobRepository($mdb);
|
||||||
|
$processor = new \Domain\CronJob\CronJobProcessor($mdb, $cronRepo);
|
||||||
|
|
||||||
|
// Rejestracja handlerów (każdy to callable)
|
||||||
|
$processor->registerHandler('apilo_token_keepalive', function($payload) use ($integrationsRepo) { ... });
|
||||||
|
$processor->registerHandler('apilo_send_order', function($payload) use ($orderService, ...) { ... });
|
||||||
|
// ... inne handlery
|
||||||
|
|
||||||
|
$result = $processor->run(20);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zmiany w OrderAdminService
|
||||||
|
|
||||||
|
1. `queueApiloSync()` → `CronJobRepository::enqueue()` zamiast zapisu do pliku JSON
|
||||||
|
2. Usunięcie: `loadApiloSyncQueue()`, `saveApiloSyncQueue()`, `apiloSyncQueuePath()`, stała `APILO_SYNC_QUEUE_FILE`
|
||||||
|
3. `syncApiloPayment()`, `syncApiloStatus()` → zmiana z `private` na `public`
|
||||||
|
4. Jednorazowa migracja: odczyt JSON → insert do DB → usunięcie pliku
|
||||||
|
|
||||||
|
## Kolejność implementacji
|
||||||
|
|
||||||
|
1. Migracja SQL
|
||||||
|
2. `CronJobType.php`
|
||||||
|
3. `CronJobRepository.php` + testy
|
||||||
|
4. `CronJobProcessor.php` + testy
|
||||||
|
5. Modyfikacja `OrderAdminService` (queue → DB, public methods)
|
||||||
|
6. Jednorazowa migracja pliku JSON → DB
|
||||||
|
7. Nowy `cron.php` z handlerami (ekstrakcja logiki z bloków proceduralnych)
|
||||||
|
8. Aktualizacja testów OrderAdminService
|
||||||
|
9. Dokumentacja (DATABASE_STRUCTURE.md, CHANGELOG.md)
|
||||||
|
|
||||||
|
## Weryfikacja
|
||||||
|
|
||||||
|
1. Uruchomienie pełnego zestawu testów: `./test.ps1`
|
||||||
|
2. Sprawdzenie czy nowe testy CronJob* przechodzą
|
||||||
|
3. Sprawdzenie czy istniejące testy OrderAdminService przechodzą po refaktorze
|
||||||
|
4. Weryfikacja migracji SQL na pustej bazie
|
||||||
@@ -46,6 +46,17 @@ Zdjęcia produktów.
|
|||||||
| src | Ścieżka do pliku |
|
| src | Ścieżka do pliku |
|
||||||
| alt | Tekst alternatywny |
|
| alt | Tekst alternatywny |
|
||||||
|
|
||||||
|
## pp_shop_products_custom_fields
|
||||||
|
Dodatkowe pola produktów (custom fields).
|
||||||
|
|
||||||
|
| Kolumna | Opis |
|
||||||
|
|---------|------|
|
||||||
|
| id_additional_field | PK |
|
||||||
|
| id_product | FK do pp_shop_products |
|
||||||
|
| name | Nazwa pola |
|
||||||
|
| type | Typ pola (VARCHAR 30) |
|
||||||
|
| is_required | Czy wymagane (0/1) |
|
||||||
|
|
||||||
## pp_shop_products_categories
|
## pp_shop_products_categories
|
||||||
Przypisanie produktów do kategorii.
|
Przypisanie produktów do kategorii.
|
||||||
|
|
||||||
@@ -643,3 +654,74 @@ Tlumaczenia producentow (per jezyk). FK kaskadowe ON DELETE CASCADE.
|
|||||||
**Aktualizacja 2026-02-15 (ver. 0.273):** modul `/admin/shop_producer` korzysta z `Domain\Producer\ProducerRepository` przez `admin\Controllers\ShopProducerController`. Usunieto legacy `admin\controls\ShopProducer` i `admin\factory\ShopProducer`. `shop\Producer` dziala jako fasada do repozytorium.
|
**Aktualizacja 2026-02-15 (ver. 0.273):** modul `/admin/shop_producer` korzysta z `Domain\Producer\ProducerRepository` przez `admin\Controllers\ShopProducerController`. Usunieto legacy `admin\controls\ShopProducer` i `admin\factory\ShopProducer`. `shop\Producer` dziala jako fasada do repozytorium.
|
||||||
|
|
||||||
**Aktualizacja 2026-02-17 (ver. 0.291):** frontend `/shop_producer/*` korzysta z `Domain\Producer\ProducerRepository` przez `front\Controllers\ShopProducerController`; usunięto legacy `front\controls\ShopProducer` i `shop\Producer`.
|
**Aktualizacja 2026-02-17 (ver. 0.291):** frontend `/shop_producer/*` korzysta z `Domain\Producer\ProducerRepository` przez `front\Controllers\ShopProducerController`; usunięto legacy `front\controls\ShopProducer` i `shop\Producer`.
|
||||||
|
|
||||||
|
## pp_cron_jobs
|
||||||
|
Kolejka zadań cron z priorytetami i retry/backoff.
|
||||||
|
|
||||||
|
| Kolumna | Opis |
|
||||||
|
|---------|------|
|
||||||
|
| id | PK auto increment |
|
||||||
|
| job_type | Typ zadania (VARCHAR 50) — np. apilo_send_order, price_history |
|
||||||
|
| status | ENUM: pending, processing, completed, failed, cancelled |
|
||||||
|
| priority | TINYINT — niższy = ważniejszy (10=krytyczny, 50=wysoki, 100=normalny, 200=niski) |
|
||||||
|
| payload | JSON z danymi zadania (TEXT NULL) |
|
||||||
|
| result | JSON z wynikiem (TEXT NULL) |
|
||||||
|
| attempts | Liczba prób (SMALLINT) |
|
||||||
|
| max_attempts | Maksymalna liczba prób (SMALLINT, domyślnie 10) |
|
||||||
|
| last_error | Ostatni błąd (VARCHAR 500) |
|
||||||
|
| scheduled_at | Kiedy zadanie ma być uruchomione (DATETIME) |
|
||||||
|
| started_at | Kiedy rozpoczęto przetwarzanie (DATETIME NULL) |
|
||||||
|
| completed_at | Kiedy zakończono (DATETIME NULL) |
|
||||||
|
| created_at | Data utworzenia (DATETIME) |
|
||||||
|
| updated_at | Data ostatniej modyfikacji (DATETIME, ON UPDATE) |
|
||||||
|
|
||||||
|
**Indeksy:** idx_status_priority_scheduled (status, priority, scheduled_at), idx_job_type, idx_status
|
||||||
|
|
||||||
|
**Używane w:** `Domain\CronJob\CronJobRepository`, `Domain\CronJob\CronJobProcessor`
|
||||||
|
|
||||||
|
## pp_cron_schedules
|
||||||
|
Harmonogram cyklicznych zadań cron.
|
||||||
|
|
||||||
|
| Kolumna | Opis |
|
||||||
|
|---------|------|
|
||||||
|
| id | PK auto increment |
|
||||||
|
| job_type | Typ zadania (VARCHAR 50, UNIQUE) |
|
||||||
|
| interval_seconds | Interwał uruchomienia w sekundach |
|
||||||
|
| priority | Priorytet tworzonych zadań (TINYINT) |
|
||||||
|
| max_attempts | Maks. prób dla tworzonych zadań (SMALLINT) |
|
||||||
|
| payload | Opcjonalny payload JSON (TEXT NULL) |
|
||||||
|
| enabled | Czy harmonogram aktywny (TINYINT 1) |
|
||||||
|
| last_run_at | Ostatnie uruchomienie (DATETIME NULL) |
|
||||||
|
| next_run_at | Następne planowane uruchomienie (DATETIME NULL) |
|
||||||
|
| created_at | Data utworzenia (DATETIME) |
|
||||||
|
|
||||||
|
**Indeksy:** idx_enabled_next_run (enabled, next_run_at)
|
||||||
|
|
||||||
|
**Używane w:** `Domain\CronJob\CronJobRepository`, `Domain\CronJob\CronJobProcessor`
|
||||||
|
|
||||||
|
**Dodano w wersji 0.324.**
|
||||||
|
|
||||||
|
## pp_routes
|
||||||
|
Tabela tras URL — mapowanie wzorców URL (regex) na parametry GET. Zastępuje reguły `RewriteRule` w `.htaccess` dla wszystkich URL-i aplikacji: produktów, kategorii, stron, artykułów oraz systemowych (koszyk, logowanie, newsletter, itp.).
|
||||||
|
|
||||||
|
| Kolumna | Opis |
|
||||||
|
|---------|------|
|
||||||
|
| id | Klucz główny (AUTO_INCREMENT) |
|
||||||
|
| product_id | ID produktu (INT NULL) — wypełnione dla tras produktów |
|
||||||
|
| category_id | ID kategorii (INT NULL) — wypełnione dla tras kategorii |
|
||||||
|
| page_id | ID strony (INT NULL) — wypełnione dla tras stron |
|
||||||
|
| article_id | ID artykułu (INT NULL) — wypełnione dla tras artykułów |
|
||||||
|
| type | Typ trasy: NULL = encja (produkt/kategoria/strona/artykuł), `'system'` = trasa systemowa (koszyk, logowanie, newsletter, AJAX moduły, itp.) |
|
||||||
|
| lang_id | ID języka (0 dla tras systemowych niezwiązanych z językiem) |
|
||||||
|
| pattern | Wyrażenie regularne dopasowywane do REQUEST_URI |
|
||||||
|
| destination | Docelowy query string, np. `index.php?category=5&lang=1` |
|
||||||
|
|
||||||
|
**Mechanizm:** `index.php` ładuje wszystkie trasy (z cache Redis `pp_routes:all`) przed `checkUrlParams()`, dopasowuje `pattern` do ścieżki żądania i ustawia `$_GET` z `destination`. Obsługuje grupy przechwytujące (np. `$1` dla paginacji).
|
||||||
|
|
||||||
|
**Trasy systemowe:** Przy każdym wywołaniu `Helpers::htacces()` wszystkie rekordy z `type='system'` są usuwane i wstawiane na nowo (32 statycznych + dynamiczne trasy językowe i producentów). Zarządzane automatycznie — nie edytować ręcznie.
|
||||||
|
|
||||||
|
**Cache:** Redis klucz `pp_routes:all`, TTL 86400s. Invalidowany automatycznie przy każdym wywołaniu `Helpers::htacces()`.
|
||||||
|
|
||||||
|
**Używane w:** `index.php`, `Shared\Helpers\Helpers::htacces()`, `Domain\Product\ProductRepository`, `Domain\Category\CategoryRepository`, `Domain\Pages\PagesRepository`, `Domain\Article\ArticleRepository`
|
||||||
|
|
||||||
|
**Dodano w wersji 0.329. Kolumna `type` i trasy systemowe dodane w wersji 0.330.**
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Kazdy modul zawiera Repository (i opcjonalnie dodatkowe klasy). Konstruktor DI z
|
|||||||
| Category | CategoryRepository | drzewa kategorii, produkty w kategorii, Redis cache |
|
| Category | CategoryRepository | drzewa kategorii, produkty w kategorii, Redis cache |
|
||||||
| Client | ClientRepository | CRUD, auth, adresy, zamowienia |
|
| Client | ClientRepository | CRUD, auth, adresy, zamowienia |
|
||||||
| Coupon | CouponRepository | kupony rabatowe, walidacja, uzycie |
|
| Coupon | CouponRepository | kupony rabatowe, walidacja, uzycie |
|
||||||
|
| CronJob | CronJobType, CronJobRepository, CronJobProcessor | kolejka zadan cron (DB), priorytety, retry/backoff, harmonogram |
|
||||||
| Dashboard | DashboardRepository | statystyki admin, Redis cache |
|
| Dashboard | DashboardRepository | statystyki admin, Redis cache |
|
||||||
| Dictionaries | DictionariesRepository | slowniki admin |
|
| Dictionaries | DictionariesRepository | slowniki admin |
|
||||||
| Integrations | IntegrationsRepository | Apilo sync, ustawienia |
|
| Integrations | IntegrationsRepository | Apilo sync, ustawienia |
|
||||||
@@ -84,6 +85,7 @@ REST API dla ordersPRO. Entry point: `api.php`. Stateless (bez sesji), autentyka
|
|||||||
- `OrdersApiController` — lista, szczegoly, zmiana statusu, platnosc (5 akcji)
|
- `OrdersApiController` — lista, szczegoly, zmiana statusu, platnosc (5 akcji)
|
||||||
- `ProductsApiController` — lista, szczegoly, tworzenie, aktualizacja produktow (4 akcje)
|
- `ProductsApiController` — lista, szczegoly, tworzenie, aktualizacja produktow (4 akcje)
|
||||||
- `DictionariesApiController` — statusy, transporty, metody platnosci (3 akcje)
|
- `DictionariesApiController` — statusy, transporty, metody platnosci (3 akcje)
|
||||||
|
- `CategoriesApiController` — lista aktywnych kategorii (1 akcja)
|
||||||
|
|
||||||
Dokumentacja: `docs/API.md`
|
Dokumentacja: `docs/API.md`
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ composer test # standard
|
|||||||
## Aktualny stan
|
## Aktualny stan
|
||||||
|
|
||||||
```text
|
```text
|
||||||
OK (750 tests, 2114 assertions)
|
OK (805 tests, 2253 assertions)
|
||||||
```
|
```
|
||||||
|
|
||||||
Zweryfikowano: 2026-02-22 (ver. 0.304)
|
Zweryfikowano: 2026-02-24 (ver. 0.318)
|
||||||
|
|
||||||
## Konfiguracja
|
## Konfiguracja
|
||||||
|
|
||||||
@@ -52,6 +52,9 @@ tests/
|
|||||||
| | |-- Cache/CacheRepositoryTest.php
|
| | |-- Cache/CacheRepositoryTest.php
|
||||||
| | |-- Category/CategoryRepositoryTest.php
|
| | |-- Category/CategoryRepositoryTest.php
|
||||||
| | |-- Coupon/CouponRepositoryTest.php
|
| | |-- Coupon/CouponRepositoryTest.php
|
||||||
|
| | |-- CronJob/CronJobTypeTest.php
|
||||||
|
| | |-- CronJob/CronJobRepositoryTest.php
|
||||||
|
| | |-- CronJob/CronJobProcessorTest.php
|
||||||
| | |-- Dictionaries/DictionariesRepositoryTest.php
|
| | |-- Dictionaries/DictionariesRepositoryTest.php
|
||||||
| | |-- Integrations/IntegrationsRepositoryTest.php
|
| | |-- Integrations/IntegrationsRepositoryTest.php
|
||||||
| | |-- Languages/LanguagesRepositoryTest.php
|
| | |-- Languages/LanguagesRepositoryTest.php
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
3. Dodać uwierzytelnienie dwuskładnikowe za pomocą aplikacji.
|
||||||
|
4. Dodać zarządzanie uprawnieniami na poziomie urzytkownika, na razie uprawnienia do poszczególnych modułów.
|
||||||
|
naprawić działanie newslettera i zapis do bazy newslettera
|
||||||
|
program lojalnościowy
|
||||||
|
proponowane produkty w koszyku
|
||||||
|
Do zamówień w statusie: realizowane lub oczekuje na wpłatę. Opcja tylko dla zarejestrowanych klientów. https://royal-stone.pl/pl/order1.html
|
||||||
|
|||||||
658
docs/plans/2026-02-27-htaccess-conf-elimination.md
Normal file
658
docs/plans/2026-02-27-htaccess-conf-elimination.md
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
# htaccess.conf Elimination — Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Eliminate `libraries/htaccess.conf` as a template file and move all remaining hardcoded URL routes into `pp_routes`, leaving only true Apache-level directives in the generated `.htaccess`.
|
||||||
|
|
||||||
|
**Architecture:** `Helpers::htacces()` generates the full `.htaccess` content from PHP strings instead of loading a template. All URL→PHP mappings (static system routes + dynamic per language/producer) are inserted into `pp_routes` with `type='system'`, deleted and reinserted on every `htacces()` call. Apache-level rules (HTTPS redirect, admin routing, thumb.php) stay in `.htaccess` only.
|
||||||
|
|
||||||
|
**Tech Stack:** PHP 7.4, Medoo ORM (`$mdb`), Redis (CacheHandler), PHPUnit 9.6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### Current `Helpers::htacces()` structure (before this plan)
|
||||||
|
1. Loads `libraries/htaccess.conf` template (contains many hardcoded URL routes)
|
||||||
|
2. Appends language switch rules to `$htaccess_data`
|
||||||
|
3. Appends newsletter and producer rules to `$htaccess_data`
|
||||||
|
4. Inserts category/product/page/article routes into `pp_routes` (done in v0.329)
|
||||||
|
5. Replaces `{HTACCESS_CACHE}` placeholder
|
||||||
|
6. Appends catch-all, writes files
|
||||||
|
|
||||||
|
### What stays in `.htaccess` after this plan
|
||||||
|
- `RewriteEngine On`, `RewriteBase /`, `Options`
|
||||||
|
- www→https redirect
|
||||||
|
- http→https redirect (with tpay/przelewy24/hotpay exclusion)
|
||||||
|
- Trailing slash removal (excluding `/admin/`)
|
||||||
|
- Admin routing: `^admin/([^/]*)/([^/]*)/(.*)$`
|
||||||
|
- `^admin/$`
|
||||||
|
- Thumbnail: `^thumb/([0-9]*)/([0-9]*)/(.*)$` → `/libraries/thumb.php` (different PHP file, cannot use pp_routes)
|
||||||
|
- `THE_REQUEST` index.php redirect
|
||||||
|
- Cache headers block (gzip/expires or no-cache based on `$settings['htaccess_cache']`)
|
||||||
|
- File protection: `<Files *.conf>`, `<Files *.log>`, `<Files *.ini>`
|
||||||
|
- Start page 301 redirects (generated dynamically in pages loop)
|
||||||
|
- Custom htaccess from `pp_settings` (param=htaccess)
|
||||||
|
- Catch-all: `RewriteRule ^ index.php [L]`
|
||||||
|
|
||||||
|
### New `type` column in `pp_routes`
|
||||||
|
- `NULL` = entity route (product/category/page/article)
|
||||||
|
- `'system'` = system route (all routes in this plan)
|
||||||
|
- On every `htacces()` call: `DELETE WHERE type='system'`, then reinsert all
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Update SQL migration — add `type` column
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `migrations/0.329.sql`
|
||||||
|
|
||||||
|
**Step 1: Add `type` column to the migration**
|
||||||
|
|
||||||
|
Open `migrations/0.329.sql` (currently has 4 lines). Append the `type` column:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE pp_routes
|
||||||
|
ADD COLUMN category_id INT NULL AFTER product_id,
|
||||||
|
ADD COLUMN page_id INT NULL AFTER category_id,
|
||||||
|
ADD COLUMN article_id INT NULL AFTER page_id,
|
||||||
|
ADD COLUMN type VARCHAR(20) NULL AFTER article_id;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Apply migration on server**
|
||||||
|
|
||||||
|
Run on the production/staging database:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE pp_routes ADD COLUMN type VARCHAR(20) NULL AFTER article_id;
|
||||||
|
```
|
||||||
|
(The other 3 columns from 0.329 should already be applied from the previous deployment.)
|
||||||
|
|
||||||
|
**Step 3: No test needed** — pure schema change, verified when routes are inserted in Task 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Refactor `Helpers::htacces()` — replace template + move all routes to pp_routes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `autoload/Shared/Helpers/Helpers.php` (method `htacces()`, lines ~408–773)
|
||||||
|
|
||||||
|
This is the core task. The entire method is refactored. Here is the complete new body:
|
||||||
|
|
||||||
|
**Step 1: Replace the method body**
|
||||||
|
|
||||||
|
Find the opening of `htacces()` at line ~408. Replace everything from the start of the method body through the end (line ~773) with the code below.
|
||||||
|
|
||||||
|
The key structural changes:
|
||||||
|
- Remove `file_get_contents(htaccess.conf)` and `str_replace('{PAGE}', ...)`
|
||||||
|
- Remove `str_replace('{HTACCESS_CACHE}', ...)` — cache block is now inline
|
||||||
|
- Build `$htaccess_data` directly as PHP string
|
||||||
|
- Delete all `type='system'` routes, then reinsert static + dynamic ones
|
||||||
|
- Language switch → `pp_routes` (remove from `$htaccess_data`)
|
||||||
|
- Newsletter → `pp_routes` (remove from `$htaccess_data`)
|
||||||
|
- Producenci/producent → `pp_routes` (remove from `$htaccess_data`)
|
||||||
|
|
||||||
|
**New `htacces()` method body** — replace lines 409–773 with:
|
||||||
|
|
||||||
|
```php
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
$settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings( true );
|
||||||
|
|
||||||
|
$url = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );
|
||||||
|
|
||||||
|
$robots = 'User-agent: *' . PHP_EOL;
|
||||||
|
$robots .= 'Allow: /' . PHP_EOL;
|
||||||
|
|
||||||
|
$site_map = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
|
||||||
|
$site_map .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . PHP_EOL;
|
||||||
|
$site_map .= '<url>' . PHP_EOL;
|
||||||
|
$site_map .= '<loc>https://' . $url . '</loc>' . PHP_EOL;
|
||||||
|
$site_map .= '<lastmod>' . date( 'Y-m-d' ) . '</lastmod>' . PHP_EOL;
|
||||||
|
$site_map .= '<changefreq>daily</changefreq>' . PHP_EOL;
|
||||||
|
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
||||||
|
$site_map .= '</url>' . PHP_EOL;
|
||||||
|
|
||||||
|
//
|
||||||
|
// SYSTEM ROUTES — delete all and reinsert
|
||||||
|
//
|
||||||
|
$mdb->delete( 'pp_routes', [ 'type' => 'system' ] );
|
||||||
|
|
||||||
|
// Static system routes (hardcoded, never change)
|
||||||
|
$systemRoutes = [
|
||||||
|
// Wyszukiwarka
|
||||||
|
[ 'pattern' => '^wyszukiwarka/([^/]+)/([0-9]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=$2' ],
|
||||||
|
[ 'pattern' => '^wyszukiwarka/([^/]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=1' ],
|
||||||
|
// Zamowienia
|
||||||
|
[ 'pattern' => '^zamowienie/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=order_details&order_hash=$1' ],
|
||||||
|
[ 'pattern' => '^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=payment_confirmation&order_hash=$1' ],
|
||||||
|
// Platnosci
|
||||||
|
[ 'pattern' => '^tpay-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_tpay' ],
|
||||||
|
[ 'pattern' => '^platnosc-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_hotpay' ],
|
||||||
|
[ 'pattern' => '^przelewy24-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_przelewy24pl' ],
|
||||||
|
// Koszyk
|
||||||
|
[ 'pattern' => '^koszyk$', 'destination' => 'index.php?module=shop_basket&action=main_view' ],
|
||||||
|
[ 'pattern' => '^koszyk-podsumowanie$', 'destination' => 'index.php?module=shop_basket&action=summary_view' ],
|
||||||
|
[ 'pattern' => '^zloz-zamowienie$', 'destination' => 'index.php?module=shop_basket&action=basket_save' ],
|
||||||
|
// Klient
|
||||||
|
[ 'pattern' => '^rejestracja$', 'destination' => 'index.php?module=shop_client&action=register_form' ],
|
||||||
|
[ 'pattern' => '^logowanie$', 'destination' => 'index.php?module=shop_client&action=login_form' ],
|
||||||
|
[ 'pattern' => '^wylogowanie$', 'destination' => 'index.php?module=shop_client&action=logout' ],
|
||||||
|
[ 'pattern' => '^odzyskiwanie-hasla$', 'destination' => 'index.php?module=shop_client&action=recover_password' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/zamowienia$', 'destination' => 'index.php?module=shop_client&action=client_orders' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/adresy$', 'destination' => 'index.php?module=shop_client&action=client_addresses' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/nowy-adres$', 'destination' => 'index.php?module=shop_client&action=address_edit' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/edytuj-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_edit&id=$1' ],
|
||||||
|
[ 'pattern' => '^panel-klienta/usun-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_delete&id=$1' ],
|
||||||
|
// Newsletter
|
||||||
|
[ 'pattern' => '^newsletter/signin$', 'destination' => 'index.php?module=newsletter&action=signin' ],
|
||||||
|
[ 'pattern' => '^newsletter/confirm/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=confirm&hash=$1' ],
|
||||||
|
[ 'pattern' => '^newsletter/unsubscribe/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=unsubscribe&hash=$1' ],
|
||||||
|
// Moduły AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search)
|
||||||
|
[ 'pattern' => '^shopBasket/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopBasket&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^shopBasket/([^/]+)$', 'destination' => 'index.php?module=shopBasket&action=$1' ],
|
||||||
|
[ 'pattern' => '^shopClient/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopClient&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^shopClient/([^/]+)$', 'destination' => 'index.php?module=shopClient&action=$1' ],
|
||||||
|
[ 'pattern' => '^shopProduct/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopProduct&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^shopProduct/([^/]+)$', 'destination' => 'index.php?module=shopProduct&action=$1' ],
|
||||||
|
[ 'pattern' => '^shopCoupon/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopCoupon&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^shopCoupon/([^/]+)$', 'destination' => 'index.php?module=shopCoupon&action=$1' ],
|
||||||
|
[ 'pattern' => '^search/([^/]+)/(.+)$', 'destination' => 'index.php?module=search&action=$1&$2' ],
|
||||||
|
[ 'pattern' => '^search/([^/]+)$', 'destination' => 'index.php?module=search&action=$1' ],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ( $systemRoutes as $route )
|
||||||
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => $route['pattern'],
|
||||||
|
'destination' => $route['destination'],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic system routes — languages
|
||||||
|
$results = $mdb->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
|
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||||
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => '^' . $row['id'] . '$',
|
||||||
|
'destination' => 'index.php?a=change_language&id=' . $row['id'],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic system routes — producenci
|
||||||
|
$categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId();
|
||||||
|
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => '^producenci$',
|
||||||
|
'destination' => 'index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$rows = $mdb->select( 'pp_shop_producer', '*', [ 'status' => 1 ] );
|
||||||
|
if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row )
|
||||||
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => '^producent/' . self::seo( $row['name'] ) . '$',
|
||||||
|
'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId,
|
||||||
|
] );
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'type' => 'system',
|
||||||
|
'lang_id' => 0,
|
||||||
|
'pattern' => '^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$',
|
||||||
|
'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1',
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// HTACCESS — generuj z PHP (bez szablonu htaccess.conf)
|
||||||
|
//
|
||||||
|
$htaccess_data = 'RewriteEngine On' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteBase /' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'Options +FollowSymlinks' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'Options -Indexes' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= '# Przekierowanie z www na bez www i z http na https w jednym kroku' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= '# Przekierowanie z http na https, jesli nie zawiera www' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{HTTPS} off' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= '# Usuwanie koncowego slasha dla niekatalogów' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} (.+)/$' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ %1 [R=301,L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^admin/$ admin/index.php [L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]' . PHP_EOL;
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ /%1 [R=301,L]' . PHP_EOL;
|
||||||
|
|
||||||
|
/* cache — zastąpienie placeholdera {HTACCESS_CACHE} */
|
||||||
|
if ( $settings['htaccess_cache'] )
|
||||||
|
{
|
||||||
|
$htaccess_data .= '<IfModule mod_deflate.c>' . PHP_EOL
|
||||||
|
. 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL
|
||||||
|
. '</IfModule>' . PHP_EOL
|
||||||
|
. '<IfModule mod_headers.c>' . PHP_EOL
|
||||||
|
. 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL
|
||||||
|
. '</IfModule>' . PHP_EOL
|
||||||
|
. '<IfModule mod_expires.c>' . PHP_EOL
|
||||||
|
. 'ExpiresActive on' . PHP_EOL
|
||||||
|
. 'ExpiresDefault "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL
|
||||||
|
. 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL
|
||||||
|
. 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL
|
||||||
|
. '</IfModule>' . PHP_EOL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$htaccess_data .= '<IfModule mod_headers.c>' . PHP_EOL
|
||||||
|
. 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL
|
||||||
|
. 'Header set Pragma "no-cache"' . PHP_EOL
|
||||||
|
. 'Header set Expires 0' . PHP_EOL
|
||||||
|
. '</IfModule>' . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$htaccess_data .= '<Files *.conf>' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Deny from all' . PHP_EOL;
|
||||||
|
$htaccess_data .= '</Files>' . PHP_EOL;
|
||||||
|
$htaccess_data .= '<Files *.log>' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Deny from all' . PHP_EOL;
|
||||||
|
$htaccess_data .= '</Files>' . PHP_EOL;
|
||||||
|
$htaccess_data .= '<Files *.ini>' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
|
||||||
|
$htaccess_data .= ' Deny from all' . PHP_EOL;
|
||||||
|
$htaccess_data .= '</Files>' . PHP_EOL;
|
||||||
|
|
||||||
|
//
|
||||||
|
// KATEGORIE — sitemap + pp_routes (bez zmian)
|
||||||
|
//
|
||||||
|
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
|
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||||
|
{
|
||||||
|
!$row['start'] ? $language_link = $row['id'] . '/' : $language_link = '';
|
||||||
|
|
||||||
|
$results2 = $mdb->select( 'pp_shop_categories_langs', [ '[><]pp_shop_categories' => [ 'category_id' => 'id' ] ], [ 'seo_link', 'title', 'category_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
|
if ( is_array( $results2 ) ) foreach ( $results2 as $row2 )
|
||||||
|
{
|
||||||
|
if ( $row2['title'] )
|
||||||
|
{
|
||||||
|
$site_map .= '<url>' . PHP_EOL;
|
||||||
|
if ( $row2['seo_link'] )
|
||||||
|
$site_map .= '<loc>https://' . $url . '/' . $language_link . self::seo( $row2['seo_link'] ) . '</loc>' . PHP_EOL;
|
||||||
|
else
|
||||||
|
$site_map .= '<loc>https://' . $url . '/' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '</loc>' . PHP_EOL;
|
||||||
|
$site_map .= '<lastmod>' . date( 'Y-m-d' ) . '</lastmod>' . PHP_EOL;
|
||||||
|
$site_map .= '<changefreq>daily</changefreq>' . PHP_EOL;
|
||||||
|
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
||||||
|
$site_map .= '</url>' . PHP_EOL;
|
||||||
|
|
||||||
|
$seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] );
|
||||||
|
|
||||||
|
$mdb->delete( 'pp_routes', [ 'AND' => [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'] ] ] );
|
||||||
|
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'category_id' => $row2['category_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $language_link . $seoSlug . '$',
|
||||||
|
'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'],
|
||||||
|
] );
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'category_id' => $row2['category_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $language_link . $seoSlug . '/([0-9]+)$',
|
||||||
|
'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1',
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// PRODUKTY — sitemap + pp_routes (bez zmian)
|
||||||
|
//
|
||||||
|
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
|
if ( is_array( $results ) )
|
||||||
|
{
|
||||||
|
foreach ( $results as $row )
|
||||||
|
{
|
||||||
|
!$row['start'] ? $language_link = $row['id'] . '/' : $language_link = '';
|
||||||
|
|
||||||
|
$results2 = $mdb->select( 'pp_shop_products_langs', [ '[><]pp_shop_products' => [ 'product_id' => 'id' ] ], [ 'seo_link', 'name', 'product_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||||
|
if ( is_array( $results2 ) )
|
||||||
|
{
|
||||||
|
foreach ( $results2 as $row2 )
|
||||||
|
{
|
||||||
|
$mdb->delete( 'pp_routes', [ 'AND' => [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'] ] ] );
|
||||||
|
|
||||||
|
if ( $row2['name'] )
|
||||||
|
{
|
||||||
|
$site_map .= '<url>' . PHP_EOL;
|
||||||
|
if ( $row2['seo_link'] )
|
||||||
|
$site_map .= '<loc>https://' . $url . '/' . $language_link . self::seo( $row2['seo_link'] ) . '</loc>' . PHP_EOL;
|
||||||
|
else
|
||||||
|
$site_map .= '<loc>https://' . $url . '/' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '</loc>' . PHP_EOL;
|
||||||
|
$site_map .= '<lastmod>' . date( 'Y-m-d' ) . '</lastmod>' . PHP_EOL;
|
||||||
|
$site_map .= '<changefreq>daily</changefreq>' . PHP_EOL;
|
||||||
|
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
||||||
|
$site_map .= '</url>' . PHP_EOL;
|
||||||
|
|
||||||
|
if ( $row2['seo_link'] )
|
||||||
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
|
||||||
|
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
|
||||||
|
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// STRONY + ARTYKULY — sitemap + pp_routes (bez zmian)
|
||||||
|
//
|
||||||
|
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
|
||||||
|
if ( is_array( $results ) )
|
||||||
|
foreach ( $results as $row )
|
||||||
|
{
|
||||||
|
( !$row['start'] and count( $results ) > 1 ) ? $language_link = $row['id'] . '/' : $language_link = '';
|
||||||
|
|
||||||
|
$results2 = $mdb->select( 'pp_pages_langs', [ '[><]pp_pages' => [ 'page_id' => 'id' ] ], [ 'seo_link', 'title', 'page_id', 'noindex', 'start', 'link', 'page_type' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'start' => 'DESC', 'o' => 'ASC' ] ] );
|
||||||
|
if ( is_array( $results2 ) )
|
||||||
|
foreach ( $results2 as $row2 )
|
||||||
|
{
|
||||||
|
if ( $row2['title'] and $row2['page_type'] != 3 and $row2['page_type'] != 5 )
|
||||||
|
{
|
||||||
|
if ( !$row2['noindex'] )
|
||||||
|
{
|
||||||
|
$site_map .= '<url>' . PHP_EOL;
|
||||||
|
if ( $row2['seo_link'] )
|
||||||
|
$site_map .= '<loc>https://' . $url . '/' . self::seo( $row2['seo_link'] ) . '</loc>' . PHP_EOL;
|
||||||
|
else
|
||||||
|
$site_map .= '<loc>https://' . $url . '/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '</loc>' . PHP_EOL;
|
||||||
|
$site_map .= '<lastmod>' . date( 'Y-m-d' ) . '</lastmod>' . PHP_EOL;
|
||||||
|
$site_map .= '<changefreq>daily</changefreq>' . PHP_EOL;
|
||||||
|
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
||||||
|
$site_map .= '</url>' . PHP_EOL;
|
||||||
|
}
|
||||||
|
else if ( $row2['noindex'] and $row2['seo_link'] )
|
||||||
|
{
|
||||||
|
$robots .= 'User-agent: GoogleBot' . PHP_EOL;
|
||||||
|
$robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $row2['start'] )
|
||||||
|
{
|
||||||
|
if ( $row2['seo_link'] )
|
||||||
|
{
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/' . self::seo( $row2['seo_link'] ) . '$';
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/' . self::seo( $row2['seo_link'] ) . '-1$';
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$';
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$';
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
|
||||||
|
}
|
||||||
|
$htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]';
|
||||||
|
}
|
||||||
|
|
||||||
|
$seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] );
|
||||||
|
$langPrefix = $row2['start'] ? '' : $language_link;
|
||||||
|
|
||||||
|
$mdb->delete( 'pp_routes', [ 'AND' => [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'] ] ] );
|
||||||
|
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'page_id' => $row2['page_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $langPrefix . $seoSlug . '$',
|
||||||
|
'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'],
|
||||||
|
] );
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'page_id' => $row2['page_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $langPrefix . $seoSlug . '/([0-9]+)$',
|
||||||
|
'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1',
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$results2 = $mdb->select( 'pp_articles_langs', [ '[><]pp_articles' => [ 'article_id' => 'id' ] ], [ 'seo_link', 'title', 'article_id', 'noindex', 'copy_from' ], [ 'AND' => [ 'status' => 1, 'lang_id' => $row['id'], 'block_direct_access' => 0 ] ] );
|
||||||
|
if ( is_array( $results2 ) )
|
||||||
|
foreach ( $results2 as $row2 )
|
||||||
|
{
|
||||||
|
if ( $row2['copy_from'] != null )
|
||||||
|
{
|
||||||
|
$results_tmp = $mdb->get( 'pp_articles_langs', [ 'seo_link', 'title' ], [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row2['copy_from'] ] ] );
|
||||||
|
$row2['seo_link'] = $results_tmp['seo_link'];
|
||||||
|
$row2['title'] = $results_tmp['title'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !$row2['noindex'] )
|
||||||
|
{
|
||||||
|
$site_map .= '<url>' . PHP_EOL;
|
||||||
|
if ( $row2['seo_link'] )
|
||||||
|
$site_map .= '<loc>https://' . $url . '/' . self::seo( $row2['seo_link'] ) . '</loc>' . PHP_EOL;
|
||||||
|
else
|
||||||
|
$site_map .= '<loc>https://' . $url . '/a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '</loc>' . PHP_EOL;
|
||||||
|
$site_map .= '<lastmod>' . date( 'Y-m-d' ) . '</lastmod>' . PHP_EOL;
|
||||||
|
$site_map .= '<changefreq>daily</changefreq>' . PHP_EOL;
|
||||||
|
$site_map .= '<priority>1</priority>' . PHP_EOL;
|
||||||
|
$site_map .= '</url>' . PHP_EOL;
|
||||||
|
}
|
||||||
|
else if ( $row2['noindex'] and $row2['seo_link'] )
|
||||||
|
{
|
||||||
|
$robots .= 'User-agent: GoogleBot' . PHP_EOL;
|
||||||
|
$robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mdb->delete( 'pp_routes', [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'] ] ] );
|
||||||
|
|
||||||
|
if ( $row2['seo_link'] )
|
||||||
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'article_id' => $row2['article_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$',
|
||||||
|
'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
else if ( $row2['title'] != null )
|
||||||
|
{
|
||||||
|
$mdb->insert( 'pp_routes', [
|
||||||
|
'article_id' => $row2['article_id'],
|
||||||
|
'lang_id' => $row['id'],
|
||||||
|
'pattern' => '^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$',
|
||||||
|
'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidacja cache tras
|
||||||
|
try {
|
||||||
|
( new \Shared\Cache\CacheHandler() )->delete( 'pp_routes:all' );
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
// Redis niedostepny — ignorujemy
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] );
|
||||||
|
if ( $results )
|
||||||
|
$htaccess_data .= PHP_EOL . $results;
|
||||||
|
|
||||||
|
$results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'robots' ] );
|
||||||
|
if ( $results )
|
||||||
|
$robots .= PHP_EOL . $results;
|
||||||
|
|
||||||
|
$site_map .= '</urlset>';
|
||||||
|
|
||||||
|
$htaccess_data .= PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL;
|
||||||
|
$htaccess_data .= 'RewriteRule ^ index.php [L]';
|
||||||
|
|
||||||
|
// Niektore hostingi blokuja zmiane wersji PHP przez .htaccess.
|
||||||
|
$htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data );
|
||||||
|
|
||||||
|
$fp = fopen( $dir . '.htaccess', 'w' );
|
||||||
|
fwrite( $fp, $htaccess_data );
|
||||||
|
fclose( $fp );
|
||||||
|
|
||||||
|
$fp = fopen( $dir . 'sitemap.xml', 'w' );
|
||||||
|
fwrite( $fp, $site_map );
|
||||||
|
fclose( $fp );
|
||||||
|
|
||||||
|
$fp = fopen( $dir . 'robots.txt', 'w' );
|
||||||
|
fwrite( $fp, $robots );
|
||||||
|
fclose( $fp );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests**
|
||||||
|
```
|
||||||
|
php phpunit.phar --configuration phpunit.xml
|
||||||
|
```
|
||||||
|
Expected: all tests pass (htacces() has no unit tests, covered by integration).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Delete `libraries/htaccess.conf`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Delete: `libraries/htaccess.conf`
|
||||||
|
|
||||||
|
**Step 1: Verify htacces() no longer references the file**
|
||||||
|
|
||||||
|
Search for any remaining `file_get_contents` referencing `htaccess.conf`:
|
||||||
|
```bash
|
||||||
|
grep -r "htaccess.conf" autoload/
|
||||||
|
```
|
||||||
|
Expected: no results.
|
||||||
|
|
||||||
|
**Step 2: Delete the file**
|
||||||
|
```bash
|
||||||
|
rm libraries/htaccess.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Run tests**
|
||||||
|
```
|
||||||
|
php phpunit.phar --configuration phpunit.xml
|
||||||
|
```
|
||||||
|
Expected: all tests still pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Update `docs/DATABASE_STRUCTURE.md`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/DATABASE_STRUCTURE.md` (section `## pp_routes`)
|
||||||
|
|
||||||
|
**Step 1: Add `type` column to the pp_routes table description**
|
||||||
|
|
||||||
|
Find the `## pp_routes` section and add the `type` row to the column table:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| type | Typ trasy: NULL = encja (produkt/kategoria/strona/artykuł), 'system' = trasa systemowa |
|
||||||
|
```
|
||||||
|
|
||||||
|
Also update the description paragraph to mention that system routes are managed automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Manual integration test on server
|
||||||
|
|
||||||
|
**Step 1: Apply migration**
|
||||||
|
```sql
|
||||||
|
ALTER TABLE pp_routes ADD COLUMN type VARCHAR(20) NULL AFTER article_id;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Trigger `htacces()` regeneration**
|
||||||
|
|
||||||
|
Log in to admin panel → save any product or category → this calls `htacces()`.
|
||||||
|
|
||||||
|
**Step 3: Verify pp_routes has system routes**
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*) FROM pp_routes WHERE type = 'system';
|
||||||
|
```
|
||||||
|
Expected: ~35+ rows (32 static + language rows + producer rows).
|
||||||
|
|
||||||
|
**Step 4: Verify .htaccess was generated correctly**
|
||||||
|
|
||||||
|
Open `.htaccess` — should NOT contain `RewriteRule ^koszyk$`, `^logowanie$`, etc. Should contain HTTPS redirect, admin routing, thumb routing, cache block.
|
||||||
|
|
||||||
|
**Step 5: Test URLs in browser**
|
||||||
|
- `/koszyk` → koszyk page ✓
|
||||||
|
- `/logowanie` → login page ✓
|
||||||
|
- `/wyszukiwarka/test` → search results ✓
|
||||||
|
- `/zamowienie/abc123` → order details ✓
|
||||||
|
- `/shopClient/confirm/hash=xyz` → client confirm action ✓
|
||||||
|
- Category URL → category page ✓
|
||||||
|
- Product URL → product page ✓
|
||||||
|
|
||||||
|
**Step 6: Run full test suite**
|
||||||
|
```
|
||||||
|
php phpunit.phar --configuration phpunit.xml
|
||||||
|
```
|
||||||
|
Expected: 807 tests, all pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Commit
|
||||||
|
|
||||||
|
**Step 1: Stage and commit**
|
||||||
|
```bash
|
||||||
|
git add migrations/0.329.sql
|
||||||
|
git add autoload/Shared/Helpers/Helpers.php
|
||||||
|
git add docs/DATABASE_STRUCTURE.md
|
||||||
|
git add docs/plans/2026-02-27-htaccess-conf-elimination.md
|
||||||
|
git add docs/plans/2026-02-27-htaccess-to-routes-design.md
|
||||||
|
git rm libraries/htaccess.conf
|
||||||
|
git commit -m "feat: eliminate htaccess.conf, move all routes to pp_routes (v0.330)"
|
||||||
|
```
|
||||||
121
docs/plans/2026-02-27-htaccess-to-routes-design.md
Normal file
121
docs/plans/2026-02-27-htaccess-to-routes-design.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Design: Eliminacja htaccess.conf i przeniesienie wszystkich tras do pp_routes
|
||||||
|
|
||||||
|
**Data:** 2026-02-27
|
||||||
|
**Wersja docelowa:** 0.330
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cel
|
||||||
|
|
||||||
|
Wyeliminowanie pliku `libraries/htaccess.conf` jako szablonu i przeniesienie wszystkich URL-i, które dotychczas były wpisane na sztywno w `.htaccess`, do tabeli `pp_routes`. Logika generowania `.htaccess` zostaje w całości w `Helpers::htacces()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Co zostaje w `.htaccess` (reguły Apache-level)
|
||||||
|
|
||||||
|
Tylko dyrektywy, których PHP nie może obsłużyć:
|
||||||
|
|
||||||
|
- `RewriteEngine On`, `Options`
|
||||||
|
- Redirect HTTPS/www
|
||||||
|
- Redirect HTTP→HTTPS (z wyłączeniem tpay-status, platnosc-status, przelewy24-status)
|
||||||
|
- Usuwanie trailing slash (z wyłączeniem `/admin/`)
|
||||||
|
- Routing `/admin/` → `admin/index.php`
|
||||||
|
- `thumb/([0-9]*)/([0-9]*)/(.*)` → `/libraries/thumb.php` (inny plik PHP — niemożliwe przez pp_routes)
|
||||||
|
- `RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php` — redirect z index.php
|
||||||
|
- Blok cache headers (gzip, expires) — zależny od `$settings['htaccess_cache']`
|
||||||
|
- Ochrona plików: `<Files *.conf>`, `<Files *.log>`, `<Files *.ini>`
|
||||||
|
- Przekierowania 301 stron startowych (generowane dynamicznie w pętli pages)
|
||||||
|
- Niestandardowe reguły z `pp_settings` (param=htaccess)
|
||||||
|
- Catch-all: `RewriteCond !-f`, `!-d`, `RewriteRule ^ index.php [L]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Co przechodzi do `pp_routes`
|
||||||
|
|
||||||
|
### Statyczne trasy systemowe (hardcoded, niezmienne)
|
||||||
|
|
||||||
|
| Pattern | Destination |
|
||||||
|
|---------|-------------|
|
||||||
|
| `^wyszukiwarka/([^/]+)/([0-9]+)$` | `index.php?module=search&action=search_results&query=$1&bs=$2` |
|
||||||
|
| `^wyszukiwarka/([^/]+)$` | `index.php?module=search&action=search_results&query=$1&bs=1` |
|
||||||
|
| `^zamowienie/([a-zA-Z0-9-]+)$` | `index.php?module=shop_order&action=order_details&order_hash=$1` |
|
||||||
|
| `^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$` | `index.php?module=shop_order&action=payment_confirmation&order_hash=$1` |
|
||||||
|
| `^tpay-status$` | `index.php?module=shop_order&action=payment_status_tpay` |
|
||||||
|
| `^platnosc-status$` | `index.php?module=shop_order&action=payment_status_hotpay` |
|
||||||
|
| `^przelewy24-status$` | `index.php?module=shop_order&action=payment_status_przelewy24pl` |
|
||||||
|
| `^koszyk$` | `index.php?module=shop_basket&action=main_view` |
|
||||||
|
| `^koszyk-podsumowanie$` | `index.php?module=shop_basket&action=summary_view` |
|
||||||
|
| `^zloz-zamowienie$` | `index.php?module=shop_basket&action=basket_save` |
|
||||||
|
| `^rejestracja$` | `index.php?module=shop_client&action=register_form` |
|
||||||
|
| `^logowanie$` | `index.php?module=shop_client&action=login_form` |
|
||||||
|
| `^wylogowanie$` | `index.php?module=shop_client&action=logout` |
|
||||||
|
| `^odzyskiwanie-hasla$` | `index.php?module=shop_client&action=recover_password` |
|
||||||
|
| `^panel-klienta/zamowienia$` | `index.php?module=shop_client&action=client_orders` |
|
||||||
|
| `^panel-klienta/adresy$` | `index.php?module=shop_client&action=client_addresses` |
|
||||||
|
| `^panel-klienta/nowy-adres$` | `index.php?module=shop_client&action=address_edit` |
|
||||||
|
| `^panel-klienta/edytuj-adres/([0-9]+)$` | `index.php?module=shop_client&action=address_edit&id=$1` |
|
||||||
|
| `^panel-klienta/usun-adres/([0-9]+)$` | `index.php?module=shop_client&action=address_delete&id=$1` |
|
||||||
|
| `^newsletter/signin$` | `index.php?module=newsletter&action=signin` |
|
||||||
|
| `^newsletter/confirm/hash=(.+)$` | `index.php?module=newsletter&action=confirm&hash=$1` |
|
||||||
|
| `^newsletter/unsubscribe/hash=(.+)$` | `index.php?module=newsletter&action=unsubscribe&hash=$1` |
|
||||||
|
|
||||||
|
### Trasy modułów AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search)
|
||||||
|
|
||||||
|
Dwa wzorce na moduł — 3-segmentowy (z parametrami) i 2-segmentowy:
|
||||||
|
|
||||||
|
| Pattern | Destination |
|
||||||
|
|---------|-------------|
|
||||||
|
| `^shopBasket/([^/]+)/(.+)$` | `index.php?module=shopBasket&action=$1&$2` |
|
||||||
|
| `^shopBasket/([^/]+)$` | `index.php?module=shopBasket&action=$1` |
|
||||||
|
| `^shopClient/([^/]+)/(.+)$` | `index.php?module=shopClient&action=$1&$2` |
|
||||||
|
| `^shopClient/([^/]+)$` | `index.php?module=shopClient&action=$1` |
|
||||||
|
| `^shopProduct/([^/]+)/(.+)$` | `index.php?module=shopProduct&action=$1&$2` |
|
||||||
|
| `^shopProduct/([^/]+)$` | `index.php?module=shopProduct&action=$1` |
|
||||||
|
| `^shopCoupon/([^/]+)/(.+)$` | `index.php?module=shopCoupon&action=$1&$2` |
|
||||||
|
| `^shopCoupon/([^/]+)$` | `index.php?module=shopCoupon&action=$1` |
|
||||||
|
| `^search/([^/]+)/(.+)$` | `index.php?module=search&action=$1&$2` |
|
||||||
|
| `^search/([^/]+)$` | `index.php?module=search&action=$1` |
|
||||||
|
|
||||||
|
### Dynamiczne trasy systemowe (wstawiane przy każdym `htacces()`)
|
||||||
|
|
||||||
|
- **Języki:** `^{lang_id}$` → `index.php?a=change_language&id={lang_id}` (per każdy aktywny język)
|
||||||
|
- **Producenci lista:** `^producenci$` → `index.php?module=shop_producer&action=list&layout_id={id}`
|
||||||
|
- **Producent detail:** `^producent/{slug}$` i `^producent/{slug}/([0-9]+)$` (per producent z DB)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nowa kolumna `type` w `pp_routes`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ADD COLUMN type VARCHAR(20) NULL AFTER article_id
|
||||||
|
```
|
||||||
|
|
||||||
|
| Wartość | Znaczenie |
|
||||||
|
|---------|-----------|
|
||||||
|
| `NULL` | Trasa encji (produkt, kategoria, strona, artykuł) |
|
||||||
|
| `'system'` | Trasa systemowa (wszystkie powyższe) |
|
||||||
|
|
||||||
|
**Zarządzanie:** przy każdym `htacces()`:
|
||||||
|
```php
|
||||||
|
$mdb->delete('pp_routes', ['type' => 'system']); // usuń wszystkie
|
||||||
|
// ... wstaw na nowo (statyczne + dynamiczne)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Eliminacja `htaccess.conf`
|
||||||
|
|
||||||
|
`file_get_contents($dir . 'libraries/htaccess.conf')` zastąpione PHP stringiem z tą samą treścią (tylko Apache-level reguły). Placeholder `{HTACCESS_CACHE}` zastąpiony bezpośrednim `if ($settings['htaccess_cache']) { ... } else { ... }` wbudowanym w odpowiednim miejscu.
|
||||||
|
|
||||||
|
Plik `libraries/htaccess.conf` zostaje usunięty.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pliki do modyfikacji
|
||||||
|
|
||||||
|
| Plik | Zmiana |
|
||||||
|
|------|--------|
|
||||||
|
| `migrations/0.329.sql` | Dodać `ADD COLUMN type VARCHAR(20) NULL` |
|
||||||
|
| `Helpers::htacces()` | Usunąć `file_get_contents`, wbudować statyczny header, dodać inserty system routes, usunąć htaccess rules dla języków/newsletter/producenci |
|
||||||
|
| `libraries/htaccess.conf` | Usunąć plik |
|
||||||
|
| `docs/DATABASE_STRUCTURE.md` | Dodać kolumnę `type` do opisu pp_routes |
|
||||||
76
index.php
76
index.php
@@ -59,6 +59,49 @@ $mdb = new medoo( [
|
|||||||
'time_debug' => $database['time_debug']
|
'time_debug' => $database['time_debug']
|
||||||
] );
|
] );
|
||||||
|
|
||||||
|
// check routes
|
||||||
|
$parsed_url = parse_url($_SERVER['REQUEST_URI']);
|
||||||
|
$request_uri = ltrim($parsed_url['path'], '/');
|
||||||
|
$query_string = isset($parsed_url['query']) ? $parsed_url['query'] : '';
|
||||||
|
parse_str($query_string, $query_params);
|
||||||
|
|
||||||
|
if ($request_uri != '')
|
||||||
|
{
|
||||||
|
$cache = new \Shared\Cache\CacheHandler();
|
||||||
|
$cacheKey = 'pp_routes:all';
|
||||||
|
$routesCached = $cache->get($cacheKey);
|
||||||
|
|
||||||
|
if ($routesCached === false || $routesCached === null)
|
||||||
|
{
|
||||||
|
$routes = $mdb->select('pp_routes', '*');
|
||||||
|
$cache->set($cacheKey, $routes, 86400);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$routes = unserialize($routesCached);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($routes as $route)
|
||||||
|
{
|
||||||
|
$pattern = $route['pattern'];
|
||||||
|
$destination = $route['destination'];
|
||||||
|
|
||||||
|
if (preg_match("#^" . $pattern . "#", $request_uri, $matches))
|
||||||
|
{
|
||||||
|
// Replace placeholders in the destination with matches from the request URI
|
||||||
|
$destination = preg_replace("#^" . $pattern . "#", $destination, $request_uri);
|
||||||
|
|
||||||
|
// Parse the destination string to extract GET parameters
|
||||||
|
parse_str(parse_url($destination, PHP_URL_QUERY), $destination_params);
|
||||||
|
|
||||||
|
// Merge the destination params with query params from the URL
|
||||||
|
$_GET = array_merge($destination_params, $query_params);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
\front\App::checkUrlParams();
|
\front\App::checkUrlParams();
|
||||||
|
|
||||||
$langRepo = new \Domain\Languages\LanguagesRepository( $mdb );
|
$langRepo = new \Domain\Languages\LanguagesRepository( $mdb );
|
||||||
@@ -99,39 +142,6 @@ if ( $request_uri != '' )
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check routes
|
|
||||||
$parsed_url = parse_url($_SERVER['REQUEST_URI']);
|
|
||||||
$request_uri = ltrim($parsed_url['path'], '/');
|
|
||||||
$query_string = isset($parsed_url['query']) ? $parsed_url['query'] : '';
|
|
||||||
parse_str($query_string, $query_params);
|
|
||||||
|
|
||||||
if ($request_uri != '')
|
|
||||||
{
|
|
||||||
$matched = false;
|
|
||||||
|
|
||||||
$routes = $mdb->select('pp_routes', '*');
|
|
||||||
foreach ($routes as $route)
|
|
||||||
{
|
|
||||||
$pattern = $route['pattern'];
|
|
||||||
$destination = $route['destination'];
|
|
||||||
|
|
||||||
if (preg_match("#^" . $pattern . "#", $request_uri, $matches))
|
|
||||||
{
|
|
||||||
// Replace placeholders in the destination with matches from the request URI
|
|
||||||
$destination = preg_replace("#^" . $pattern . "#", $destination, $request_uri);
|
|
||||||
|
|
||||||
// Parse the destination string to extract GET parameters
|
|
||||||
parse_str(parse_url($destination, PHP_URL_QUERY), $destination_params);
|
|
||||||
|
|
||||||
// Merge the destination params with query params from the URL
|
|
||||||
$_GET = array_merge($destination_params, $query_params);
|
|
||||||
|
|
||||||
$matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pagesRepo = new \Domain\Pages\PagesRepository( $mdb );
|
$pagesRepo = new \Domain\Pages\PagesRepository( $mdb );
|
||||||
|
|
||||||
if ( \Shared\Helpers\Helpers::get( 'a' ) == 'page' and \Shared\Helpers\Helpers::get( 'id' ) )
|
if ( \Shared\Helpers\Helpers::get( 'a' ) == 'page' and \Shared\Helpers\Helpers::get( 'id' ) )
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
RewriteEngine On
|
|
||||||
RewriteBase /
|
|
||||||
Options +FollowSymlinks
|
|
||||||
Options -Indexes
|
|
||||||
|
|
||||||
# Przekierowanie z www na bez www i z http na https w jednym kroku
|
|
||||||
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
|
||||||
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
|
|
||||||
|
|
||||||
# Przekierowanie z http na https, jeśli nie zawiera www
|
|
||||||
RewriteCond %{HTTPS} off
|
|
||||||
RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]
|
|
||||||
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
|
||||||
|
|
||||||
# Usuwanie końcowego slash'a dla niekatalogów
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC] # Wyklucza ścieżki rozpoczynające się od "admin/"
|
|
||||||
RewriteCond %{REQUEST_URI} (.+)/$
|
|
||||||
RewriteRule ^ %1 [R=301,L]
|
|
||||||
|
|
||||||
RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]
|
|
||||||
RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]
|
|
||||||
RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]
|
|
||||||
|
|
||||||
RewriteRule ^admin/$ admin/index.php [L]
|
|
||||||
|
|
||||||
RewriteRule ^wyszukiwarka/(.*)/([0-9]*)$ index.php?module=search&action=search_results&query=$1&bs=$2 [L]
|
|
||||||
RewriteRule ^wyszukiwarka/(.*)$ index.php?module=search&action=search_results&query=$1&bs=1 [L]
|
|
||||||
RewriteRule ^zamowienie/([a-zA-Z0-9-]*)$ index.php?module=shop_order&action=order_details&order_hash=$1 [L]
|
|
||||||
RewriteRule ^potwierdzenie-platnosci/([a-zA-Z0-9-]*)$ index.php?module=shop_order&action=payment_confirmation&order_hash=$1 [L]
|
|
||||||
RewriteRule ^tpay-status$ index.php?module=shop_order&action=payment_status_tpay [QSA,L]
|
|
||||||
RewriteRule ^platnosc-status$ index.php?module=shop_order&action=payment_status_hotpay [QSA,L]
|
|
||||||
RewriteRule ^przelewy24-status$ index.php?module=shop_order&action=payment_status_przelewy24pl [QSA,L]
|
|
||||||
RewriteRule ^koszyk$ index.php?module=shop_basket&action=main_view [L]
|
|
||||||
RewriteRule ^koszyk-podsumowanie$ index.php?module=shop_basket&action=summary_view [L]
|
|
||||||
RewriteRule ^zloz-zamowienie$ index.php?module=shop_basket&action=basket_save [L]
|
|
||||||
RewriteRule ^rejestracja$ index.php?module=shop_client&action=register_form [L]
|
|
||||||
RewriteRule ^logowanie$ index.php?module=shop_client&action=login_form [L]
|
|
||||||
RewriteRule ^wylogowanie$ index.php?module=shop_client&action=logout [L]
|
|
||||||
RewriteRule ^odzyskiwanie-hasla$ index.php?module=shop_client&action=recover_password [L]
|
|
||||||
RewriteRule ^panel-klienta/zamowienia$ index.php?module=shop_client&action=client_orders [L]
|
|
||||||
RewriteRule ^panel-klienta/adresy$ index.php?module=shop_client&action=client_addresses [L]
|
|
||||||
RewriteRule ^panel-klienta/nowy-adres$ index.php?module=shop_client&action=address_edit [L]
|
|
||||||
RewriteRule ^panel-klienta/edytuj-adres/([0-9]*)$ index.php?module=shop_client&action=address_edit&id=$1 [L]
|
|
||||||
RewriteRule ^panel-klienta/usun-adres/([0-9]*)$ index.php?module=shop_client&action=address_delete&id=$1 [L]
|
|
||||||
RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]
|
|
||||||
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopBasket/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopClient/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopProduct/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopCoupon/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/search/(.*)/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
|
|
||||||
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopBasket/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopClient/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopProduct/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/shopCoupon/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
RewriteCond %{REQUEST_URI} ^/search/(.*) [NC]
|
|
||||||
RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L]
|
|
||||||
|
|
||||||
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php
|
|
||||||
RewriteRule ^ /%1 [R=301,L]
|
|
||||||
{HTACCESS_CACHE}
|
|
||||||
<Files *.conf>
|
|
||||||
Order Deny,Allow
|
|
||||||
Deny from all
|
|
||||||
</Files>
|
|
||||||
<Files *.log>
|
|
||||||
Order Deny,Allow
|
|
||||||
Deny from all
|
|
||||||
</Files>
|
|
||||||
<Files *.ini>
|
|
||||||
Order Deny,Allow
|
|
||||||
Deny from all
|
|
||||||
</Files>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
2024-01-05 08:31:37 | 157 | 2024/01/001 | 12.30 | pyziak84@gmail.com
|
|
||||||
<pre></pre>
|
|
||||||
|
|
||||||
2024-01-05 08:36:54 | 157 | 2024/01/001 | 12.30 | pyziak84@gmail.com
|
|
||||||
{"id":516}
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
2024-08-20 19:28:32 | 159 | 2024/08/002 | 70.99 | pyziak84@gmail.com
|
|
||||||
[]
|
|
||||||
|
|
||||||
2024-08-20 19:42:45 | 160 | 2024/08/003 | 59.99 | pyziak84@gmail.com
|
|
||||||
{"updates":1}
|
|
||||||
|
|
||||||
1
migrations/0.316.sql
Normal file
1
migrations/0.316.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `pp_shop_products_custom_fields` ADD COLUMN `type` VARCHAR(30) NOT NULL DEFAULT '' AFTER `name`;
|
||||||
48
migrations/0.324.sql
Normal file
48
migrations/0.324.sql
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
-- System kolejki zadań cron
|
||||||
|
-- Wersja: 0.324
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pp_cron_jobs (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
job_type VARCHAR(50) NOT NULL,
|
||||||
|
status ENUM('pending','processing','completed','failed','cancelled') NOT NULL DEFAULT 'pending',
|
||||||
|
priority TINYINT UNSIGNED NOT NULL DEFAULT 100,
|
||||||
|
payload TEXT NULL,
|
||||||
|
result TEXT NULL,
|
||||||
|
attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
max_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 10,
|
||||||
|
last_error VARCHAR(500) NULL,
|
||||||
|
scheduled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
started_at DATETIME NULL,
|
||||||
|
completed_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_status_priority_scheduled (status, priority, scheduled_at),
|
||||||
|
INDEX idx_job_type (job_type),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pp_cron_schedules (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
job_type VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
interval_seconds INT UNSIGNED NOT NULL,
|
||||||
|
priority TINYINT UNSIGNED NOT NULL DEFAULT 100,
|
||||||
|
max_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 3,
|
||||||
|
payload TEXT NULL,
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
last_run_at DATETIME NULL,
|
||||||
|
next_run_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_enabled_next_run (enabled, next_run_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
|
-- Harmonogramy zadań
|
||||||
|
INSERT INTO pp_cron_schedules (job_type, interval_seconds, priority, max_attempts) VALUES
|
||||||
|
('apilo_token_keepalive', 240, 10, 3),
|
||||||
|
('apilo_send_order', 60, 40, 10),
|
||||||
|
('apilo_product_sync', 600, 100, 3),
|
||||||
|
('apilo_pricelist_sync', 3600, 100, 3),
|
||||||
|
('apilo_status_poll', 600, 100, 3),
|
||||||
|
('price_history', 86400, 100, 3),
|
||||||
|
('order_analysis', 600, 100, 3),
|
||||||
|
('trustmate_invitation', 600, 200, 3),
|
||||||
|
('google_xml_feed', 3600, 200, 3);
|
||||||
5
migrations/0.329.sql
Normal file
5
migrations/0.329.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE pp_routes
|
||||||
|
ADD COLUMN category_id INT NULL AFTER product_id,
|
||||||
|
ADD COLUMN page_id INT NULL AFTER category_id,
|
||||||
|
ADD COLUMN article_id INT NULL AFTER page_id,
|
||||||
|
ADD COLUMN type VARCHAR(20) NULL AFTER article_id;
|
||||||
BIN
templates/.DS_Store
vendored
BIN
templates/.DS_Store
vendored
Binary file not shown.
@@ -179,7 +179,7 @@
|
|||||||
'id': <?= (int)$product['product_id'];?>,
|
'id': <?= (int)$product['product_id'];?>,
|
||||||
'name': '<?= $product['name'];?>',
|
'name': '<?= $product['name'];?>',
|
||||||
'quantity': <?= $product['quantity'];?>,
|
'quantity': <?= $product['quantity'];?>,
|
||||||
'price': <?= $product['price_brutto_promo'];?>
|
'price': <?= ((float)$product['price_brutto_promo'] > 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?>
|
||||||
}<? if ( $product != end( $this -> order['products'] ) ) echo ',';?>
|
}<? if ( $product != end( $this -> order['products'] ) ) echo ',';?>
|
||||||
<? endforeach;?>
|
<? endforeach;?>
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ class ArticleRepositoryTest extends TestCase
|
|||||||
$mockDb = $this->createMock(\medoo::class);
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
$deleteCalls = [];
|
$deleteCalls = [];
|
||||||
|
|
||||||
$mockDb->expects($this->exactly(5))
|
$mockDb->expects($this->exactly(6))
|
||||||
->method('delete')
|
->method('delete')
|
||||||
->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
|
->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
|
||||||
$deleteCalls[] = ['table' => $table, 'where' => $where];
|
$deleteCalls[] = ['table' => $table, 'where' => $where];
|
||||||
@@ -524,12 +524,13 @@ class ArticleRepositoryTest extends TestCase
|
|||||||
$result = $repository->deletePermanently(77);
|
$result = $repository->deletePermanently(77);
|
||||||
|
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
$this->assertCount(5, $deleteCalls);
|
$this->assertCount(6, $deleteCalls);
|
||||||
$this->assertSame('pp_articles_pages', $deleteCalls[0]['table']);
|
$this->assertSame('pp_articles_pages', $deleteCalls[0]['table']);
|
||||||
$this->assertSame('pp_articles_langs', $deleteCalls[1]['table']);
|
$this->assertSame('pp_articles_langs', $deleteCalls[1]['table']);
|
||||||
$this->assertSame('pp_articles_images', $deleteCalls[2]['table']);
|
$this->assertSame('pp_articles_images', $deleteCalls[2]['table']);
|
||||||
$this->assertSame('pp_articles_files', $deleteCalls[3]['table']);
|
$this->assertSame('pp_articles_files', $deleteCalls[3]['table']);
|
||||||
$this->assertSame('pp_articles', $deleteCalls[4]['table']);
|
$this->assertSame('pp_routes', $deleteCalls[4]['table']);
|
||||||
|
$this->assertSame('pp_articles', $deleteCalls[5]['table']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPagesSummaryForArticlesBuildsLabels(): void
|
public function testPagesSummaryForArticlesBuildsLabels(): void
|
||||||
|
|||||||
@@ -175,14 +175,19 @@ class CategoryRepositoryTest extends TestCase
|
|||||||
$mockDb = $this->createMock(\medoo::class);
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
|
||||||
$mockDb->method('count')->willReturn(0);
|
$mockDb->method('count')->willReturn(0);
|
||||||
$mockDb->expects($this->once())
|
$deleteCalls = [];
|
||||||
|
$mockDb->expects($this->exactly(2))
|
||||||
->method('delete')
|
->method('delete')
|
||||||
->with('pp_shop_categories', ['id' => 8])
|
->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
|
||||||
->willReturn(true);
|
$deleteCalls[] = ['table' => $table, 'where' => $where];
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
$repository = new CategoryRepository($mockDb);
|
$repository = new CategoryRepository($mockDb);
|
||||||
|
|
||||||
$this->assertTrue($repository->categoryDelete(8));
|
$this->assertTrue($repository->categoryDelete(8));
|
||||||
|
$this->assertSame('pp_shop_categories', $deleteCalls[0]['table']);
|
||||||
|
$this->assertSame('pp_routes', $deleteCalls[1]['table']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCategoryTitleReturnsEmptyWhenNotFound(): void
|
public function testCategoryTitleReturnsEmptyWhenNotFound(): void
|
||||||
|
|||||||
301
tests/Unit/Domain/CronJob/CronJobProcessorTest.php
Normal file
301
tests/Unit/Domain/CronJob/CronJobProcessorTest.php
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Domain\CronJob;
|
||||||
|
|
||||||
|
use Domain\CronJob\CronJobProcessor;
|
||||||
|
use Domain\CronJob\CronJobRepository;
|
||||||
|
use Domain\CronJob\CronJobType;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CronJobProcessorTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var \PHPUnit\Framework\MockObject\MockObject|CronJobRepository */
|
||||||
|
private $mockRepo;
|
||||||
|
|
||||||
|
/** @var CronJobProcessor */
|
||||||
|
private $processor;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo = $this->createMock(CronJobRepository::class);
|
||||||
|
$this->processor = new CronJobProcessor($this->mockRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- registerHandler ---
|
||||||
|
|
||||||
|
public function testRegisterHandlerAndProcessJob(): void
|
||||||
|
{
|
||||||
|
$handlerCalled = false;
|
||||||
|
|
||||||
|
$this->processor->registerHandler('test_job', function ($payload) use (&$handlerCalled) {
|
||||||
|
$handlerCalled = true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([
|
||||||
|
['id' => 1, 'job_type' => 'test_job', 'payload' => null, 'attempts' => 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->once())->method('markCompleted')->with(1, null);
|
||||||
|
|
||||||
|
$stats = $this->processor->processQueue(1);
|
||||||
|
|
||||||
|
$this->assertTrue($handlerCalled);
|
||||||
|
$this->assertSame(1, $stats['processed']);
|
||||||
|
$this->assertSame(1, $stats['succeeded']);
|
||||||
|
$this->assertSame(0, $stats['failed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- processQueue ---
|
||||||
|
|
||||||
|
public function testProcessQueueReturnsEmptyStatsWhenNoJobs(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([]);
|
||||||
|
|
||||||
|
$stats = $this->processor->processQueue(5);
|
||||||
|
|
||||||
|
$this->assertSame(0, $stats['processed']);
|
||||||
|
$this->assertSame(0, $stats['succeeded']);
|
||||||
|
$this->assertSame(0, $stats['failed']);
|
||||||
|
$this->assertSame(0, $stats['skipped']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessQueueHandlerReturnsFalse(): void
|
||||||
|
{
|
||||||
|
$this->processor->registerHandler('fail_job', function ($payload) {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([
|
||||||
|
['id' => 2, 'job_type' => 'fail_job', 'payload' => null, 'attempts' => 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->once())->method('markFailed')
|
||||||
|
->with(2, 'Handler returned false', 1);
|
||||||
|
|
||||||
|
$stats = $this->processor->processQueue(1);
|
||||||
|
|
||||||
|
$this->assertSame(1, $stats['failed']);
|
||||||
|
$this->assertSame(0, $stats['succeeded']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessQueueHandlerThrowsException(): void
|
||||||
|
{
|
||||||
|
$this->processor->registerHandler('error_job', function ($payload) {
|
||||||
|
throw new \RuntimeException('Connection failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([
|
||||||
|
['id' => 3, 'job_type' => 'error_job', 'payload' => null, 'attempts' => 2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->once())->method('markFailed')
|
||||||
|
->with(3, 'Connection failed', 2);
|
||||||
|
|
||||||
|
$stats = $this->processor->processQueue(1);
|
||||||
|
|
||||||
|
$this->assertSame(1, $stats['failed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessQueueNoHandlerRegistered(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([
|
||||||
|
['id' => 4, 'job_type' => 'unknown_job', 'payload' => null, 'attempts' => 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->once())->method('markFailed')
|
||||||
|
->with(4, $this->stringContains('No handler registered'), 1);
|
||||||
|
|
||||||
|
$stats = $this->processor->processQueue(1);
|
||||||
|
|
||||||
|
$this->assertSame(1, $stats['skipped']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessQueueHandlerReturnsArray(): void
|
||||||
|
{
|
||||||
|
$resultData = ['synced' => true, 'items' => 5];
|
||||||
|
|
||||||
|
$this->processor->registerHandler('array_job', function ($payload) use ($resultData) {
|
||||||
|
return $resultData;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([
|
||||||
|
['id' => 5, 'job_type' => 'array_job', 'payload' => null, 'attempts' => 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->once())->method('markCompleted')
|
||||||
|
->with(5, $resultData);
|
||||||
|
|
||||||
|
$stats = $this->processor->processQueue(1);
|
||||||
|
|
||||||
|
$this->assertSame(1, $stats['succeeded']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessQueuePassesPayloadToHandler(): void
|
||||||
|
{
|
||||||
|
$receivedPayload = null;
|
||||||
|
|
||||||
|
$this->processor->registerHandler('payload_job', function ($payload) use (&$receivedPayload) {
|
||||||
|
$receivedPayload = $payload;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([
|
||||||
|
['id' => 6, 'job_type' => 'payload_job', 'payload' => ['order_id' => 42], 'attempts' => 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->method('markCompleted');
|
||||||
|
|
||||||
|
$this->processor->processQueue(1);
|
||||||
|
|
||||||
|
$this->assertSame(['order_id' => 42], $receivedPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessQueueMultipleJobs(): void
|
||||||
|
{
|
||||||
|
$this->processor->registerHandler('ok_job', function ($payload) {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
$this->processor->registerHandler('fail_job', function ($payload) {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([
|
||||||
|
['id' => 10, 'job_type' => 'ok_job', 'payload' => null, 'attempts' => 1],
|
||||||
|
['id' => 11, 'job_type' => 'fail_job', 'payload' => null, 'attempts' => 1],
|
||||||
|
['id' => 12, 'job_type' => 'ok_job', 'payload' => null, 'attempts' => 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stats = $this->processor->processQueue(10);
|
||||||
|
|
||||||
|
$this->assertSame(3, $stats['processed']);
|
||||||
|
$this->assertSame(2, $stats['succeeded']);
|
||||||
|
$this->assertSame(1, $stats['failed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- createScheduledJobs ---
|
||||||
|
|
||||||
|
public function testCreateScheduledJobsFromDueSchedules(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo->method('getDueSchedules')->willReturn([
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'job_type' => 'price_history',
|
||||||
|
'interval_seconds' => 86400,
|
||||||
|
'priority' => 100,
|
||||||
|
'max_attempts' => 3,
|
||||||
|
'payload' => null,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->method('hasPendingJob')->willReturn(false);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->once())->method('enqueue')
|
||||||
|
->with('price_history', null, 100, 3);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->once())->method('touchSchedule')
|
||||||
|
->with(1, 86400);
|
||||||
|
|
||||||
|
$created = $this->processor->createScheduledJobs();
|
||||||
|
$this->assertSame(1, $created);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateScheduledJobsSkipsDuplicates(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo->method('getDueSchedules')->willReturn([
|
||||||
|
[
|
||||||
|
'id' => 2,
|
||||||
|
'job_type' => 'apilo_send_order',
|
||||||
|
'interval_seconds' => 60,
|
||||||
|
'priority' => 50,
|
||||||
|
'max_attempts' => 10,
|
||||||
|
'payload' => null,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->method('hasPendingJob')->willReturn(true);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->never())->method('enqueue');
|
||||||
|
// touchSchedule still called to prevent re-checking
|
||||||
|
$this->mockRepo->expects($this->once())->method('touchSchedule');
|
||||||
|
|
||||||
|
$created = $this->processor->createScheduledJobs();
|
||||||
|
$this->assertSame(0, $created);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateScheduledJobsWithPayload(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo->method('getDueSchedules')->willReturn([
|
||||||
|
[
|
||||||
|
'id' => 3,
|
||||||
|
'job_type' => 'custom_job',
|
||||||
|
'interval_seconds' => 600,
|
||||||
|
'priority' => 100,
|
||||||
|
'max_attempts' => 3,
|
||||||
|
'payload' => '{"key":"value"}',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->method('hasPendingJob')->willReturn(false);
|
||||||
|
|
||||||
|
$this->mockRepo->expects($this->once())->method('enqueue')
|
||||||
|
->with('custom_job', ['key' => 'value'], 100, 3);
|
||||||
|
|
||||||
|
$this->processor->createScheduledJobs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateScheduledJobsReturnsZeroWhenNoSchedules(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo->method('getDueSchedules')->willReturn([]);
|
||||||
|
|
||||||
|
$created = $this->processor->createScheduledJobs();
|
||||||
|
$this->assertSame(0, $created);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- run ---
|
||||||
|
|
||||||
|
public function testRunExecutesFullPipeline(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo->expects($this->once())->method('recoverStuck')->with(30);
|
||||||
|
$this->mockRepo->method('getDueSchedules')->willReturn([]);
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([]);
|
||||||
|
$this->mockRepo->expects($this->once())->method('cleanup')->with(30);
|
||||||
|
|
||||||
|
$stats = $this->processor->run(20);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('scheduled', $stats);
|
||||||
|
$this->assertArrayHasKey('processed', $stats);
|
||||||
|
$this->assertArrayHasKey('succeeded', $stats);
|
||||||
|
$this->assertArrayHasKey('failed', $stats);
|
||||||
|
$this->assertArrayHasKey('skipped', $stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRunReturnsScheduledCount(): void
|
||||||
|
{
|
||||||
|
$this->mockRepo->method('getDueSchedules')->willReturn([
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'job_type' => 'job_a',
|
||||||
|
'interval_seconds' => 60,
|
||||||
|
'priority' => 100,
|
||||||
|
'max_attempts' => 3,
|
||||||
|
'payload' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 2,
|
||||||
|
'job_type' => 'job_b',
|
||||||
|
'interval_seconds' => 120,
|
||||||
|
'priority' => 100,
|
||||||
|
'max_attempts' => 3,
|
||||||
|
'payload' => null,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockRepo->method('hasPendingJob')->willReturn(false);
|
||||||
|
$this->mockRepo->method('fetchNext')->willReturn([]);
|
||||||
|
|
||||||
|
$stats = $this->processor->run(20);
|
||||||
|
|
||||||
|
$this->assertSame(2, $stats['scheduled']);
|
||||||
|
}
|
||||||
|
}
|
||||||
385
tests/Unit/Domain/CronJob/CronJobRepositoryTest.php
Normal file
385
tests/Unit/Domain/CronJob/CronJobRepositoryTest.php
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Domain\CronJob;
|
||||||
|
|
||||||
|
use Domain\CronJob\CronJobRepository;
|
||||||
|
use Domain\CronJob\CronJobType;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CronJobRepositoryTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var \PHPUnit\Framework\MockObject\MockObject|\medoo */
|
||||||
|
private $mockDb;
|
||||||
|
|
||||||
|
/** @var CronJobRepository */
|
||||||
|
private $repo;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->mockDb = $this->createMock(\medoo::class);
|
||||||
|
$this->repo = new CronJobRepository($this->mockDb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- enqueue ---
|
||||||
|
|
||||||
|
public function testEnqueueInsertsJobAndReturnsId(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('insert')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return $data['job_type'] === 'apilo_send_order'
|
||||||
|
&& $data['status'] === 'pending'
|
||||||
|
&& $data['priority'] === 50
|
||||||
|
&& $data['max_attempts'] === 10
|
||||||
|
&& isset($data['scheduled_at']);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->mockDb->method('id')->willReturn('42');
|
||||||
|
|
||||||
|
$id = $this->repo->enqueue('apilo_send_order', null, CronJobType::PRIORITY_HIGH);
|
||||||
|
|
||||||
|
$this->assertSame(42, $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnqueueWithPayloadEncodesJson(): void
|
||||||
|
{
|
||||||
|
$payload = ['order_id' => 123, 'action' => 'sync'];
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('insert')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) use ($payload) {
|
||||||
|
return $data['payload'] === json_encode($payload);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->mockDb->method('id')->willReturn('1');
|
||||||
|
|
||||||
|
$this->repo->enqueue('apilo_sync_payment', $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnqueueWithoutPayloadDoesNotSetPayloadKey(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('insert')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return !array_key_exists('payload', $data);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->mockDb->method('id')->willReturn('1');
|
||||||
|
|
||||||
|
$this->repo->enqueue('price_history');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnqueueWithScheduledAt(): void
|
||||||
|
{
|
||||||
|
$scheduled = '2026-03-01 10:00:00';
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('insert')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) use ($scheduled) {
|
||||||
|
return $data['scheduled_at'] === $scheduled;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->mockDb->method('id')->willReturn('1');
|
||||||
|
|
||||||
|
$this->repo->enqueue('price_history', null, CronJobType::PRIORITY_NORMAL, 10, $scheduled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnqueueReturnsNullOnFailure(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->method('insert');
|
||||||
|
$this->mockDb->method('id')->willReturn(null);
|
||||||
|
|
||||||
|
$id = $this->repo->enqueue('test_job');
|
||||||
|
$this->assertNull($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fetchNext ---
|
||||||
|
|
||||||
|
public function testFetchNextReturnsEmptyArrayWhenNoJobs(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->method('select')->willReturn([]);
|
||||||
|
|
||||||
|
$result = $this->repo->fetchNext(5);
|
||||||
|
$this->assertSame([], $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFetchNextUpdatesStatusToProcessing(): void
|
||||||
|
{
|
||||||
|
$pendingJobs = [
|
||||||
|
['id' => 1, 'job_type' => 'test', 'status' => 'pending', 'payload' => null],
|
||||||
|
['id' => 2, 'job_type' => 'test2', 'status' => 'pending', 'payload' => '{"x":1}'],
|
||||||
|
];
|
||||||
|
$claimedJobs = [
|
||||||
|
['id' => 1, 'job_type' => 'test', 'status' => 'processing', 'payload' => null],
|
||||||
|
['id' => 2, 'job_type' => 'test2', 'status' => 'processing', 'payload' => '{"x":1}'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->mockDb->method('select')
|
||||||
|
->willReturnOnConsecutiveCalls($pendingJobs, $claimedJobs);
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return $data['status'] === 'processing'
|
||||||
|
&& isset($data['started_at']);
|
||||||
|
}),
|
||||||
|
$this->callback(function ($where) {
|
||||||
|
return $where['id'] === [1, 2]
|
||||||
|
&& $where['status'] === 'pending';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->repo->fetchNext(5);
|
||||||
|
|
||||||
|
$this->assertCount(2, $result);
|
||||||
|
$this->assertSame('processing', $result[0]['status']);
|
||||||
|
$this->assertSame('processing', $result[1]['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFetchNextDecodesPayloadJson(): void
|
||||||
|
{
|
||||||
|
$jobs = [
|
||||||
|
['id' => 1, 'job_type' => 'test', 'status' => 'pending', 'payload' => '{"order_id":99}'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->mockDb->method('select')->willReturn($jobs);
|
||||||
|
$this->mockDb->method('update');
|
||||||
|
|
||||||
|
$result = $this->repo->fetchNext(1);
|
||||||
|
|
||||||
|
$this->assertSame(['order_id' => 99], $result[0]['payload']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- markCompleted ---
|
||||||
|
|
||||||
|
public function testMarkCompletedUpdatesStatus(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return $data['status'] === 'completed'
|
||||||
|
&& isset($data['completed_at']);
|
||||||
|
}),
|
||||||
|
['id' => 5]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->repo->markCompleted(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMarkCompletedWithResult(): void
|
||||||
|
{
|
||||||
|
$result = ['synced' => true, 'count' => 3];
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) use ($result) {
|
||||||
|
return $data['result'] === json_encode($result);
|
||||||
|
}),
|
||||||
|
['id' => 7]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->repo->markCompleted(7, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- markFailed ---
|
||||||
|
|
||||||
|
public function testMarkFailedWithRetriesLeft(): void
|
||||||
|
{
|
||||||
|
// Job with attempts < max_attempts → reschedule with backoff
|
||||||
|
$this->mockDb->method('get')->willReturn([
|
||||||
|
'max_attempts' => 10,
|
||||||
|
'attempts' => 2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return $data['status'] === 'pending'
|
||||||
|
&& isset($data['scheduled_at'])
|
||||||
|
&& isset($data['last_error']);
|
||||||
|
}),
|
||||||
|
['id' => 3]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->repo->markFailed(3, 'Connection timeout', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMarkFailedWhenMaxAttemptsReached(): void
|
||||||
|
{
|
||||||
|
// Job with attempts >= max_attempts → permanent failure
|
||||||
|
$this->mockDb->method('get')->willReturn([
|
||||||
|
'max_attempts' => 3,
|
||||||
|
'attempts' => 3,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return $data['status'] === 'failed'
|
||||||
|
&& isset($data['completed_at']);
|
||||||
|
}),
|
||||||
|
['id' => 4]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->repo->markFailed(4, 'Max retries exceeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMarkFailedTruncatesErrorTo500Chars(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->method('get')->willReturn([
|
||||||
|
'max_attempts' => 10,
|
||||||
|
'attempts' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$longError = str_repeat('x', 600);
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return mb_strlen($data['last_error']) <= 500;
|
||||||
|
}),
|
||||||
|
['id' => 1]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->repo->markFailed(1, $longError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- hasPendingJob ---
|
||||||
|
|
||||||
|
public function testHasPendingJobReturnsTrueWhenExists(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->method('count')
|
||||||
|
->with('pp_cron_jobs', $this->callback(function ($where) {
|
||||||
|
return $where['job_type'] === 'apilo_sync_payment'
|
||||||
|
&& $where['status'] === ['pending', 'processing'];
|
||||||
|
}))
|
||||||
|
->willReturn(1);
|
||||||
|
|
||||||
|
$this->assertTrue($this->repo->hasPendingJob('apilo_sync_payment'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasPendingJobReturnsFalseWhenNone(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->method('count')->willReturn(0);
|
||||||
|
|
||||||
|
$this->assertFalse($this->repo->hasPendingJob('apilo_sync_payment'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasPendingJobWithPayloadMatch(): void
|
||||||
|
{
|
||||||
|
$payload = ['order_id' => 42];
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('count')
|
||||||
|
->with('pp_cron_jobs', $this->callback(function ($where) use ($payload) {
|
||||||
|
return $where['payload'] === json_encode($payload);
|
||||||
|
}))
|
||||||
|
->willReturn(1);
|
||||||
|
|
||||||
|
$this->assertTrue($this->repo->hasPendingJob('apilo_sync_payment', $payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- cleanup ---
|
||||||
|
|
||||||
|
public function testCleanupDeletesOldCompletedJobs(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('delete')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($where) {
|
||||||
|
return $where['status'] === ['completed', 'failed', 'cancelled']
|
||||||
|
&& isset($where['updated_at[<]']);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->repo->cleanup(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- recoverStuck ---
|
||||||
|
|
||||||
|
public function testRecoverStuckResetsProcessingJobs(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_cron_jobs',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return $data['status'] === 'pending'
|
||||||
|
&& $data['started_at'] === null;
|
||||||
|
}),
|
||||||
|
$this->callback(function ($where) {
|
||||||
|
return $where['status'] === 'processing'
|
||||||
|
&& isset($where['started_at[<]']);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->repo->recoverStuck(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- getDueSchedules ---
|
||||||
|
|
||||||
|
public function testGetDueSchedulesReturnsEnabledSchedules(): void
|
||||||
|
{
|
||||||
|
$schedules = [
|
||||||
|
['id' => 1, 'job_type' => 'price_history', 'interval_seconds' => 86400],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('select')
|
||||||
|
->with(
|
||||||
|
'pp_cron_schedules',
|
||||||
|
'*',
|
||||||
|
$this->callback(function ($where) {
|
||||||
|
return $where['enabled'] === 1
|
||||||
|
&& isset($where['OR']);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
->willReturn($schedules);
|
||||||
|
|
||||||
|
$result = $this->repo->getDueSchedules();
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- touchSchedule ---
|
||||||
|
|
||||||
|
public function testTouchScheduleUpdatesTimestamps(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_cron_schedules',
|
||||||
|
$this->callback(function ($data) {
|
||||||
|
return isset($data['last_run_at'])
|
||||||
|
&& isset($data['next_run_at']);
|
||||||
|
}),
|
||||||
|
['id' => 5]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->repo->touchSchedule(5, 3600);
|
||||||
|
}
|
||||||
|
}
|
||||||
97
tests/Unit/Domain/CronJob/CronJobTypeTest.php
Normal file
97
tests/Unit/Domain/CronJob/CronJobTypeTest.php
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Domain\CronJob;
|
||||||
|
|
||||||
|
use Domain\CronJob\CronJobType;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CronJobTypeTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testAllTypesReturnsAllJobTypes(): void
|
||||||
|
{
|
||||||
|
$types = CronJobType::allTypes();
|
||||||
|
|
||||||
|
$this->assertContains('apilo_token_keepalive', $types);
|
||||||
|
$this->assertContains('apilo_send_order', $types);
|
||||||
|
$this->assertContains('apilo_sync_payment', $types);
|
||||||
|
$this->assertContains('apilo_sync_status', $types);
|
||||||
|
$this->assertContains('apilo_product_sync', $types);
|
||||||
|
$this->assertContains('apilo_pricelist_sync', $types);
|
||||||
|
$this->assertContains('apilo_status_poll', $types);
|
||||||
|
$this->assertContains('price_history', $types);
|
||||||
|
$this->assertContains('order_analysis', $types);
|
||||||
|
$this->assertContains('trustmate_invitation', $types);
|
||||||
|
$this->assertContains('google_xml_feed', $types);
|
||||||
|
$this->assertCount(11, $types);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllStatusesReturnsAllStatuses(): void
|
||||||
|
{
|
||||||
|
$statuses = CronJobType::allStatuses();
|
||||||
|
|
||||||
|
$this->assertContains('pending', $statuses);
|
||||||
|
$this->assertContains('processing', $statuses);
|
||||||
|
$this->assertContains('completed', $statuses);
|
||||||
|
$this->assertContains('failed', $statuses);
|
||||||
|
$this->assertContains('cancelled', $statuses);
|
||||||
|
$this->assertCount(5, $statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPriorityConstants(): void
|
||||||
|
{
|
||||||
|
$this->assertSame(10, CronJobType::PRIORITY_CRITICAL);
|
||||||
|
$this->assertSame(40, CronJobType::PRIORITY_SEND_ORDER);
|
||||||
|
$this->assertSame(50, CronJobType::PRIORITY_HIGH);
|
||||||
|
$this->assertSame(100, CronJobType::PRIORITY_NORMAL);
|
||||||
|
$this->assertSame(200, CronJobType::PRIORITY_LOW);
|
||||||
|
|
||||||
|
// Lower value = higher priority
|
||||||
|
$this->assertLessThan(CronJobType::PRIORITY_SEND_ORDER, CronJobType::PRIORITY_CRITICAL);
|
||||||
|
$this->assertLessThan(CronJobType::PRIORITY_HIGH, CronJobType::PRIORITY_SEND_ORDER);
|
||||||
|
$this->assertLessThan(CronJobType::PRIORITY_NORMAL, CronJobType::PRIORITY_HIGH);
|
||||||
|
$this->assertLessThan(CronJobType::PRIORITY_LOW, CronJobType::PRIORITY_NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateBackoffExponential(): void
|
||||||
|
{
|
||||||
|
// Attempt 1: 60s
|
||||||
|
$this->assertSame(60, CronJobType::calculateBackoff(1));
|
||||||
|
// Attempt 2: 120s
|
||||||
|
$this->assertSame(120, CronJobType::calculateBackoff(2));
|
||||||
|
// Attempt 3: 240s
|
||||||
|
$this->assertSame(240, CronJobType::calculateBackoff(3));
|
||||||
|
// Attempt 4: 480s
|
||||||
|
$this->assertSame(480, CronJobType::calculateBackoff(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateBackoffCapsAtMax(): void
|
||||||
|
{
|
||||||
|
// Very high attempt should cap at MAX_BACKOFF_SECONDS (3600)
|
||||||
|
$this->assertSame(3600, CronJobType::calculateBackoff(10));
|
||||||
|
$this->assertSame(3600, CronJobType::calculateBackoff(20));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testJobTypeConstantsMatchStrings(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('apilo_token_keepalive', CronJobType::APILO_TOKEN_KEEPALIVE);
|
||||||
|
$this->assertSame('apilo_send_order', CronJobType::APILO_SEND_ORDER);
|
||||||
|
$this->assertSame('apilo_sync_payment', CronJobType::APILO_SYNC_PAYMENT);
|
||||||
|
$this->assertSame('apilo_sync_status', CronJobType::APILO_SYNC_STATUS);
|
||||||
|
$this->assertSame('apilo_product_sync', CronJobType::APILO_PRODUCT_SYNC);
|
||||||
|
$this->assertSame('apilo_pricelist_sync', CronJobType::APILO_PRICELIST_SYNC);
|
||||||
|
$this->assertSame('apilo_status_poll', CronJobType::APILO_STATUS_POLL);
|
||||||
|
$this->assertSame('price_history', CronJobType::PRICE_HISTORY);
|
||||||
|
$this->assertSame('order_analysis', CronJobType::ORDER_ANALYSIS);
|
||||||
|
$this->assertSame('trustmate_invitation', CronJobType::TRUSTMATE_INVITATION);
|
||||||
|
$this->assertSame('google_xml_feed', CronJobType::GOOGLE_XML_FEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStatusConstantsMatchStrings(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('pending', CronJobType::STATUS_PENDING);
|
||||||
|
$this->assertSame('processing', CronJobType::STATUS_PROCESSING);
|
||||||
|
$this->assertSame('completed', CronJobType::STATUS_COMPLETED);
|
||||||
|
$this->assertSame('failed', CronJobType::STATUS_FAILED);
|
||||||
|
$this->assertSame('cancelled', CronJobType::STATUS_CANCELLED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -298,4 +298,69 @@ class IntegrationsRepositoryTest extends TestCase
|
|||||||
$this->assertSame('1', (string)$result[0]['id']);
|
$this->assertSame('1', (string)$result[0]['id']);
|
||||||
$this->assertSame('Przelew', (string)$result[0]['name']);
|
$this->assertSame('Przelew', (string)$result[0]['name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Logs ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testGetLogsReturnsItemsAndTotal(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('count')
|
||||||
|
->with('pp_log', $this->anything())
|
||||||
|
->willReturn(2);
|
||||||
|
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('select')
|
||||||
|
->with('pp_log', '*', $this->anything())
|
||||||
|
->willReturn([
|
||||||
|
['id' => 1, 'action' => 'send_order', 'message' => 'OK', 'date' => '2026-01-01 12:00:00'],
|
||||||
|
['id' => 2, 'action' => 'status_sync', 'message' => 'Synced', 'date' => '2026-01-02 12:00:00'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->repository->getLogs([], 'id', 'DESC', 1, 15);
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertArrayHasKey('items', $result);
|
||||||
|
$this->assertArrayHasKey('total', $result);
|
||||||
|
$this->assertCount(2, $result['items']);
|
||||||
|
$this->assertSame(2, $result['total']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLogsReturnsEmptyWhenNoResults(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->method('count')->willReturn(0);
|
||||||
|
$this->mockDb->method('select')->willReturn([]);
|
||||||
|
|
||||||
|
$result = $this->repository->getLogs([], 'id', 'DESC', 1, 15);
|
||||||
|
|
||||||
|
$this->assertSame(0, $result['total']);
|
||||||
|
$this->assertEmpty($result['items']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLogsHandlesNullFromSelect(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->method('count')->willReturn(0);
|
||||||
|
$this->mockDb->method('select')->willReturn(null);
|
||||||
|
|
||||||
|
$result = $this->repository->getLogs([], 'id', 'DESC', 1, 15);
|
||||||
|
|
||||||
|
$this->assertSame([], $result['items']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteLogCallsDelete(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('delete')
|
||||||
|
->with('pp_log', ['id' => 42]);
|
||||||
|
|
||||||
|
$this->assertTrue($this->repository->deleteLog(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testClearLogsDeletesAll(): void
|
||||||
|
{
|
||||||
|
$this->mockDb->expects($this->once())
|
||||||
|
->method('delete')
|
||||||
|
->with('pp_log', []);
|
||||||
|
|
||||||
|
$this->assertTrue($this->repository->clearLogs());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use Domain\Order\OrderRepository;
|
|||||||
use Domain\Product\ProductRepository;
|
use Domain\Product\ProductRepository;
|
||||||
use Domain\Settings\SettingsRepository;
|
use Domain\Settings\SettingsRepository;
|
||||||
use Domain\Transport\TransportRepository;
|
use Domain\Transport\TransportRepository;
|
||||||
|
use Domain\CronJob\CronJobRepository;
|
||||||
|
use Domain\CronJob\CronJobType;
|
||||||
|
|
||||||
class OrderAdminServiceTest extends TestCase
|
class OrderAdminServiceTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -227,4 +229,16 @@ class OrderAdminServiceTest extends TestCase
|
|||||||
$service = $this->createService(null, null, $settingsRepo);
|
$service = $this->createService(null, null, $settingsRepo);
|
||||||
$this->assertSame(150.0, $service->getFreeDeliveryThreshold());
|
$this->assertSame(150.0, $service->getFreeDeliveryThreshold());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// queueApiloSync — DB-based via CronJobRepository
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function testConstructorAcceptsCronJobRepo(): void
|
||||||
|
{
|
||||||
|
$orderRepo = $this->createMock(OrderRepository::class);
|
||||||
|
$cronJobRepo = $this->createMock(CronJobRepository::class);
|
||||||
|
$service = new OrderAdminService($orderRepo, null, null, null, $cronJobRepo);
|
||||||
|
$this->assertInstanceOf(OrderAdminService::class, $service);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,33 @@ class IntegrationsControllerTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHasLogsMethods(): void
|
||||||
|
{
|
||||||
|
$methods = [
|
||||||
|
'logs',
|
||||||
|
'logs_clear',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
$this->assertTrue(
|
||||||
|
method_exists($this->controller, $method),
|
||||||
|
"Method $method does not exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLogsReturnsString(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
$this->assertEquals('string', (string) $reflection->getMethod('logs')->getReturnType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLogsClearReturnsVoid(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
$this->assertEquals('void', (string) $reflection->getMethod('logs_clear')->getReturnType());
|
||||||
|
}
|
||||||
|
|
||||||
public function testHasAllApiloSettingsMethods(): void
|
public function testHasAllApiloSettingsMethods(): void
|
||||||
{
|
{
|
||||||
$methods = [
|
$methods = [
|
||||||
|
|||||||
@@ -53,4 +53,15 @@ class ProductArchiveControllerTest extends TestCase
|
|||||||
$this->assertCount(1, $params);
|
$this->assertCount(1, $params);
|
||||||
$this->assertEquals('Domain\Product\ProductRepository', $params[0]->getType()->getName());
|
$this->assertEquals('Domain\Product\ProductRepository', $params[0]->getType()->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHasBulkDeletePermanentMethod(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'bulk_delete_permanent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBulkDeletePermanentMethodReturnType(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
$this->assertEquals('void', (string)$reflection->getMethod('bulk_delete_permanent')->getReturnType());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace Tests\Unit\api\Controllers;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use api\Controllers\DictionariesApiController;
|
use api\Controllers\DictionariesApiController;
|
||||||
use Domain\Attribute\AttributeRepository;
|
use Domain\Attribute\AttributeRepository;
|
||||||
|
use Domain\Producer\ProducerRepository;
|
||||||
use Domain\ShopStatus\ShopStatusRepository;
|
use Domain\ShopStatus\ShopStatusRepository;
|
||||||
use Domain\Transport\TransportRepository;
|
use Domain\Transport\TransportRepository;
|
||||||
use Domain\PaymentMethod\PaymentMethodRepository;
|
use Domain\PaymentMethod\PaymentMethodRepository;
|
||||||
@@ -14,6 +15,7 @@ class DictionariesApiControllerTest extends TestCase
|
|||||||
private $mockTransportRepo;
|
private $mockTransportRepo;
|
||||||
private $mockPaymentRepo;
|
private $mockPaymentRepo;
|
||||||
private $mockAttrRepo;
|
private $mockAttrRepo;
|
||||||
|
private $mockProducerRepo;
|
||||||
private $controller;
|
private $controller;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
@@ -22,12 +24,14 @@ class DictionariesApiControllerTest extends TestCase
|
|||||||
$this->mockTransportRepo = $this->createMock(TransportRepository::class);
|
$this->mockTransportRepo = $this->createMock(TransportRepository::class);
|
||||||
$this->mockPaymentRepo = $this->createMock(PaymentMethodRepository::class);
|
$this->mockPaymentRepo = $this->createMock(PaymentMethodRepository::class);
|
||||||
$this->mockAttrRepo = $this->createMock(AttributeRepository::class);
|
$this->mockAttrRepo = $this->createMock(AttributeRepository::class);
|
||||||
|
$this->mockProducerRepo = $this->createMock(ProducerRepository::class);
|
||||||
|
|
||||||
$this->controller = new DictionariesApiController(
|
$this->controller = new DictionariesApiController(
|
||||||
$this->mockStatusRepo,
|
$this->mockStatusRepo,
|
||||||
$this->mockTransportRepo,
|
$this->mockTransportRepo,
|
||||||
$this->mockPaymentRepo,
|
$this->mockPaymentRepo,
|
||||||
$this->mockAttrRepo
|
$this->mockAttrRepo,
|
||||||
|
$this->mockProducerRepo
|
||||||
);
|
);
|
||||||
|
|
||||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||||
@@ -186,4 +190,52 @@ class DictionariesApiControllerTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame(405, http_response_code());
|
$this->assertSame(405, http_response_code());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testEnsureAttributeRejectsGetMethod(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$this->controller->ensure_attribute();
|
||||||
|
ob_get_clean();
|
||||||
|
|
||||||
|
$this->assertSame(405, http_response_code());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnsureAttributeReturns400WhenNoBody(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$this->controller->ensure_attribute();
|
||||||
|
$output = ob_get_clean();
|
||||||
|
|
||||||
|
$this->assertSame(400, http_response_code());
|
||||||
|
$json = json_decode($output, true);
|
||||||
|
$this->assertSame('BAD_REQUEST', $json['code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnsureAttributeValueRejectsGetMethod(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$this->controller->ensure_attribute_value();
|
||||||
|
ob_get_clean();
|
||||||
|
|
||||||
|
$this->assertSame(405, http_response_code());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnsureAttributeValueReturns400WhenNoBody(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$this->controller->ensure_attribute_value();
|
||||||
|
$output = ob_get_clean();
|
||||||
|
|
||||||
|
$this->assertSame(400, http_response_code());
|
||||||
|
$json = json_decode($output, true);
|
||||||
|
$this->assertSame('BAD_REQUEST', $json['code']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,6 +351,19 @@ class ProductsApiControllerTest extends TestCase
|
|||||||
$this->assertSame('5901234123457', $result['ean']);
|
$this->assertSame('5901234123457', $result['ean']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMapApiToFormDataPreservesZeroBasePriceForSaveProduct(): void
|
||||||
|
{
|
||||||
|
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$result = $method->invoke($this->controller, [
|
||||||
|
'price_brutto' => 0.0,
|
||||||
|
'languages' => ['pl' => ['name' => 'Zero']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('0', $result['price_brutto']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testMapApiToFormDataMapsCategories(): void
|
public function testMapApiToFormDataMapsCategories(): void
|
||||||
{
|
{
|
||||||
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
|
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
|
||||||
|
|||||||
BIN
updates/0.30/ver_0.309.zip
Normal file
BIN
updates/0.30/ver_0.309.zip
Normal file
Binary file not shown.
35
updates/0.30/ver_0.309_manifest.json
Normal file
35
updates/0.30/ver_0.309_manifest.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"changelog": "NEW - ApiloLogger (logowanie operacji Apilo do pp_log), cache-busting CSS/JS w admin panelu, poprawki UI listy produktow, clipboard API",
|
||||||
|
"version": "0.309",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
"autoload/Domain/Integrations/ApiloLogger.php"
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"admin/index.php",
|
||||||
|
"admin/layout/style-css/style.css",
|
||||||
|
"admin/layout/style-css/style.css.map",
|
||||||
|
"admin/layout/style-scss/style.scss",
|
||||||
|
"admin/templates/site/main-layout.php",
|
||||||
|
"autoload/Domain/Order/OrderAdminService.php",
|
||||||
|
"autoload/admin/Controllers/ShopProductController.php",
|
||||||
|
"cron.php",
|
||||||
|
"libraries/functions.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:87a3db1a6038da742d21d92b65c21156493af52543b3810694ea91e000acf920",
|
||||||
|
"sql": [
|
||||||
|
"ALTER TABLE pp_log ADD COLUMN `action` VARCHAR(100) NULL DEFAULT NULL AFTER `id`;",
|
||||||
|
"ALTER TABLE pp_log ADD COLUMN `order_id` INT NULL DEFAULT NULL AFTER `action`;",
|
||||||
|
"ALTER TABLE pp_log ADD COLUMN `context` TEXT NULL DEFAULT NULL AFTER `message`;",
|
||||||
|
"ALTER TABLE pp_log ADD INDEX `idx_action` (`action`);",
|
||||||
|
"ALTER TABLE pp_log ADD INDEX `idx_order_id` (`order_id`);"
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.310.zip
Normal file
BIN
updates/0.30/ver_0.310.zip
Normal file
Binary file not shown.
25
updates/0.30/ver_0.310_manifest.json
Normal file
25
updates/0.30/ver_0.310_manifest.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"changelog": "NEW - Zakladka Logi w sekcji Integracje (podglad pp_log z paginacja, sortowaniem, filtrami)",
|
||||||
|
"version": "0.310",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
"admin/templates/integrations/logs.php"
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"admin/templates/site/main-layout.php",
|
||||||
|
"autoload/Domain/Integrations/IntegrationsRepository.php",
|
||||||
|
"autoload/admin/Controllers/IntegrationsController.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:e3b14e239230548aba203a83f01c91b00651e5114e92e162f6da7389c6a92975",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.311.zip
Normal file
BIN
updates/0.30/ver_0.311.zip
Normal file
Binary file not shown.
27
updates/0.30/ver_0.311_manifest.json
Normal file
27
updates/0.30/ver_0.311_manifest.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"changelog": "FIX - race condition callback płatności Apilo, persistence filtrów tabel admin, poprawki cen zamówień",
|
||||||
|
"version": "0.311",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"admin/templates/components/table-list.php",
|
||||||
|
"admin/templates/shop-order/order-details.php",
|
||||||
|
"autoload/Domain/Order/OrderAdminService.php",
|
||||||
|
"cron.php",
|
||||||
|
"templates/shop-order/order-details.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:542f599e844d48bba1378fbe91c06ef00d5487aca56af118ad9c9ea27343ebdb",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.312.zip
Normal file
BIN
updates/0.30/ver_0.312.zip
Normal file
Binary file not shown.
23
updates/0.30/ver_0.312_manifest.json
Normal file
23
updates/0.30/ver_0.312_manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"changelog": "FIX - krytyczne bugi integracji Apilo: curl_getinfo po curl_close, nieskończona pętla wysyłki, ceny 0.00 PLN, walidacja cen",
|
||||||
|
"version": "0.312",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"cron.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:07f9efd02a6a83327ab8dd9403e0c072a5f38b680d6e3f6c67a96d2af8b8fc85",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.313.zip
Normal file
BIN
updates/0.30/ver_0.313.zip
Normal file
Binary file not shown.
24
updates/0.30/ver_0.313_manifest.json
Normal file
24
updates/0.30/ver_0.313_manifest.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"changelog": "FIX - sync płatności Apilo (int cast na apilo_order_id PPxxxxxx dawał 0) + logowanie decyzji sync do pp_log",
|
||||||
|
"version": "0.313",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"admin/templates/shop-order/order-details.php",
|
||||||
|
"autoload/Domain/Order/OrderAdminService.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:f344da1f3270abfc63653f8912ec1abbc006154db784cfee5a565fc0daaa75f8",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.314.zip
Normal file
BIN
updates/0.30/ver_0.314.zip
Normal file
Binary file not shown.
25
updates/0.30/ver_0.314_manifest.json
Normal file
25
updates/0.30/ver_0.314_manifest.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"changelog": "FIX - naprawa globalnej wyszukiwarki admin (Content-Type, Cache-Control, POST, try/catch), NEW - title strony z numerem zamówienia",
|
||||||
|
"version": "0.314",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"admin/templates/shop-order/order-details.php",
|
||||||
|
"admin/templates/site/main-layout.php",
|
||||||
|
"autoload/admin/Controllers/SettingsController.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:5ef21d158850db4036e3ee3ed4e9d2938d0451cd9b8602d26fd53163085a391f",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.315.zip
Normal file
BIN
updates/0.30/ver_0.315.zip
Normal file
Binary file not shown.
23
updates/0.30/ver_0.315_manifest.json
Normal file
23
updates/0.30/ver_0.315_manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"changelog": "FIX - PDOException w listowaniu atrybutow admin (SQLSTATE HY093)",
|
||||||
|
"version": "0.315",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"autoload/Domain/Attribute/AttributeRepository.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:cfe6eb7dfad896c4ea885c2f9a52c6a389d4e38379a0bf64f5d429910d87e55f",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.316.zip
Normal file
BIN
updates/0.30/ver_0.316.zip
Normal file
Binary file not shown.
23
updates/0.30/ver_0.316_manifest.json
Normal file
23
updates/0.30/ver_0.316_manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"changelog": "FIX - migracja brakujacej kolumny type w pp_shop_products_custom_fields",
|
||||||
|
"version": "0.316",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:654a3683c0add19d0cb2f87db6f6a45cd4ce08799dd3692c6adacc07666f13b1",
|
||||||
|
"sql": [
|
||||||
|
"ALTER TABLE `pp_shop_products_custom_fields` ADD COLUMN `type` VARCHAR(30) NOT NULL DEFAULT \u0027\u0027 AFTER `name`;"
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.317.zip
Normal file
BIN
updates/0.30/ver_0.317.zip
Normal file
Binary file not shown.
26
updates/0.30/ver_0.317_manifest.json
Normal file
26
updates/0.30/ver_0.317_manifest.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"changelog": "FIX - klucz API: fix zapisu (brakowalo w whiteliście), przycisk Generuj losowy klucz, ulepszony routing API",
|
||||||
|
"version": "0.317",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"api.php",
|
||||||
|
"autoload/Domain/Settings/SettingsRepository.php",
|
||||||
|
"autoload/admin/Controllers/SettingsController.php",
|
||||||
|
"autoload/api/ApiRouter.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:130e5f4fd15b635bb277dc639d528e017de866fbaada5db2d08d9426251824b3",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.318.zip
Normal file
BIN
updates/0.30/ver_0.318.zip
Normal file
Binary file not shown.
30
updates/0.30/ver_0.318_manifest.json
Normal file
30
updates/0.30/ver_0.318_manifest.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"changelog": "NEW - shopPRO export produktów + API endpoints (ensure_attribute, ensure_attribute_value, upload_image)",
|
||||||
|
"version": "0.318",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"admin/templates/integrations/shoppro-settings.php",
|
||||||
|
"autoload/Domain/Attribute/AttributeRepository.php",
|
||||||
|
"autoload/Domain/Integrations/IntegrationsRepository.php",
|
||||||
|
"autoload/Domain/Product/ProductRepository.php",
|
||||||
|
"autoload/admin/Controllers/IntegrationsController.php",
|
||||||
|
"autoload/admin/Controllers/ShopProductController.php",
|
||||||
|
"autoload/api/Controllers/DictionariesApiController.php",
|
||||||
|
"autoload/api/Controllers/ProductsApiController.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:6a7eba1b390db94ccda210a5f2cbcd33f17f43d9f34031c4d0793d224df5d541",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-24",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user