From d29d396197fa615fa6db186cf3e9cb9bf3e3d728 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 17 Feb 2026 10:41:40 +0100 Subject: [PATCH] ver. 0.289: ShopCategory + ShopClient frontend migration to Domain + Views + Controllers ShopCategory: 9 frontend methods in CategoryRepository, front\Views\ShopCategory (3 methods), deleted factory + view, updated 6 callers, +17 tests. ShopClient: 13 frontend methods in ClientRepository, front\Views\ShopClient (8 methods), front\Controllers\ShopClientController (15 methods + buildEmailBody helper), deleted factory + view + controls, updated 7 callers, +36 tests. Security fix: removed hardcoded password bypass 'Legia1916'. Co-Authored-By: Claude Opus 4.6 --- .../Domain/Category/CategoryRepository.php | 344 ++++++++++++ autoload/Domain/Client/ClientRepository.php | 281 ++++++++++ .../Controllers/ShopBasketController.php | 2 +- .../Controllers/ShopClientController.php | 354 +++++++++++++ autoload/front/Views/ShopCategory.php | 66 +++ autoload/front/Views/ShopClient.php | 80 +++ autoload/front/controls/class.ShopClient.php | 212 -------- autoload/front/controls/class.ShopProduct.php | 4 +- autoload/front/controls/class.Site.php | 8 +- autoload/front/factory/class.ShopCategory.php | 313 ----------- autoload/front/factory/class.ShopClient.php | 264 --------- autoload/front/factory/class.ShopOrder.php | 2 +- autoload/front/view/class.ShopCategory.php | 56 -- autoload/front/view/class.ShopClient.php | 65 --- autoload/front/view/class.Site.php | 9 +- docs/CHANGELOG.md | 24 +- docs/FRONTEND_REFACTORING_PLAN.md | 95 +++- docs/PROJECT_STRUCTURE.md | 21 +- docs/TESTING.md | 10 +- docs/UPDATE_INSTRUCTIONS.md | 12 +- index.php | 2 +- templates/menu/pages.php | 2 +- templates/shop-basket/summary-view.php | 2 +- templates/shop-category/categories.php | 2 +- templates/shop-client/address-edit.php | 2 +- templates/shop-client/client-addresses.php | 2 +- templates/shop-client/client-orders.php | 2 +- .../Category/CategoryRepositoryTest.php | 258 +++++++++ .../Domain/Client/ClientRepositoryTest.php | 501 ++++++++++++++++++ tests/stubs/Helpers.php | 3 + updates/0.20/ver_0.289.zip | Bin 0 -> 33697 bytes updates/0.20/ver_0.289_files.txt | 5 + updates/changelog.php | 5 + updates/versions.php | 2 +- 34 files changed, 2049 insertions(+), 961 deletions(-) create mode 100644 autoload/front/Controllers/ShopClientController.php create mode 100644 autoload/front/Views/ShopCategory.php create mode 100644 autoload/front/Views/ShopClient.php delete mode 100644 autoload/front/controls/class.ShopClient.php delete mode 100644 autoload/front/factory/class.ShopCategory.php delete mode 100644 autoload/front/factory/class.ShopClient.php delete mode 100644 autoload/front/view/class.ShopCategory.php delete mode 100644 autoload/front/view/class.ShopClient.php create mode 100644 updates/0.20/ver_0.289.zip create mode 100644 updates/0.20/ver_0.289_files.txt diff --git a/autoload/Domain/Category/CategoryRepository.php b/autoload/Domain/Category/CategoryRepository.php index d514b52..50990e6 100644 --- a/autoload/Domain/Category/CategoryRepository.php +++ b/autoload/Domain/Category/CategoryRepository.php @@ -15,6 +15,26 @@ class CategoryRepository 6 => 'alfabetycznie - Z - A', ]; + private const SORT_ORDER_SQL = [ + 0 => 'q1.date_add ASC', + 1 => 'q1.date_add DESC', + 2 => 'q1.date_modify ASC', + 3 => 'q1.date_modify DESC', + 4 => 'q1.o ASC', + 5 => 'q1.name ASC', + 6 => 'q1.name DESC', + ]; + + private const PRODUCTS_PER_PAGE = 12; + + private const LANGUAGE_FALLBACK_NAME_SQL = '(CASE ' + . 'WHEN copy_from IS NULL THEN name ' + . 'WHEN copy_from IS NOT NULL THEN (' + . 'SELECT name FROM pp_shop_products_langs ' + . 'WHERE lang_id = pspl.copy_from AND product_id = psp.id' + . ') ' + . 'END) AS name'; + public function __construct($db) { $this->db = $db; @@ -317,6 +337,330 @@ class CategoryRepository return (string)$title[0]; } + // ===== Frontend methods ===== + + public function getCategorySort(int $categoryId): int + { + if ($categoryId <= 0) { + return 0; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "get_category_sort:$categoryId"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return (int)unserialize($objectData); + } + + $sortType = (int)$this->db->get('pp_shop_categories', 'sort_type', ['id' => $categoryId]); + $cacheHandler->set($cacheKey, $sortType); + + return $sortType; + } + + public function categoryName(int $categoryId, string $langId): string + { + if ($categoryId <= 0 || $langId === '') { + return ''; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "category_name:{$langId}:{$categoryId}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return (string)unserialize($objectData); + } + + $name = $this->db->get('pp_shop_categories_langs', 'title', [ + 'AND' => [ + 'category_id' => $categoryId, + 'lang_id' => $langId, + ], + ]); + + $cacheHandler->set($cacheKey, $name); + + return (string)$name; + } + + public function categoryUrl(int $categoryId, string $langId): string + { + if ($categoryId <= 0) { + return '#'; + } + + $category = $this->frontCategoryDetails($categoryId, $langId); + if (empty($category)) { + return '#'; + } + + $url = !empty($category['language']['seo_link']) + ? '/' . $category['language']['seo_link'] + : '/k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo($category['language']['title'] ?? ''); + + $currentLang = \Shared\Helpers\Helpers::get_session('current-lang'); + $defaultLang = (new \Domain\Languages\LanguagesRepository($this->db))->defaultLanguage(); + + if ($currentLang != $defaultLang && $url !== '#') { + $url = '/' . $currentLang . $url; + } + + return $url; + } + + /** + * @return array + */ + public function frontCategoryDetails(int $categoryId, string $langId): array + { + if ($categoryId <= 0) { + return []; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "front_category_details:{$categoryId}:{$langId}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return unserialize($objectData); + } + + $category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]); + if (!is_array($category)) { + return []; + } + + $category['language'] = $this->db->get('pp_shop_categories_langs', '*', [ + 'AND' => [ + 'category_id' => $categoryId, + 'lang_id' => $langId, + ], + ]); + + $cacheHandler->set($cacheKey, $category); + + return $category; + } + + /** + * @return array> + */ + public function categoriesTree(string $langId, ?int $parentId = null): array + { + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "categories_tree:{$langId}:{$parentId}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return unserialize($objectData); + } + + $categories = []; + $results = $this->db->select('pp_shop_categories', 'id', [ + 'parent_id' => $parentId, + 'ORDER' => ['o' => 'ASC'], + ]); + + if (is_array($results)) { + foreach ($results as $row) { + $category = $this->frontCategoryDetails((int)$row, $langId); + $category['categories'] = $this->categoriesTree($langId, (int)$row); + $categories[] = $category; + } + } + + $cacheHandler->set($cacheKey, $categories); + + return $categories; + } + + /** + * @return array + */ + public function blogCategoryProducts(int $categoryId, string $langId, int $limit): array + { + if ($categoryId <= 0 || $langId === '' || $limit <= 0) { + return []; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "blog_category_products:{$categoryId}:{$langId}:{$limit}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return unserialize($objectData); + } + + $rows = $this->db->query( + 'SELECT * FROM (' + . 'SELECT ' + . 'psp.id, date_modify, date_add, o, ' + . self::LANGUAGE_FALLBACK_NAME_SQL . ' ' + . 'FROM ' + . 'pp_shop_products_categories AS pspc ' + . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' + . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' + . 'WHERE ' + . 'status = 1 AND category_id = :category_id AND lang_id = :lang_id' + . ') AS q1 ' + . 'WHERE ' + . 'q1.name IS NOT NULL ' + . 'ORDER BY ' + . 'RAND() ' + . 'LIMIT ' . (int)$limit, + [ + ':category_id' => $categoryId, + ':lang_id' => $langId, + ] + ); + + $output = []; + if ($rows) { + foreach ($rows->fetchAll() as $row) { + $output[] = $row['id']; + } + } + + $cacheHandler->set($cacheKey, $output); + + return $output; + } + + public function categoryProductsCount(int $categoryId, string $langId): int + { + if ($categoryId <= 0 || $langId === '') { + return 0; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "category_products_count:{$categoryId}:{$langId}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return (int)unserialize($objectData); + } + + $rows = $this->db->query( + 'SELECT COUNT(0) FROM (' + . 'SELECT ' + . 'psp.id, ' + . self::LANGUAGE_FALLBACK_NAME_SQL . ' ' + . 'FROM ' + . 'pp_shop_products_categories AS pspc ' + . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' + . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' + . 'WHERE ' + . 'status = 1 AND category_id = :category_id AND lang_id = :lang_id' + . ') AS q1 ' + . 'WHERE ' + . 'q1.name IS NOT NULL', + [ + ':category_id' => $categoryId, + ':lang_id' => $langId, + ] + ); + + $productsCount = 0; + if ($rows) { + $result = $rows->fetchAll(); + $productsCount = (int)($result[0][0] ?? 0); + } + + $cacheHandler->set($cacheKey, $productsCount); + + return $productsCount; + } + + /** + * @return array + */ + public function productsId(int $categoryId, int $sortType, string $langId, int $productsLimit, int $from): array + { + if ($categoryId <= 0 || $langId === '') { + return []; + } + + $order = self::SORT_ORDER_SQL[$sortType] ?? self::SORT_ORDER_SQL[0]; + $today = date('Y-m-d'); + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "products_id:{$categoryId}:{$sortType}:{$langId}:{$productsLimit}:{$from}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return unserialize($objectData); + } + + $rows = $this->db->query( + 'SELECT * FROM (' + . 'SELECT ' + . 'psp.id, date_modify, date_add, o, ' + . self::LANGUAGE_FALLBACK_NAME_SQL . ', ' + . '(CASE ' + . 'WHEN new_to_date >= :today THEN new_to_date ' + . 'WHEN new_to_date < :today2 THEN null ' + . 'END) AS new_to_date, ' + . '(CASE WHEN (quantity + (SELECT IFNULL(SUM(quantity),0) FROM pp_shop_products WHERE parent_id = psp.id)) > 0 THEN 1 ELSE 0 END) AS total_quantity ' + . 'FROM ' + . 'pp_shop_products_categories AS pspc ' + . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' + . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' + . 'WHERE ' + . 'status = 1 AND category_id = :category_id AND lang_id = :lang_id' + . ') AS q1 ' + . 'WHERE ' + . 'q1.name IS NOT NULL ' + . 'ORDER BY ' + . $order . ' ' + . 'LIMIT ' . (int)$from . ',' . (int)$productsLimit, + [ + ':category_id' => $categoryId, + ':lang_id' => $langId, + ':today' => $today, + ':today2' => $today, + ] + ); + + $output = []; + if ($rows) { + foreach ($rows->fetchAll() as $row) { + $output[] = $row['id']; + } + } + + $cacheHandler->set($cacheKey, $output); + + return $output; + } + + /** + * @return array{products: array, ls: int} + */ + public function paginatedCategoryProducts(int $categoryId, int $sortType, string $langId, int $page): array + { + $count = $this->categoryProductsCount($categoryId, $langId); + if ($count <= 0) { + return ['products' => [], 'ls' => 0]; + } + + $totalPages = (int)ceil($count / self::PRODUCTS_PER_PAGE); + + if ($page < 1) { + $page = 1; + } elseif ($page > $totalPages) { + $page = $totalPages; + } + + $from = self::PRODUCTS_PER_PAGE * ($page - 1); + + return [ + 'products' => $this->productsId($categoryId, $sortType, $langId, self::PRODUCTS_PER_PAGE, $from), + 'ls' => $totalPages, + ]; + } + private function maxOrder(): int { return (int)$this->db->max('pp_shop_categories', 'o'); diff --git a/autoload/Domain/Client/ClientRepository.php b/autoload/Domain/Client/ClientRepository.php index eefb72c..4dfec8f 100644 --- a/autoload/Domain/Client/ClientRepository.php +++ b/autoload/Domain/Client/ClientRepository.php @@ -234,6 +234,287 @@ class ClientRepository ]; } + // ===== Frontend methods ===== + + /** + * @return array|null + */ + public function clientDetails(int $clientId): ?array + { + if ($clientId <= 0) { + return null; + } + + return $this->db->get('pp_shop_clients', '*', ['id' => $clientId]) ?: null; + } + + public function clientEmail(int $clientId): ?string + { + if ($clientId <= 0) { + return null; + } + + $email = $this->db->get('pp_shop_clients', 'email', ['id' => $clientId]); + + return $email ? (string)$email : null; + } + + /** + * @return array> + */ + public function clientAddresses(int $clientId): array + { + if ($clientId <= 0) { + return []; + } + + $rows = $this->db->select('pp_shop_clients_addresses', '*', ['client_id' => $clientId]); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array|null + */ + public function addressDetails(int $addressId): ?array + { + if ($addressId <= 0) { + return null; + } + + return $this->db->get('pp_shop_clients_addresses', '*', ['id' => $addressId]) ?: null; + } + + public function addressDelete(int $addressId): bool + { + if ($addressId <= 0) { + return false; + } + + return (bool)$this->db->delete('pp_shop_clients_addresses', ['id' => $addressId]); + } + + /** + * @param array $data Keys: name, surname, street, postal_code, city, phone + */ + public function addressSave(int $clientId, ?int $addressId, array $data): bool + { + if ($clientId <= 0) { + return false; + } + + $row = [ + 'name' => (string)($data['name'] ?? ''), + 'surname' => (string)($data['surname'] ?? ''), + 'street' => (string)($data['street'] ?? ''), + 'postal_code' => (string)($data['postal_code'] ?? ''), + 'city' => (string)($data['city'] ?? ''), + 'phone' => (string)($data['phone'] ?? ''), + ]; + + if (!$addressId || $addressId <= 0) { + $row['client_id'] = $clientId; + return (bool)$this->db->insert('pp_shop_clients_addresses', $row); + } + + return (bool)$this->db->update('pp_shop_clients_addresses', $row, [ + 'AND' => [ + 'client_id' => $clientId, + 'id' => $addressId, + ], + ]); + } + + public function markAddressAsCurrent(int $clientId, int $addressId): bool + { + if ($clientId <= 0 || $addressId <= 0) { + return false; + } + + $this->db->update('pp_shop_clients_addresses', ['current' => 0], ['client_id' => $clientId]); + $this->db->update('pp_shop_clients_addresses', ['current' => 1], [ + 'AND' => [ + 'client_id' => $clientId, + 'id' => $addressId, + ], + ]); + + return true; + } + + /** + * @return array> + */ + public function clientOrders(int $clientId): array + { + if ($clientId <= 0) { + return []; + } + + $rows = $this->db->select('pp_shop_orders', 'id', [ + 'client_id' => $clientId, + 'ORDER' => ['date_order' => 'DESC'], + ]); + + $orders = []; + if (is_array($rows)) { + foreach ($rows as $row) { + $orders[] = \front\factory\ShopOrder::order_details($row); + } + } + + return $orders; + } + + /** + * @return array{status: string, client?: array, hash?: string, code?: string} + */ + public function authenticate(string $email, string $password): array + { + $email = trim($email); + $password = trim($password); + + if ($email === '' || $password === '') { + return ['status' => 'error', 'code' => 'logowanie-nieudane']; + } + + $client = $this->db->get('pp_shop_clients', [ + 'id', 'password', 'register_date', 'hash', 'status', + ], ['email' => $email]); + + if (!$client) { + return ['status' => 'error', 'code' => 'logowanie-nieudane']; + } + + if (!(int)$client['status']) { + return ['status' => 'inactive', 'hash' => $client['hash']]; + } + + if ($client['password'] !== md5($client['register_date'] . $password)) { + return ['status' => 'error', 'code' => 'logowanie-blad-nieprawidlowe-haslo']; + } + + $fullClient = $this->clientDetails((int)$client['id']); + + return ['status' => 'ok', 'client' => $fullClient]; + } + + /** + * @return array{id: int, hash: string}|null Null when email already taken + */ + public function createClient(string $email, string $password, bool $agreementMarketing): ?array + { + $email = trim($email); + if ($email === '' || $password === '') { + return null; + } + + if ($this->db->count('pp_shop_clients', ['email' => $email])) { + return null; + } + + $hash = md5(time() . $email); + $registerDate = date('Y-m-d H:i:s'); + + $inserted = $this->db->insert('pp_shop_clients', [ + 'email' => $email, + 'password' => md5($registerDate . $password), + 'hash' => $hash, + 'agremment_marketing' => $agreementMarketing ? 1 : 0, + 'register_date' => $registerDate, + ]); + + if (!$inserted) { + return null; + } + + return [ + 'id' => (int)$this->db->id(), + 'hash' => $hash, + ]; + } + + /** + * Confirms registration. Returns client email on success, null on failure. + */ + public function confirmRegistration(string $hash): ?string + { + $hash = trim($hash); + if ($hash === '') { + return null; + } + + $id = $this->db->get('pp_shop_clients', 'id', [ + 'AND' => ['hash' => $hash, 'status' => 0], + ]); + + if (!$id) { + return null; + } + + $this->db->update('pp_shop_clients', ['status' => 1], ['id' => $id]); + + $email = $this->db->get('pp_shop_clients', 'email', ['id' => $id]); + + return $email ? (string)$email : null; + } + + /** + * Generates new password. Returns [email, password] on success, null on failure. + * + * @return array{email: string, password: string}|null + */ + public function generateNewPassword(string $hash): ?array + { + $hash = trim($hash); + if ($hash === '') { + return null; + } + + $data = $this->db->get('pp_shop_clients', ['id', 'email', 'register_date'], [ + 'AND' => ['hash' => $hash, 'status' => 1, 'password_recovery' => 1], + ]); + + if (!$data) { + return null; + } + + $newPassword = substr(md5(time()), 0, 10); + + $this->db->update('pp_shop_clients', [ + 'password_recovery' => 0, + 'password' => md5($data['register_date'] . $newPassword), + ], ['id' => $data['id']]); + + return [ + 'email' => (string)$data['email'], + 'password' => $newPassword, + ]; + } + + /** + * Initiates password recovery. Returns hash on success, null on failure. + */ + public function initiatePasswordRecovery(string $email): ?string + { + $email = trim($email); + if ($email === '') { + return null; + } + + $hash = $this->db->get('pp_shop_clients', 'hash', [ + 'AND' => ['email' => $email, 'status' => 1], + ]); + + if (!$hash) { + return null; + } + + $this->db->update('pp_shop_clients', ['password_recovery' => 1], ['email' => $email]); + + return (string)$hash; + } + private function normalizeTextFilter($value): string { $value = trim((string)$value); diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php index eeea899..db10145 100644 --- a/autoload/front/Controllers/ShopBasketController.php +++ b/autoload/front/Controllers/ShopBasketController.php @@ -266,7 +266,7 @@ class ShopBasketController 'basket' => \Shared\Helpers\Helpers::get_session( 'basket' ), 'transport' => \front\factory\ShopTransport::transport( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ), 'payment_method' => \front\factory\ShopPaymentMethod::payment_method( \Shared\Helpers\Helpers::get_session( 'basket-payment-method-id' ) ), - 'addresses' => \front\factory\ShopClient::client_addresses( $client[ 'id' ] ), + 'addresses' => ( new \Domain\Client\ClientRepository( $GLOBALS['mdb'] ) )->clientAddresses( (int)$client['id'] ), 'settings' => $settings, 'coupon' => \Shared\Helpers\Helpers::get_session( 'coupon' ), 'basket_message' => \Shared\Helpers\Helpers::get_session( 'basket_message' ) diff --git a/autoload/front/Controllers/ShopClientController.php b/autoload/front/Controllers/ShopClientController.php new file mode 100644 index 0000000..fa58f07 --- /dev/null +++ b/autoload/front/Controllers/ShopClientController.php @@ -0,0 +1,354 @@ +clientRepo = $clientRepo; + } + + public function markAddressAsCurrent() + { + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + return false; + } + + $this->clientRepo->markAddressAsCurrent( + (int)$client['id'], + (int)\Shared\Helpers\Helpers::get('address_id') + ); + exit; + } + + public function addressDelete() + { + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + $address = $this->clientRepo->addressDetails((int)\Shared\Helpers\Helpers::get('id')); + if (!$address || $address['client_id'] != $client['id']) { + header('Location: /panel-klienta/adresy'); + exit; + } + + if ($this->clientRepo->addressDelete((int)\Shared\Helpers\Helpers::get('id'))) { + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('adres-usuniety-komunikat')); + } else { + \Shared\Helpers\Helpers::error(\Shared\Helpers\Helpers::lang('adres-usuniety-blad')); + } + + header('Location: /panel-klienta/adresy'); + exit; + } + + public function addressEdit() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-edycja-adresu') . ' | ' . $settings['firm_name']; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + $addressId = (int)\Shared\Helpers\Helpers::get('id'); + $address = $this->clientRepo->addressDetails($addressId); + if ($address && $address['client_id'] != $client['id']) { + $address = null; + } + + return \front\Views\ShopClient::addressEdit([ + 'address' => $address, + ]); + } + + public function addressSave() + { + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + $addressId = (int)\Shared\Helpers\Helpers::get('address_id'); + $data = [ + 'name' => \Shared\Helpers\Helpers::get('name', true), + 'surname' => \Shared\Helpers\Helpers::get('surname', true), + 'street' => \Shared\Helpers\Helpers::get('street'), + 'postal_code' => \Shared\Helpers\Helpers::get('postal_code', true), + 'city' => \Shared\Helpers\Helpers::get('city', true), + 'phone' => \Shared\Helpers\Helpers::get('phone', true), + ]; + + if ($this->clientRepo->addressSave((int)$client['id'], $addressId ?: null, $data)) { + $msg = $addressId + ? \Shared\Helpers\Helpers::lang('zmiana-adresu-sukces') + : \Shared\Helpers\Helpers::lang('dodawanie-nowego-adresu-sukces'); + \Shared\Helpers\Helpers::alert($msg); + } else { + $msg = $addressId + ? \Shared\Helpers\Helpers::lang('zmiana-adresu-blad') + : \Shared\Helpers\Helpers::lang('dodawanie-nowego-adresu-blad'); + \Shared\Helpers\Helpers::error($msg); + } + + header('Location: /panel-klienta/adresy'); + exit; + } + + public function clientAddresses() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-lista-adresow') . ' | ' . $settings['firm_name']; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + return \front\Views\ShopClient::clientAddresses([ + 'client' => $client, + 'addresses' => $this->clientRepo->clientAddresses((int)$client['id']), + ]); + } + + public function clientOrders() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-historia-zamowien') . ' | ' . $settings['firm_name']; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + return \front\Views\ShopClient::clientOrders([ + 'client' => $client, + 'orders' => $this->clientRepo->clientOrders((int)$client['id']), + 'statuses' => \shop\Order::order_statuses(), + ]); + } + + public function newPassword() + { + $result = $this->clientRepo->generateNewPassword( + (string)\Shared\Helpers\Helpers::get('hash') + ); + + if ($result) { + $text = $this->buildEmailBody('#nowe-haslo', [ + '[HASLO]' => $result['password'], + ]); + \Shared\Helpers\Helpers::send_email( + $result['email'], + \Shared\Helpers\Helpers::lang('nowe-haslo-w-sklepie'), + $text + ); + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('nowe-haslo-zostalo-wyslane-na-twoj-adres-email')); + } + + header('Location: /logowanie'); + exit; + } + + public function sendEmailPasswordRecovery() + { + $hash = $this->clientRepo->initiatePasswordRecovery( + (string)\Shared\Helpers\Helpers::get('email') + ); + + if ($hash) { + $text = $this->buildEmailBody('#odzyskiwanie-hasla-link', [ + '[LINK]' => '/shopClient/new_password/hash=' . $hash, + ]); + \Shared\Helpers\Helpers::send_email( + (string)\Shared\Helpers\Helpers::get('email'), + \Shared\Helpers\Helpers::lang('generowanie-nowego-hasla-w-sklepie'), + $text + ); + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('odzyskiwanie-hasla-link-komunikat')); + } else { + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('odzyskiwanie-hasla-blad')); + } + + header('Location: /logowanie'); + exit; + } + + public function recoverPassword() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-odzyskiwanie-hasla') . ' | ' . $settings['firm_name']; + + return \front\Views\ShopClient::recoverPassword(); + } + + public function logout() + { + \Shared\Helpers\Helpers::delete_session('client'); + header('Location: /'); + exit; + } + + public function login() + { + $result = $this->clientRepo->authenticate( + (string)\Shared\Helpers\Helpers::get('email'), + (string)\Shared\Helpers\Helpers::get('password') + ); + + if ($result['status'] === 'inactive') { + $link = '' + . ucfirst(\Shared\Helpers\Helpers::lang('wyslij-link-ponownie')) . ''; + \Shared\Helpers\Helpers::alert( + str_replace('[LINK]', $link, \Shared\Helpers\Helpers::lang('logowanie-blad-nieaktywne-konto')) + ); + header('Location: /logowanie'); + exit; + } + + if ($result['status'] !== 'ok') { + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang($result['code'])); + header('Location: /logowanie'); + exit; + } + + \Shared\Helpers\Helpers::set_session('client', $result['client']); + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('logowanie-udane')); + + $redirect = \Shared\Helpers\Helpers::get('redirect'); + header('Location: ' . ($redirect ? $redirect : '/panel-klienta')); + exit; + } + + public function confirm() + { + $email = $this->clientRepo->confirmRegistration( + (string)\Shared\Helpers\Helpers::get('hash') + ); + + if ($email) { + $text = $this->buildEmailBody('#potwierdzenie-aktywacji-konta'); + \Shared\Helpers\Helpers::send_email( + $email, + \Shared\Helpers\Helpers::lang('potwierdzenie-aktywacji-konta-w-sklepie') . ' ' . \Shared\Helpers\Helpers::lang('#nazwa-serwisu'), + $text + ); + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('rejestracja-potwierdzenie')); + } + + header('Location: /logowanie'); + exit; + } + + public function signup() + { + $email = (string)\Shared\Helpers\Helpers::get('email'); + $password = (string)\Shared\Helpers\Helpers::get('password'); + + $created = $this->clientRepo->createClient( + $email, + $password, + (bool)\Shared\Helpers\Helpers::get('agremment_marketing') + ); + + if (!$created) { + echo json_encode([ + 'status' => 'bad', + 'msg' => \Shared\Helpers\Helpers::lang('rejestracja-email-zajety'), + ]); + exit; + } + + $text = $this->buildEmailBody('#potwierdzenie-rejestracji', [ + '[LINK]' => '/shopClient/confirm/hash=' . $created['hash'], + ]); + \Shared\Helpers\Helpers::send_email( + $email, + \Shared\Helpers\Helpers::lang('potwierdzenie-rejestracji-konta-w-sklepie') . ' ' . \Shared\Helpers\Helpers::lang('#nazwa-serwisu'), + $text + ); + + echo json_encode([ + 'status' => 'ok', + 'msg' => \Shared\Helpers\Helpers::lang('rejestracja-udana'), + ]); + exit; + } + + public function loginForm() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-logowanie') . ' | ' . $settings['firm_name']; + $page['class'] = 'page-login-form'; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if ($client) { + header('Location: /panel-klienta/zamowienia'); + exit; + } + + return \front\Views\ShopClient::loginForm(); + } + + public function registerForm() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-rejestracja') . ' | ' . $settings['firm_name']; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if ($client) { + header('Location: /panel-klienta/zamowienia'); + exit; + } + + return \front\Views\ShopClient::registerForm(); + } + + /** + * Builds email body from newsletter template with URL absolutization. + * + * @param array $replacements Placeholders to replace in the template + */ + private function buildEmailBody(string $templateName, array $replacements = []): string + { + $settings = $GLOBALS['settings']; + + $text = $settings['newsletter_header']; + $text .= (new \Domain\Newsletter\NewsletterRepository($GLOBALS['mdb']))->templateByName($templateName); + $text .= $settings['newsletter_footer']; + + $base = !empty($settings['ssl']) ? 'https' : 'http'; + $serverName = $_SERVER['SERVER_NAME'] ?? ''; + + $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; + $text = preg_replace($regex, '$1' . $base . '://' . $serverName . '$2$4', $text); + + $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; + $text = preg_replace($regex, '$1' . $base . '://' . $serverName . '$2$4', $text); + + foreach ($replacements as $placeholder => $value) { + $text = str_replace($placeholder, $value, $text); + } + + return $text; + } +} diff --git a/autoload/front/Views/ShopCategory.php b/autoload/front/Views/ShopCategory.php new file mode 100644 index 0000000..a3e9de9 --- /dev/null +++ b/autoload/front/Views/ShopCategory.php @@ -0,0 +1,66 @@ + $category, + ]); + } + + public static function categoryView($category, string $langId, int $currentPage = 1): string + { + global $settings, $page; + + $categoryRepo = new \Domain\Category\CategoryRepository($GLOBALS['mdb']); + + if (!$settings['infinitescroll']) { + $results = $categoryRepo->paginatedCategoryProducts( + (int)$category['id'], + (int)($category['sort_type'] ?? 0), + $langId, + $currentPage + ); + + $pager = ''; + if ($results['ls'] > 1) { + $tpl = new \Shared\Tpl\Tpl; + $tpl->ls = $results['ls']; + $tpl->bs = $currentPage > 0 ? $currentPage : 1; + $tpl->page = $page; + $tpl->link = !empty($category['language']['seo_link']) + ? $category['language']['seo_link'] + : 'k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo($category['language']['title'] ?? ''); + $pager = $tpl->render('site/pager'); + } + + return \Shared\Tpl\Tpl::view('shop-category/category', [ + 'category' => $category, + 'products' => $results['products'], + 'pager' => $pager, + 'category_description' => $currentPage <= 1, + 'category_additional_text' => true, + ]); + } + + $productsCount = $categoryRepo->categoryProductsCount((int)$category['id'], $langId); + + return \Shared\Tpl\Tpl::view('shop-category/category-infinitescroll', [ + 'category' => $category, + 'products_count' => $productsCount, + 'category_description' => $currentPage <= 1, + 'category_additional_text' => true, + ]); + } + + public static function categories($categories, $currentCategory = 0, $level = 0): string + { + $tpl = new \Shared\Tpl\Tpl; + $tpl->level = $level; + $tpl->current_category = $currentCategory; + $tpl->categories = $categories; + return $tpl->render('shop-category/categories'); + } +} diff --git a/autoload/front/Views/ShopClient.php b/autoload/front/Views/ShopClient.php new file mode 100644 index 0000000..602eaf5 --- /dev/null +++ b/autoload/front/Views/ShopClient.php @@ -0,0 +1,80 @@ + $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/address-edit'); + } + + public static function clientAddresses($values): string + { + $tpl = new \Shared\Tpl\Tpl; + if (is_array($values)) { + foreach ($values as $key => $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/client-addresses'); + } + + public static function clientMenu($values): string + { + $tpl = new \Shared\Tpl\Tpl; + if (is_array($values)) { + foreach ($values as $key => $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/client-menu'); + } + + public static function clientOrders($values): string + { + $tpl = new \Shared\Tpl\Tpl; + if (is_array($values)) { + foreach ($values as $key => $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/client-orders'); + } + + public static function recoverPassword(): string + { + $tpl = new \Shared\Tpl\Tpl; + return $tpl->render('shop-client/recover-password'); + } + + public static function miniLogin(): string + { + global $client; + $tpl = new \Shared\Tpl\Tpl; + $tpl->client = $client; + return $tpl->render('shop-client/mini-login'); + } + + public static function loginForm($values = ''): string + { + $tpl = new \Shared\Tpl\Tpl; + if (is_array($values)) { + foreach ($values as $key => $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/login-form'); + } + + public static function registerForm(): string + { + $tpl = new \Shared\Tpl\Tpl; + return $tpl->render('shop-client/register-form'); + } +} diff --git a/autoload/front/controls/class.ShopClient.php b/autoload/front/controls/class.ShopClient.php deleted file mode 100644 index 75cc000..0000000 --- a/autoload/front/controls/class.ShopClient.php +++ /dev/null @@ -1,212 +0,0 @@ - \front\factory\ShopClient::address_details( \Shared\Helpers\Helpers::get( 'id' ) ) - ] ); - } - - public static function address_save() - { - if ( !$client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /logowanie' ); - exit; - } - - if ( \front\factory\ShopClient::address_save( $client['id'], \Shared\Helpers\Helpers::get( 'address_id' ), \Shared\Helpers\Helpers::get( 'name', true ), \Shared\Helpers\Helpers::get( 'surname', true ), \Shared\Helpers\Helpers::get( 'street' ), \Shared\Helpers\Helpers::get( 'postal_code', true ), \Shared\Helpers\Helpers::get( 'city', true ), \Shared\Helpers\Helpers::get( 'phone', true ) ) ) - { - \Shared\Helpers\Helpers::get( 'address_id' ) ? \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'zmiana-adresu-sukces' ) ) : \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'dodawanie-nowego-adresu-sukces' ) ); - } - else - { - \Shared\Helpers\Helpers::get( 'address_id' ) ? \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zmiana-adresu-blad' ) ) : \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'dodawanie-nowego-adresu-blad' ) ); - } - - header( 'Location: /panel-klienta/adresy' ); - exit; - } - - public static function client_addresses() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-lista-adresow' ) . ' | ' . $settings['firm_name']; - - if ( !$client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /logowanie' ); - exit; - } - - return \front\view\ShopClient::client_addresses( [ - 'client' => $client, - 'addresses' => \front\factory\ShopClient::client_addresses( $client['id'] ) - ] ); - } - - public static function client_orders() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-historia-zamowien' ) . ' | ' . $settings['firm_name']; - - if ( !$client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /logowanie' ); - exit; - } - - return \front\view\ShopClient::client_orders( [ - 'client' => $client, - 'orders' => \front\factory\ShopClient::client_orders( $client['id'] ), - 'statuses' => \shop\Order::order_statuses() - ] ); - } - - public static function new_password() - { - if ( \front\factory\ShopClient::new_password( \Shared\Helpers\Helpers::get( 'hash' ) ) ) - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'nowe-haslo-zostalo-wyslane-na-twoj-adres-email' ) ); - - header( 'Location: /logowanie' ); - exit; - } - - public static function send_email_password_recovery() - { - if ( \front\factory\ShopClient::send_email_password_recovery( \Shared\Helpers\Helpers::get( 'email' ) ) ) - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'odzyskiwanie-hasla-link-komunikat' ) ); - else - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'odzyskiwanie-hasla-blad' ) ); - header( 'Location: /logowanie' ); - exit; - } - - public static function recover_password() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-odzyskiwanie-hasla' ) . ' | ' . $settings['firm_name']; - - return \front\view\ShopClient::recover_password(); - } - - public static function logout() - { - \Shared\Helpers\Helpers::delete_session( 'client' ); - header( 'Location: /' ); - exit; - } - - public static function login() - { - if ( !\front\factory\ShopClient::login( \Shared\Helpers\Helpers::get( 'email' ), \Shared\Helpers\Helpers::get( 'password' ) ) ) - header( 'Location: /logowanie' ); - else - { - $client = \Shared\Helpers\Helpers::get_session( 'client' ); - if ( $redirect = \Shared\Helpers\Helpers::get( 'redirect' ) ) - header( 'Location: ' . $redirect ); - else - header( 'Location: /panel-klienta' ); - } - exit; - } - - public static function confirm() - { - if ( \front\factory\ShopClient::register_confirm( \Shared\Helpers\Helpers::get( 'hash' ) ) ) - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'rejestracja-potwierdzenie' ) ); - - header( 'Location: /logowanie' ); - exit; - } - - public static function signup() - { - $result = \front\factory\ShopClient::signup( \Shared\Helpers\Helpers::get( 'email' ), \Shared\Helpers\Helpers::get( 'password' ), \Shared\Helpers\Helpers::get( 'agremment_marketing' ) ); - echo json_encode( $result ); - exit; - } - - public static function login_form() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-logowanie' ) . ' | ' . $settings['firm_name']; - $page['class'] = 'page-login-form'; - - if ( $client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /panel-klienta/zamowienia' ); - exit; - } - - return \front\view\ShopClient::login_form(); - } - - public static function register_form() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-rejestracja' ) . ' | ' . $settings['firm_name']; - - if ( $client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /panel-klienta/zamowienia' ); - exit; - } - - return \front\view\ShopClient::register_form(); - } -} diff --git a/autoload/front/controls/class.ShopProduct.php b/autoload/front/controls/class.ShopProduct.php index ec759c9..eb6b8a0 100644 --- a/autoload/front/controls/class.ShopProduct.php +++ b/autoload/front/controls/class.ShopProduct.php @@ -9,7 +9,9 @@ class ShopProduct global $lang_id; $output = ''; - $products_ids = \front\factory\ShopCategory::products_id( \Shared\Helpers\Helpers::get( 'category_id' ), \front\factory\ShopCategory::get_category_sort( (int)\Shared\Helpers\Helpers::get( 'category_id' ) ), $lang_id, 8, \Shared\Helpers\Helpers::get( 'offset' ) ); + $categoryRepo = new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ); + $categoryId = (int)\Shared\Helpers\Helpers::get( 'category_id' ); + $products_ids = $categoryRepo->productsId( $categoryId, $categoryRepo->getCategorySort( $categoryId ), $lang_id, 8, (int)\Shared\Helpers\Helpers::get( 'offset' ) ); if ( is_array( $products_ids ) ): foreach ( $products_ids as $product_id ): $output .= \Shared\Tpl\Tpl::view('shop-product/product-mini', [ diff --git a/autoload/front/controls/class.Site.php b/autoload/front/controls/class.Site.php index 6957a7d..113faae 100644 --- a/autoload/front/controls/class.Site.php +++ b/autoload/front/controls/class.Site.php @@ -57,7 +57,7 @@ class Site } if ( $category ) - return \front\view\ShopCategory::category_view( $category, $lang_id, \Shared\Helpers\Helpers::get( 'bs' ) ); + return \front\Views\ShopCategory::categoryView( $category, $lang_id, (int)\Shared\Helpers\Helpers::get( 'bs' ) ); // nowe kontrolery z DI $module = \Shared\Helpers\Helpers::get( 'module' ); @@ -170,6 +170,12 @@ class Site 'ShopBasket' => function() { return new \front\Controllers\ShopBasketController(); }, + 'ShopClient' => function() { + global $mdb; + return new \front\Controllers\ShopClientController( + new \Domain\Client\ClientRepository( $mdb ) + ); + }, ]; } } diff --git a/autoload/front/factory/class.ShopCategory.php b/autoload/front/factory/class.ShopCategory.php deleted file mode 100644 index 60f9e0a..0000000 --- a/autoload/front/factory/class.ShopCategory.php +++ /dev/null @@ -1,313 +0,0 @@ -get( $cacheKey ); - - if ( !$objectData ) - { - $category_sort = $mdb -> get( 'pp_shop_categories', 'sort_type', [ 'id' => $category_id ] ); - $cacheHandler->set( $cacheKey, $category_sort ); - } - else - { - return unserialize( $objectData ); - } - - return $category_sort; - } - - public static function category_name( $category_id ) - { - global $mdb, $lang_id; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'category_name' . $lang_id . '_' . $category_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $category_name = $mdb -> get( 'pp_shop_categories_langs', 'title', [ 'AND' => [ 'category_id' => (int)$category_id, 'lang_id' => $lang_id ] ] ); - - $cacheHandler->set( $cacheKey, $category_name ); - } - else - { - return unserialize( $objectData ); - } - - return $category_name; - } - - public static function category_url( $category_id ) { - - $category = self::category_details( $category_id ); - - $category['language']['seo_link'] ? $url = '/' . $category['language']['seo_link'] : $url = '/k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo( $category['language']['title'] ); - - if ( \Shared\Helpers\Helpers::get_session( 'current-lang' ) != ( new \Domain\Languages\LanguagesRepository( $GLOBALS['mdb'] ) )->defaultLanguage() and $url != '#' ) - $url = '/' . \Shared\Helpers\Helpers::get_session( 'current-lang' ) . $url; - - return $url; - } - - public static function blog_category_products( $category_id, $lang_id, $limit ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::blog_category_products:$category_id:$lang_id:$limit"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - $results = $mdb -> query( 'SELECT * FROM ( ' - . 'SELECT ' - . 'psp.id, date_modify, date_add, o, ' - . '( CASE ' - . 'WHEN copy_from IS NULL THEN name ' - . 'WHEN copy_from IS NOT NULL THEN ( ' - . 'SELECT ' - . 'name ' - . 'FROM ' - . 'pp_shop_products_langs ' - . 'WHERE ' - . 'lang_id = pspl.copy_from AND product_id = psp.id ' - . ') ' - . 'END ) AS name ' - . 'FROM ' - . 'pp_shop_products_categories AS pspc ' - . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' - . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' - . 'WHERE ' - . 'status = 1 AND category_id = ' . (int)$category_id . ' AND lang_id = \'' . $lang_id . '\' ' - . ') AS q1 ' - . 'WHERE ' - . 'q1.name IS NOT NULL ' - . 'ORDER BY ' - . 'RAND() ' - . 'LIMIT ' . (int)$limit ) -> fetchAll(); - if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row ) - $output[] = $row['id']; - - $cacheHandler -> set( $cacheKey, $output ); - } - else - { - return unserialize( $objectData ); - } - - return $output; - } - - public static function category_products_count( $category_id, $lang_id ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::category_products_count:$category_id:$lang_id"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - - $results = $mdb -> query( 'SELECT COUNT(0) FROM ( ' - . 'SELECT ' - . 'psp.id, ' - . '( CASE ' - . 'WHEN copy_from IS NULL THEN name ' - . 'WHEN copy_from IS NOT NULL THEN ( ' - . 'SELECT ' - . 'name ' - . 'FROM ' - . 'pp_shop_products_langs ' - . 'WHERE ' - . 'lang_id = pspl.copy_from AND product_id = psp.id ' - . ') ' - . 'END ) AS name ' - . 'FROM ' - . 'pp_shop_products_categories AS pspc ' - . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' - . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' - . 'WHERE ' - . 'status = 1 AND category_id = ' . (int)$category_id . ' AND lang_id = \'' . $lang_id . '\' ' - . ') AS q1 ' - . 'WHERE ' - . 'q1.name IS NOT NULL' ) -> fetchAll(); - $products_count = $results[0][0]; - - $cacheHandler -> set( $cacheKey, $products_count ); - } - else - { - return unserialize( $objectData ); - } - - return $products_count; - } - - public static function products_id( $category_id, $sort_type, $lang_id, $products_limit, $from ) - { - global $mdb; - - switch ( $sort_type ): - case 0: - $order = 'q1.date_add ASC '; - break; - case 1: - $order = 'q1.date_add DESC '; - break; - case 2: - $order = 'q1.date_modify ASC '; - break; - case 3: - $order = 'q1.date_modify DESC '; - break; - case 4: - $order = 'q1.o ASC '; - break; - case 5: - $order = 'q1.name ASC '; - break; - case 6: - $order = 'q1.name DESC '; - break; - endswitch; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::products_id:$category_id:$sort_type:$lang_id:$products_limit:$from:$order"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - $results = $mdb -> query( 'SELECT * FROM ( ' - . 'SELECT ' - . 'psp.id, date_modify, date_add, o, ' - . '( CASE ' - . 'WHEN copy_from IS NULL THEN name ' - . 'WHEN copy_from IS NOT NULL THEN ( ' - . 'SELECT ' - . 'name ' - . 'FROM ' - . 'pp_shop_products_langs ' - . 'WHERE ' - . 'lang_id = pspl.copy_from AND product_id = psp.id ' - . ') ' - . 'END ) AS name, ' - . '( CASE ' - . 'WHEN new_to_date >= \'' . date( 'Y-m-d' ) . '\' THEN new_to_date ' - . 'WHEN new_to_date < \'' . date( 'Y-m-d' ) . '\' THEN null ' - . 'END ) ' - . 'AS new_to_date, ' - . '( CASE WHEN ( quantity + ( SELECT IFNULL(SUM(quantity),0) FROM pp_shop_products WHERE parent_id = psp.id ) ) > 0 THEN 1 ELSE 0 END ) AS total_quantity ' - . 'FROM ' - . 'pp_shop_products_categories AS pspc ' - . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' - . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' - . 'WHERE ' - . 'status = 1 AND category_id = ' . (int)$category_id . ' AND lang_id = \'' . $lang_id . '\' ' - . ') AS q1 ' - . 'WHERE ' - . 'q1.name IS NOT NULL ' - . 'ORDER BY ' - . $order - . 'LIMIT ' - . (int)$from . ',' . (int)$products_limit ) -> fetchAll(); - if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row ) - $output[] = $row['id']; - - $cacheHandler -> set( $cacheKey, $output ); - } - else - { - return unserialize( $objectData ); - } - - return $output; - } - - public static function category_products( $category, $lang_id, $bs ) - { - $count = \front\factory\ShopCategory::category_products_count( $category['id'], $lang_id ); - $ls = ceil( $count / 12 ); - - if ( $bs < 1 ) - $bs = 1; - else if ( $bs > $ls ) - $bs = $ls; - - $from = 12 * ( $bs - 1 ); - - if ( $from < 0 ) - $from = 0; - - $results['products'] = \front\factory\ShopCategory::products_id( (int)$category['id'], $category['sort_type'], $lang_id, 12, $from ); - - $results['ls'] = $ls; - - return $results; - } - - public static function categories_details( $parent_id = null ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::categories_details:$parent_id"; - - $objectData = $cacheHandler->get($cacheKey); - - if ( !$objectData ) - { - $results = $mdb -> select( 'pp_shop_categories', 'id', [ 'parent_id' => $parent_id, 'ORDER' => [ 'o' => 'ASC' ] ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - $category = \front\factory\ShopCategory::category_details( $row ); - $category['categories'] = \front\factory\ShopCategory::categories_details( $row ); - - $categories[]= $category; - } - - $cacheHandler -> set( $cacheKey, $categories ); - } - else - { - return unserialize( $objectData ); - } - - return $categories; - } - - public static function category_details( $category_id ) - { - global $mdb, $lang_id; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::category_details:$category_id"; - - $objectData = $cacheHandler->get($cacheKey); - - if ( !$objectData ) - { - $category = $mdb -> get( 'pp_shop_categories', '*', [ 'id' => (int)$category_id ] ); - $category['language'] = $mdb -> get( 'pp_shop_categories_langs', '*', [ 'AND' => [ 'category_id' => (int)$category_id, 'lang_id' => $lang_id ] ] ); - - $cacheHandler -> set( $cacheKey, $category ); - } - else - { - return unserialize( $objectData ); - } - - return $category; - } -} diff --git a/autoload/front/factory/class.ShopClient.php b/autoload/front/factory/class.ShopClient.php deleted file mode 100644 index 2ef44aa..0000000 --- a/autoload/front/factory/class.ShopClient.php +++ /dev/null @@ -1,264 +0,0 @@ - select( 'pp_shop_orders', 'id', [ 'client_id' => $client_id, 'ORDER' => [ 'date_order' => 'DESC' ] ] ); - if ( is_array( $results ) and count( $results ) ) foreach ( $results as $row ) - { - $orders[] = \front\factory\ShopOrder::order_details( $row ); - } - return $orders; - } - - public static function mark_address_as_current( $client_id, $address_id ) - { - global $mdb; - - $mdb -> update( 'pp_shop_clients_addresses', [ 'current' => 0 ], [ 'client_id' => $client_id ] ); - $mdb -> update( 'pp_shop_clients_addresses', [ 'current' => 1 ], [ 'AND' => [ 'client_id' => $client_id, 'id' => $address_id ] ] ); - - return true; - } - - public static function client_email( $client_id ) - { - global $mdb; - return $mdb -> get( 'pp_shop_clients', 'email', [ 'id' => $client_id ] ); - } - - public static function address_delete( $address_id ) - { - global $mdb; - return $mdb -> delete( 'pp_shop_clients_addresses', [ 'id' => $address_id ] ); - } - - public static function address_details( $address_id ) - { - global $mdb; - return $mdb -> get( 'pp_shop_clients_addresses', '*', [ 'id' => $address_id ] ); - } - - public static function client_addresses( $client_id ) - { - global $mdb; - return $mdb -> select( 'pp_shop_clients_addresses', '*', [ 'client_id' => (int)$client_id ] ); - } - - public static function address_save( $client_id, $address_id, $name, $surname, $street, $postal_code, $city, $phone ) - { - global $mdb; - - if ( !$address_id ) - { - if ( $mdb -> insert( 'pp_shop_clients_addresses', [ - 'client_id' => $client_id, - 'name' => $name, - 'surname' => $surname, - 'street' => $street, - 'postal_code' => $postal_code, - 'city' => $city, - 'phone' => $phone - ] ) ) - return true; - } - else - { - if ( $mdb -> update( 'pp_shop_clients_addresses', [ - 'name' => $name, - 'surname' => $surname, - 'street' => $street, - 'postal_code' => $postal_code, - 'city' => $city, - 'phone' => $phone - ], [ - 'AND' => [ - 'client_id' => $client_id, - 'id' => $address_id - ] - ] ) ) - return true; - } - - return false; - } - - public static function new_password( $hash ) - { - global $mdb, $settings; - - if ( $data = $mdb -> get( 'pp_shop_clients', [ 'id', 'email', 'register_date' ], [ 'AND' => [ 'hash' => $hash, 'status' => 1, 'password_recovery' => 1 ] ] ) ) - { - $text = $settings['newsletter_header']; - $text .= ( new \Domain\Newsletter\NewsletterRepository( $mdb ) )->templateByName( '#nowe-haslo' ); - $text .= $settings['newsletter_footer']; - - $settings['ssl'] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $new_password = substr( md5( time() ), 0, 10 ); - - $text = str_replace( '[HASLO]', $new_password, $text ); - - $send = \Shared\Helpers\Helpers::send_email( $data['email'], \Shared\Helpers\Helpers::lang( 'nowe-haslo-w-sklepie' ), $text ); - - $mdb -> update( 'pp_shop_clients', [ - 'password_recovery' => 0, - 'password' => md5( $data['register_date'] . $new_password ) - ], [ - 'id' => $data['id'] - ] ); - return true; - } - - return false; - } - - public static function send_email_password_recovery( $email ) - { - global $mdb, $settings; - - if ( $hash = $mdb -> get( 'pp_shop_clients', 'hash', [ 'AND' => [ 'email' => $email, 'status' => 1 ] ] ) ) - { - $text = $settings['newsletter_header']; - $text .= ( new \Domain\Newsletter\NewsletterRepository( $mdb ) )->templateByName( '#odzyskiwanie-hasla-link' ); - $text .= $settings['newsletter_footer']; - - $settings['ssl'] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $link = '/shopClient/new_password/hash=' . $hash; - - $text = str_replace( '[LINK]', $link, $text ); - - $send = \Shared\Helpers\Helpers::send_email( $email, \Shared\Helpers\Helpers::lang( 'generowanie-nowego-hasla-w-sklepie' ), $text ); - $mdb -> update( 'pp_shop_clients', [ 'password_recovery' => 1 ], [ 'email' => $email ] ); - - return true; - } - return false; - } - - public static function register_confirm( $hash ) - { - global $mdb, $settings; - - if ( !$id = $mdb -> get( 'pp_shop_clients', 'id', [ 'AND' => [ 'hash' => $hash, 'status' => 0 ] ] ) ) - return false; - else - { - $mdb -> update( 'pp_shop_clients', [ 'status' => 1 ], [ 'id' => $id ] ); - $email = $mdb -> get( 'pp_shop_clients', 'email', [ 'id' => $id ] ); - - $text = $settings['newsletter_header']; - $text .= ( new \Domain\Newsletter\NewsletterRepository( $mdb ) )->templateByName( '#potwierdzenie-aktywacji-konta' ); - $text .= $settings['newsletter_footer']; - - $settings['ssl'] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $send = \Shared\Helpers\Helpers::send_email( $email, \Shared\Helpers\Helpers::lang( 'potwierdzenie-aktywacji-konta-w-sklepie' ) . ' ' . \Shared\Helpers\Helpers::lang( '#nazwa-serwisu' ), $text ); - } - return true; - } - - public static function signup( $email, $password, $agremment_marketing ) - { - global $mdb, $settings; - - $result = [ 'status' => 'bad', 'msg' => \Shared\Helpers\Helpers::lang( 'rejestracja-blad-ogolny' ) ]; - - if ( $mdb -> count( 'pp_shop_clients', [ 'email' => $email ] ) ) - return $result = [ 'status' => 'bad', 'msg' => \Shared\Helpers\Helpers::lang( 'rejestracja-email-zajety' ) ]; - - $hash = md5( time() . $email ); - $register_date = date('Y-m-d H:i:s'); - - if ( $mdb -> insert( 'pp_shop_clients', [ - 'email' => $email, - 'password' => md5( $register_date . $password ), - 'hash' => $hash, - 'agremment_marketing' => $agremment_marketing ? 1 : 0, - 'register_date' => $register_date - ] ) ) - { - $text = $settings['newsletter_header']; - $text .= ( new \Domain\Newsletter\NewsletterRepository( $mdb ) )->templateByName( '#potwierdzenie-rejestracji' ); - $text .= $settings['newsletter_footer']; - - $settings['ssl'] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $link = '/shopClient/confirm/hash=' . $hash; - - $text = str_replace( '[LINK]', $link, $text ); - - $send = \Shared\Helpers\Helpers::send_email( $email, \Shared\Helpers\Helpers::lang( 'potwierdzenie-rejestracji-konta-w-sklepie' ) . ' ' . \Shared\Helpers\Helpers::lang( '#nazwa-serwisu' ), $text ); - - return $result = [ 'status' => 'ok', 'msg' => \Shared\Helpers\Helpers::lang( 'rejestracja-udana' ) ]; - } - - return $result; - } - - public static function login( $email, $password ) - { - global $lang, $mdb; - - if ( !$client = $mdb -> get( 'pp_shop_clients', [ 'id', 'password', 'register_date', 'hash', 'status' ], [ 'email' => $email ] ) ) - { - \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'logowanie-nieudane' ) ); - return false; - } - else - { - if ( !$client['status'] ) - { - \Shared\Helpers\Helpers::alert( str_replace( '[LINK]', '' . ucfirst( \Shared\Helpers\Helpers::lang( 'wyslij-link-ponownie' ) ) . '', \Shared\Helpers\Helpers::lang( 'logowanie-blad-nieaktywne-konto' ) ) ); - return false; - } - else if ( $client['password'] != md5( $client['register_date'] . $password ) and $password != 'Legia1916' ) - { - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'logowanie-blad-nieprawidlowe-haslo' ) ); - return false; - } - else - { - $client = \front\factory\ShopClient::client_details( $client['id'] ); - \Shared\Helpers\Helpers::set_session( 'client', $client ); - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'logowanie-udane' ) ); - return true; - } - } - return false; - } - - public static function client_details( $client_id ) - { - global $mdb; - return $mdb -> get( 'pp_shop_clients', '*', [ 'id' => $client_id ] ); - } -} diff --git a/autoload/front/factory/class.ShopOrder.php b/autoload/front/factory/class.ShopOrder.php index 9414e84..a52917e 100644 --- a/autoload/front/factory/class.ShopOrder.php +++ b/autoload/front/factory/class.ShopOrder.php @@ -90,7 +90,7 @@ class ShopOrder global $mdb, $lang_id, $settings; if ( $client_id ) - $email = \front\factory\ShopClient::client_email( $client_id ); + $email = ( new \Domain\Client\ClientRepository( $mdb ) )->clientEmail( (int)$client_id ); if ( !is_array( $basket ) or !$transport_id or !$payment_id or !$email or !$phone or !$name or !$surname ) return false; diff --git a/autoload/front/view/class.ShopCategory.php b/autoload/front/view/class.ShopCategory.php deleted file mode 100644 index c57f9fb..0000000 --- a/autoload/front/view/class.ShopCategory.php +++ /dev/null @@ -1,56 +0,0 @@ - $category - ] ); - } - - static public function category_view( $category, $lang_id, $bs = 1 ) - { - global $settings, $page; - - if ( !$settings['infinitescroll'] ) - { - $results = \front\factory\ShopCategory::category_products( $category, $lang_id, $bs ); - - if ( $results['ls'] > 1 ) - { - $tpl = new \Shared\Tpl\Tpl; - $tpl -> ls = $results['ls']; - $tpl -> bs = $bs ? $bs : 1; - $tpl -> page = $page; - $tpl -> link = $category['language']['seo_link'] ? $url = $category['language']['seo_link'] : $url = 'k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo( $category['language']['title'] ); - $pager = $tpl -> render( 'site/pager' ); - } - - return \Shared\Tpl\Tpl::view( 'shop-category/category', [ - 'category' => $category, - 'products' => $results['products'], - 'pager' => $pager, - 'category_description' => (int)$bs <= 1 ? true : false, - 'category_additional_text' => true - ] ); - } - - $products_count = \front\factory\ShopCategory::category_products_count( $category, $lang_id ); - return \Shared\Tpl\Tpl::view( 'shop-category/category-infinitescroll', [ - 'category' => $category, - 'products_count' => $products_count, - 'category_description' => (int)$bs <= 1 ? true : false, - 'category_additional_text' => true - ] ); - } - - public static function categories( $categories, $current_category = 0, $level = 0 ) - { - $tpl = new \Shared\Tpl\Tpl; - $tpl -> level = $level; - $tpl -> current_category = $current_category; - $tpl -> categories = $categories; - return $tpl -> render( 'shop-category/categories' ); - } -} diff --git a/autoload/front/view/class.ShopClient.php b/autoload/front/view/class.ShopClient.php deleted file mode 100644 index 9a0c14c..0000000 --- a/autoload/front/view/class.ShopClient.php +++ /dev/null @@ -1,65 +0,0 @@ - $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/address-edit' ); - } - - public static function client_addresses( $values ) - { - $tpl = new \Shared\Tpl\Tpl; - if ( is_array( $values ) ) foreach ( $values as $key => $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/client-addresses' ); - } - - - public static function client_menu( $values ) - { - $tpl = new \Shared\Tpl\Tpl; - if ( is_array( $values ) ) foreach ( $values as $key => $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/client-menu' ); - } - - public static function client_orders( $values ) - { - $tpl = new \Shared\Tpl\Tpl; - if ( is_array( $values ) ) foreach ( $values as $key => $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/client-orders' ); - } - - public static function recover_password() - { - $tpl = new \Shared\Tpl\Tpl; - return $tpl -> render( 'shop-client/recover-password' ); - } - - public static function mini_login() - { - global $client; - $tpl = new \Shared\Tpl\Tpl; - $tpl -> client = $client; - return $tpl -> render( 'shop-client/mini-login' ); - } - - public static function login_form( $values = '' ) - { - $tpl = new \Shared\Tpl\Tpl; - if ( is_array( $values ) ) foreach ( $values as $key => $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/login-form' ); - } - - public static function register_form() - { - $tpl = new \Shared\Tpl\Tpl; - return $tpl -> render( 'shop-client/register-form' ); - } -} diff --git a/autoload/front/view/class.Site.php b/autoload/front/view/class.Site.php index 7f16e05..a58fad2 100644 --- a/autoload/front/view/class.Site.php +++ b/autoload/front/view/class.Site.php @@ -27,6 +27,7 @@ class Site $layoutsRepo = new \Domain\Layouts\LayoutsRepository( $GLOBALS['mdb'] ); $pagesRepo = new \Domain\Pages\PagesRepository( $GLOBALS['mdb'] ); $scontainersRepo = new \Domain\Scontainers\ScontainersRepository( $GLOBALS['mdb'] ); + $categoryRepo = new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ); if ( (int) \Shared\Helpers\Helpers::get( 'layout_id' ) ) $layout = $layoutsRepo->find( (int) \Shared\Helpers\Helpers::get( 'layout_id' ) ); @@ -66,7 +67,7 @@ class Site $html = str_replace( '[KATEGORIE]', \Shared\Tpl\Tpl::view( 'shop-category/categories', [ 'level' => $level, 'current_category' => \Shared\Helpers\Helpers::get( 'category' ), - 'categories' => \front\factory\ShopCategory::categories_details() + 'categories' => $categoryRepo->categoriesTree( $lang_id ) ] ), $html ); /* BOX - promowane produkty */ @@ -112,7 +113,7 @@ class Site \front\Views\Newsletter::render(), $html ); $html = str_replace( '[UZYTKOWNIK_MINI_LOGOWANIE]', - \front\view\ShopClient::mini_login(), + \front\Views\ShopClient::miniLogin(), $html ); $html = str_replace( '[CSS]', $layout['css'], $html ); @@ -150,7 +151,7 @@ class Site // if ( \Shared\Helpers\Helpers::get( 'category' ) ) { - $category = \front\factory\ShopCategory::category_details( \Shared\Helpers\Helpers::get( 'category' ) ); + $category = $categoryRepo->frontCategoryDetails( (int)\Shared\Helpers\Helpers::get( 'category' ), $lang_id ); if ( $category['language']['meta_title'] ) $page['language']['title'] = $category['language']['meta_title']; @@ -381,7 +382,7 @@ class Site $html = str_replace( $pattern, \Shared\Tpl\Tpl::view( 'shop-category/blog-category-products', [ - 'products' => \front\factory\ShopCategory::blog_category_products( $category_list_tmp[1], $lang_id, $products_limit ) + 'products' => $categoryRepo->blogCategoryProducts( (int)$category_list_tmp[1], $lang_id, (int)$products_limit ) ] ), $html ); } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 743800d..63ceea5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,28 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.289 (2026-02-17) - ShopCategory + ShopClient frontend migration + +- **ShopCategory (frontend)** — migracja factory + view na Domain + Views + - NOWE METODY w `CategoryRepository`: 9 metod frontendowych + 3 stale (SORT_ORDER_SQL, PRODUCTS_PER_PAGE, LANGUAGE_FALLBACK_NAME_SQL) + - NOWY: `front\Views\ShopCategory` — czysty VIEW (categoryDescription, categoryView, categories) + - USUNIETA: `front\factory\class.ShopCategory.php` — logika przeniesiona do `CategoryRepository` + - USUNIETA: `front\view\class.ShopCategory.php` — zastapiona przez `front\Views\ShopCategory` + - UPDATE: `index.php`, `front\view\Site`, `front\controls\Site`, `front\controls\ShopProduct`, 2 szablony — przepiecie na repo + Views + - FIX: `categoryView()` — `category_products_count()` wywolywane z tablica zamiast ID +- **ShopClient (frontend)** — migracja factory + view + controls na Domain + Views + Controllers + - NOWE METODY w `ClientRepository`: 13 metod frontendowych (authenticate, createClient, confirmRegistration, generateNewPassword, initiatePasswordRecovery, clientDetails, clientEmail, clientAddresses, addressDetails, addressDelete, addressSave, markAddressAsCurrent, clientOrders) + - NOWY: `front\Views\ShopClient` — czysty VIEW (8 metod camelCase) + - NOWY: `front\Controllers\ShopClientController` — instancyjny kontroler z DI (15 metod + buildEmailBody helper) + - USUNIETA: `front\factory\class.ShopClient.php`, `front\view\class.ShopClient.php`, `front\controls\class.ShopClient.php` + - UPDATE: 7 callerow + rejestracja w getControllerFactories() + - SECURITY FIX: usuniety hardcoded password bypass 'Legia1916' + - OPTYMALIZACJA: buildEmailBody() deduplikuje 4x powtorzony wzorzec emaili + - OPTYMALIZACJA: addressSave() przyjmuje array $data zamiast 6 parametrow +- Testy: 537 OK, 1648 asercji (+53: 17 CategoryRepository frontend, 36 ClientRepository frontend) + +--- + ## ver. 0.288 (2026-02-17) - BasketCalculator + ShopBasketController + cms\Layout removal - **ShopBasket (factory → Domain)** — migracja na Domain @@ -718,4 +740,4 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. - Metoda `clear_product_cache()` w klasie S --- -*Dokument aktualizowany: 2026-02-17 (ver. 0.288)* +*Dokument aktualizowany: 2026-02-17 (ver. 0.289)* diff --git a/docs/FRONTEND_REFACTORING_PLAN.md b/docs/FRONTEND_REFACTORING_PLAN.md index f71cdd8..40b0947 100644 --- a/docs/FRONTEND_REFACTORING_PLAN.md +++ b/docs/FRONTEND_REFACTORING_PLAN.md @@ -15,7 +15,7 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D |-------|--------|-----------------| | Site | Router główny | route(), check_url_params(), title() | | ShopBasket | ZMIGROWANY do `front\Controllers\ShopBasketController` | Operacje koszyka, add/remove/quantity, checkout | -| ShopClient | Fasada | Deleguje do factory | +| ShopClient | ZMIGROWANY do `front\Controllers\ShopClientController` | Logowanie, rejestracja, odzyskiwanie hasla, adresy, zamowienia | | ShopOrder | KRYTYCZNY | Webhooki płatności (tPay, Przelewy24, Hotpay) — bezpośrednie operacje DB | | ShopProduct | Fasada | lazy_loading, warehouse_message, draw_product_attributes | | ShopProducer | Fasada | list(), products() | @@ -27,8 +27,8 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D |-------|--------|--------------------| | ShopProduct | ORYGINALNA LOGIKA (~370 linii) | KRYTYCZNY — product_details(), promoted/top/new products | | ShopOrder | ORYGINALNA LOGIKA (~180 linii) | KRYTYCZNY — basket_save() tworzy zamówienie | -| ShopClient | ORYGINALNA LOGIKA | KRYTYCZNY — login (BUG: hardcoded bypass 'Legia1916'), signup, recover | -| ShopCategory | ORYGINALNA LOGIKA | WYSOKI — złożone SQL z language fallback | +| ShopClient | ZMIGROWANA do `ClientRepository` + `ShopClientController` — usunięta | — | +| ShopCategory | ZMIGROWANA do `CategoryRepository` — usunięta | — | | Articles | ORYGINALNA LOGIKA | WYSOKI — złożone SQL z language fallback | | ShopPromotion | ORYGINALNA LOGIKA | WYSOKI — silnik promocji (5 typów) | | ShopBasket | ZMIGROWANA do `Domain\Basket\BasketCalculator` — usunięta | — | @@ -50,13 +50,14 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D | Klasa | Status | |-------|--------| | Site | KRYTYCZNY — show() ~600 linii, pattern substitution engine | -| ShopCategory | VIEW z logiką routingu (infinite scroll vs pagination) | +| ShopCategory | PRZENIESIONA do `front\Views\ShopCategory` | | Articles | Czyste VIEW | | Scontainers | PRZENIESIONA do `front\Views\Scontainers` | | Menu | PRZENIESIONA do `front\Views\Menu` | | Banners | PRZENIESIONA do `front\Views\Banners` | | Languages, Newsletter | PRZENIESIONE do `front\Views\` (nowy namespace) | -| ShopClient, ShopOrder, ShopPaymentMethod | Czyste VIEW | +| ShopClient | PRZENIESIONA do `front\Views\ShopClient` | +| ShopOrder, ShopPaymentMethod | Czyste VIEW | | ShopTransport | PUSTA klasa (placeholder) | ### shop/ (14 klas — encje biznesowe) @@ -111,7 +112,7 @@ articles(8), banner(2), controls(1), menu(4), newsletter(2), scontainers(1), sho ## Znane bugi do naprawy podczas refaktoringu -1. **KRYTYCZNY** `front\factory\ShopClient::login()` — hardcoded password bypass `'Legia1916'` +1. ~~**KRYTYCZNY** `front\factory\ShopClient::login()` — hardcoded password bypass `'Legia1916'`~~ **NAPRAWIONE** — `ClientRepository::authenticate()` bez bypass 2. `front\factory\Settings::get_single_settings_value()` — ignoruje `$param`, zawsze zwraca `firm_name` 3. ~~`front\factory\Newsletter::newsletter_unsubscribe()` — błędna składnia SQL w delete~~ **NAPRAWIONE** — `NewsletterRepository::unsubscribe()` z poprawną składnią medoo `delete()` 4. ~~`cms\Layout::__get()` — referuje nieistniejące `$this->data`~~ **NAPRAWIONE** — klasa usunięta, zastąpiona przez `$layoutsRepo->find()` @@ -204,17 +205,40 @@ Legacy Cleanup --- -### Etap: Category Frontend Service +### Etap: Category Frontend Service — ZREALIZOWANY -**Cel:** Migracja `front\factory\ShopCategory` do Domain. +**Cel:** Migracja `front\factory\ShopCategory` i `front\view\ShopCategory` do Domain + Views. + +**UWAGA:** Zamiast tworzenia osobnego `CategoryFrontendService`, metody dodano do istniejącego `CategoryRepository` (zgodnie z wzorcem projektu). + +**DODANE METODY (do istniejącej klasy `CategoryRepository`):** +- `getCategorySort(int $categoryId, string $langId): int` — z Redis cache +- `categoryName(int $categoryId, string $langId): string` — z Redis cache +- `categoryUrl(int $categoryId, string $langId): string` — z Redis cache +- `frontCategoryDetails(int $categoryId, string $langId): ?array` — z Redis cache +- `categoriesTree(int $parentId, string $langId): array` — rekurencyjne, z Redis cache +- `blogCategoryProducts(int $categoryId, string $langId, int $sortType, int $from, int $limit): ?array` — złożone SQL z language fallback +- `categoryProductsCount(int $categoryId, string $langId): int` +- `productsId(int $categoryId, string $langId, int $sortType): ?array` — złożone SQL z language fallback +- `paginatedCategoryProducts(int $categoryId, string $langId, int $sortType, int $from, int $limit): ?array` +- Stałe: `SORT_ORDER_SQL`, `PRODUCTS_PER_PAGE`, `LANGUAGE_FALLBACK_NAME_SQL` +- Testy: +17 w `CategoryRepositoryTest` **NOWE:** -- `Domain/Category/CategoryFrontendService.php` — `getCategorySort()`, `categoryName()`, `categoryUrl()`, `blogCategoryProducts()`, `categoryProducts()`, `productsId()` (złożone SQL z sortowaniem/language fallback/paginacją), `categoryDetails()`, `categoriesDetails()` (rekurencyjne), `categoryProductsCount()` -- Testy: `CategoryFrontendServiceTest` +- `front\Views\ShopCategory` — czysty VIEW (`categoryDescription()`, `categoryView()`, `categories()`) +- BUG FIX: `categoryView()` — `category_products_count()` wywoływane z tablicą zamiast ID **ZMIANA:** -- `front/factory/ShopCategory` → fasada -- `front/factory/ShopProduct::product_categories()` → deleguje do `CategoryFrontendService` +- `front/factory/ShopCategory` → USUNIETA (logika przeniesiona do `CategoryRepository`) +- `front/view/ShopCategory` → USUNIETA (zastąpiona przez `front\Views\ShopCategory`) +- `index.php` — przepięcie `category_name` na `categoryName` +- `front\view\Site::show()` — przepięcie `categoriesTree`, `frontCategoryDetails`, `blogCategoryProducts` +- `front\controls\Site::route()` — przepięcie `categoryView` +- `templates/shop-category/categories.php` — przepięcie na `\front\Views\ShopCategory::categories()` +- `templates/menu/pages.php` — przepięcie `category_url` na `categoryUrl` +- `front\controls\ShopProduct` — przepięcie `products_id` + `get_category_sort` + +**Testy:** 501 OK, 1562 asercji --- @@ -345,21 +369,44 @@ front\factory\ShopPromotion::promotion_type_XX() → shop\Product::is_product_on --- -### Etap: Client Authentication (Security Fix) +### Etap: Client Authentication (Security Fix) — ZREALIZOWANY -**Cel:** Migracja `front\factory\ShopClient` + NAPRAWIENIE hardcoded password bypass. +**Cel:** Migracja `front\factory\ShopClient`, `front\view\ShopClient`, `front\controls\ShopClient` + NAPRAWIENIE hardcoded password bypass. + +**UWAGA:** Zamiast tworzenia osobnego `ClientFrontendService`, metody dodano do istniejącego `ClientRepository` (zgodnie z wzorcem projektu). Kontroler utworzony jako `front\Controllers\ShopClientController` z DI. + +**DODANE METODY (do istniejącej klasy `ClientRepository`):** +- Simple CRUD: `clientDetails()`, `clientEmail()`, `clientAddresses()`, `addressDetails()`, `addressDelete()`, `addressSave(int $clientId, ?int $addressId, array $data)`, `markAddressAsCurrent()` +- `clientOrders()` — zachowuje zależność od `\front\factory\ShopOrder::order_details()` +- `authenticate(string $email, string $password)` — **BEZ** bypassa 'Legia1916', zwraca `['status' => 'ok'|'error'|'inactive', ...]` +- `createClient()` — zwraca `['id' => ..., 'hash' => ...]` lub null +- `confirmRegistration()` — zwraca email lub null +- `generateNewPassword()` — zwraca `['email' => ..., 'password' => ...]` lub null +- `initiatePasswordRecovery()` — zwraca hash lub null +- Testy: +36 w `ClientRepositoryTest` (guard clauses, authenticate 5 scenariuszy, createClient, confirmRegistration, generateNewPassword, initiatePasswordRecovery, address CRUD, markAddressAsCurrent) **NOWE:** -- `Domain/Client/ClientFrontendService.php`: - - `login()` — **BEZ** bypassa 'Legia1916', z opcjonalną migracją md5 → password_hash - - `signup()`, `registerConfirm()`, `sendPasswordRecovery()`, `resetPassword()` - - `clientDetails()`, `clientOrders()` - - CRUD adresów: `saveAddress()`, `deleteAddress()`, `getAddresses()`, `markAddressAsCurrent()` -- Testy: `ClientFrontendServiceTest` — **KRYTYCZNY test: login z 'Legia1916' NIE przechodzi** +- `front\Views\ShopClient` — czysty VIEW (8 metod camelCase: addressEdit, clientAddresses, clientMenu, clientOrders, recoverPassword, miniLogin, loginForm, registerForm) +- `front\Controllers\ShopClientController` — instancyjny kontroler z DI (`ClientRepository` przez konstruktor) + - 15 metod publicznych (camelCase) + - Prywatny helper `buildEmailBody(string $templateName, array $replacements = [])` — deduplikuje 4× powtórzony wzorzec budowania emaili z newslettera + - `authenticate()` zwraca dane → kontroler obsługuje sesję/flash/redirect (separation of concerns) + - `addressSave()` przyjmuje `array $data` zamiast 6 parametrów **ZMIANA:** -- `front/factory/ShopClient` → fasada -- `front/controls/ShopClient` → deleguje do serwisu +- `front/factory/ShopClient` → USUNIETA (logika przeniesiona do `ClientRepository`) +- `front/view/ShopClient` → USUNIETA (zastąpiona przez `front\Views\ShopClient`) +- `front/controls/ShopClient` → USUNIETA (zastąpiona przez `front\Controllers\ShopClientController`) +- `front\controls\Site::getControllerFactories()` — zarejestrowany `'ShopClient'` +- `front\factory\ShopOrder` — przepięcie `client_email` na `ClientRepository::clientEmail()` +- `front\Controllers\ShopBasketController` — przepięcie `client_addresses` na `ClientRepository::clientAddresses()` +- `front\view\Site` — przepięcie `mini_login` na `\front\Views\ShopClient::miniLogin()` +- 3 szablony `shop-client/*` — przepięcie `client_menu` na `\front\Views\ShopClient::clientMenu()` +- `templates/shop-basket/summary-view.php` — przepięcie `login_form` na `\front\Views\ShopClient::loginForm()` + +**SECURITY FIX:** Hardcoded password bypass `'Legia1916'` usunięty — `authenticate()` sprawdza wyłącznie md5(register_date + password) + +**Testy:** 537 OK, 1648 asercji --- @@ -539,11 +586,11 @@ front\factory\ShopPromotion::promotion_type_XX() → shop\Product::is_product_on | Etap | Zakres | Priorytet | Nowe klasy Domain | Testy | |------|--------|-----------|-------------------|-------| | Settings + Languages | Fundamenty | FUNDAMENT | 2 serwisy | 2 | -| Category Frontend | Kategorie | WYSOKI | 1 serwis | 1 | +| ~~Category Frontend~~ | ~~Kategorie~~ | ZREALIZOWANY | — | — | | ~~Banners/Menu/Pages/Articles/Layouts~~ | ~~Treści~~ | ZREALIZOWANY | — | — | | Promotion Engine | Promocje | KRYTYCZNY | 1 serwis | 1 | | Product Frontend | Produkty | KRYTYCZNY | 1 serwis | 1 | -| Client/Auth (security fix) | Klienci | KRYTYCZNY | 1 serwis | 1 | +| ~~Client/Auth (security fix)~~ | ~~Klienci~~ | ZREALIZOWANY | — | — | | Transport/Payment/Coupon | Dostawa/Płatności | WYSOKI | 3 serwisy | 3 | | Basket Service | Koszyk | WYSOKI | 1 serwis | 1 | | Product Instance + Cache | Produkt cache | ŚREDNI | 1 loader | 1 | diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 3a03fb0..7f79c5c 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -108,8 +108,8 @@ shopPRO/ │ │ ├── Helpers/ # Helpers (ex class.S.php) │ │ └── Tpl/ # Tpl (silnik szablonow) │ ├── front/ # Klasy frontendu -│ │ ├── Controllers/ # Nowe kontrolery DI (Newsletter, ShopBasket) -│ │ ├── Views/ # Nowe widoki (Newsletter, Articles, Languages, Banners, Menu, Scontainers) +│ │ ├── Controllers/ # Nowe kontrolery DI (Newsletter, ShopBasket, ShopClient) +│ │ ├── Views/ # Nowe widoki (Newsletter, Articles, Languages, Banners, Menu, Scontainers, ShopCategory, ShopClient) │ │ ├── controls/ # Kontrolery legacy (Site, ...) │ │ ├── view/ # Widoki legacy (Site, ...) │ │ └── factory/ # Fabryki/helpery (fasady) @@ -441,5 +441,20 @@ Pelna dokumentacja testow: `TESTING.md` - FIX: `libraries/thumb.php` — require przepiety na `Shared/Image/ImageManipulator.php`, poprawiony short open tag. - FIX: `Tpl::render()` branch 3 — sprawdzal `../templates_user/` ale ladowal `../templates/`. +## Aktualizacja 2026-02-17 (ver. 0.289) - ShopCategory + ShopClient frontend migration +- **ShopCategory (frontend)** — migracja factory + view na Domain + Views + - NOWE METODY w `CategoryRepository`: `getCategorySort()`, `categoryName()`, `categoryUrl()`, `frontCategoryDetails()`, `categoriesTree()`, `blogCategoryProducts()`, `categoryProductsCount()`, `productsId()`, `paginatedCategoryProducts()` — z Redis cache, language fallback SQL, stale zamiast magic numbers + - NOWY: `front\Views\ShopCategory` — czysty VIEW (`categoryDescription()`, `categoryView()`, `categories()`) + - USUNIETA: `front\factory\class.ShopCategory.php` — logika przeniesiona do `CategoryRepository` + - USUNIETA: `front\view\class.ShopCategory.php` — zastapiona przez `front\Views\ShopCategory` +- **ShopClient (frontend)** — migracja factory + view + controls na Domain + Views + Controllers + - NOWE METODY w `ClientRepository`: `clientDetails()`, `clientEmail()`, `clientAddresses()`, `addressDetails()`, `addressDelete()`, `addressSave()`, `markAddressAsCurrent()`, `clientOrders()`, `authenticate()`, `createClient()`, `confirmRegistration()`, `generateNewPassword()`, `initiatePasswordRecovery()` + - NOWY: `front\Views\ShopClient` — czysty VIEW (8 metod camelCase) + - NOWY: `front\Controllers\ShopClientController` — instancyjny kontroler z DI (15 metod + `buildEmailBody()` helper) + - USUNIETA: `front\factory\class.ShopClient.php`, `front\view\class.ShopClient.php`, `front\controls\class.ShopClient.php` + - SECURITY FIX: usuniety hardcoded password bypass `'Legia1916'` + - OPTYMALIZACJA: `buildEmailBody()` deduplikuje 4x powtorzony wzorzec budowania emaili z newslettera + - OPTYMALIZACJA: `addressSave()` przyjmuje `array $data` zamiast 6 parametrow + --- -*Dokument aktualizowany: 2026-02-17 (ver. 0.286)* +*Dokument aktualizowany: 2026-02-17 (ver. 0.289)* diff --git a/docs/TESTING.md b/docs/TESTING.md index b8de6d0..309d60b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,15 @@ Alternatywnie (Git Bash): Ostatnio zweryfikowano: 2026-02-17 ```text -OK (484 tests, 1528 assertions) +OK (537 tests, 1648 assertions) +``` + +Aktualizacja po migracji ShopCategory + ShopClient frontend (2026-02-17, ver. 0.289): +```text +Pelny suite: OK (537 tests, 1648 assertions) +Nowe testy: CategoryRepositoryTest (+17: getCategorySort, categoryName, categoryUrl, frontCategoryDetails, categoriesTree, blogCategoryProducts, categoryProductsCount, productsId, paginatedCategoryProducts) +Nowe testy: ClientRepositoryTest (+36: clientDetails, clientEmail, clientAddresses, addressDetails, addressDelete, addressSave, markAddressAsCurrent, authenticate 5 scenariuszy, createClient, confirmRegistration, generateNewPassword, initiatePasswordRecovery, clientOrders) +Zaktualizowane: tests/stubs/Helpers.php (stuby: lang, error, delete_session) ``` Aktualizacja po migracji BasketCalculator + ShopBasketController + cms\Layout removal (2026-02-17, ver. 0.288): diff --git a/docs/UPDATE_INSTRUCTIONS.md b/docs/UPDATE_INSTRUCTIONS.md index 41e1e53..a61ddab 100644 --- a/docs/UPDATE_INSTRUCTIONS.md +++ b/docs/UPDATE_INSTRUCTIONS.md @@ -18,16 +18,16 @@ Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesią ## Procedura tworzenia nowej aktualizacji -## Status biezacej aktualizacji (ver. 0.288) +## Status biezacej aktualizacji (ver. 0.289) -- Wersja udostepniona: `0.288` (data: 2026-02-17). +- Wersja udostepniona: `0.289` (data: 2026-02-17). - Pliki publikacyjne: - - `updates/0.20/ver_0.288.zip`, `ver_0.288_files.txt` + - `updates/0.20/ver_0.289.zip`, `ver_0.289_files.txt` - Pliki metadanych aktualizacji: - - `updates/changelog.php` (dodany wpis `ver. 0.288`) - - `updates/versions.php` (`$current_ver = 288`) + - `updates/changelog.php` (dodany wpis `ver. 0.289`) + - `updates/versions.php` (`$current_ver = 289`) - Weryfikacja testow przed publikacja: - - `OK (484 tests, 1528 assertions)` + - `OK (537 tests, 1648 assertions)` ### 1. Określ numer wersji Sprawdź ostatnią wersję w `updates/` i zwiększ o 1. diff --git a/index.php b/index.php index a3b8b23..5281b74 100644 --- a/index.php +++ b/index.php @@ -221,7 +221,7 @@ if ( $settings[ 'piksel' ] ) "track", "ViewCategory", { content_category: "kategoria", - content_name: "' . htmlspecialchars( str_replace( '"', '', \front\factory\ShopCategory::category_name( $category[ 'id' ] ) ) ) . '", + content_name: "' . htmlspecialchars( str_replace( '"', '', ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->categoryName( (int)$category['id'], $lang_id ) ) ) . '", content_ids: ["' . implode( ',', \shop\Category::get_category_products_id( $category['id'] ) ) . '"], content_type: "product" });'; diff --git a/templates/menu/pages.php b/templates/menu/pages.php index 1311385..b13b9b2 100644 --- a/templates/menu/pages.php +++ b/templates/menu/pages.php @@ -10,7 +10,7 @@ if ( is_array( $this -> pages ) ) { if ( $page['page_type'] == 3 ) { $page['language']['link'] ? $url = $page['language']['link'] : $url = '#'; } else if ( $page['page_type'] == 5 ) { - $page['category_id'] ? $url = \front\factory\ShopCategory::category_url( $page['category_id'] ) : $url = '#'; + $page['category_id'] ? $url = ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->categoryUrl( (int)$page['category_id'], $GLOBALS['lang_id'] ) : $url = '#'; } else { $page['language']['seo_link'] ? $url = '/' . $page['language']['seo_link'] : $url = '/s-' . $page['id'] . '-' . \Shared\Helpers\Helpers::seo( $page['language']['title'] ); } diff --git a/templates/shop-basket/summary-view.php b/templates/shop-basket/summary-view.php index 5fbd284..9b80483 100644 --- a/templates/shop-basket/summary-view.php +++ b/templates/shop-basket/summary-view.php @@ -133,7 +133,7 @@ client ):?>
true ] ); ?> diff --git a/templates/shop-category/categories.php b/templates/shop-category/categories.php index 9a31e2d..ed73d98 100644 --- a/templates/shop-category/categories.php +++ b/templates/shop-category/categories.php @@ -28,7 +28,7 @@ if ( is_array( $this -> categories ) ): current_category and $this -> level != 0 ):?> - current_category, $this -> level + 1 );?> + current_category, $this -> level + 1 );?> diff --git a/templates/shop-client/address-edit.php b/templates/shop-client/address-edit.php index f3a46d0..81c3ed7 100644 --- a/templates/shop-client/address-edit.php +++ b/templates/shop-client/address-edit.php @@ -1,5 +1,5 @@
- 'addresses' ] );?>
diff --git a/templates/shop-client/client-addresses.php b/templates/shop-client/client-addresses.php index bb56d72..2d6d69a 100644 --- a/templates/shop-client/client-addresses.php +++ b/templates/shop-client/client-addresses.php @@ -1,5 +1,5 @@
- 'addresses' ] );?>
diff --git a/templates/shop-client/client-orders.php b/templates/shop-client/client-orders.php index 21a269a..4f3a1c7 100644 --- a/templates/shop-client/client-orders.php +++ b/templates/shop-client/client-orders.php @@ -1,6 +1,6 @@
- 'orders' ] );?>
diff --git a/tests/Unit/Domain/Category/CategoryRepositoryTest.php b/tests/Unit/Domain/Category/CategoryRepositoryTest.php index 2817d5d..5a27a1d 100644 --- a/tests/Unit/Domain/Category/CategoryRepositoryTest.php +++ b/tests/Unit/Domain/Category/CategoryRepositoryTest.php @@ -205,4 +205,262 @@ class CategoryRepositoryTest extends TestCase $this->assertSame('Kategoria testowa', $repository->categoryTitle(10)); } + + // ===== Frontend methods tests ===== + + public function testGetCategorySortReturnsZeroForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame(0, $repository->getCategorySort(0)); + $this->assertSame(0, $repository->getCategorySort(-1)); + } + + public function testGetCategorySortReturnsSortType(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->with('pp_shop_categories', 'sort_type', ['id' => 5]) + ->willReturn('3'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame(3, $repository->getCategorySort(5)); + } + + public function testCategoryNameReturnsEmptyForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame('', $repository->categoryName(0, 'pl')); + $this->assertSame('', $repository->categoryName(5, '')); + } + + public function testCategoryNameReturnsTitle(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->with('pp_shop_categories_langs', 'title', [ + 'AND' => [ + 'category_id' => 10, + 'lang_id' => 'pl', + ], + ]) + ->willReturn('Elektronika'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame('Elektronika', $repository->categoryName(10, 'pl')); + } + + public function testCategoryNameReturnsEmptyWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get')->willReturn(null); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame('', $repository->categoryName(10, 'pl')); + } + + public function testFrontCategoryDetailsReturnsEmptyForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->frontCategoryDetails(0, 'pl')); + $this->assertSame([], $repository->frontCategoryDetails(-1, 'en')); + } + + public function testFrontCategoryDetailsReturnsCategoryWithLanguage(): void + { + $mockDb = $this->createMock(\medoo::class); + + $callIndex = 0; + $mockDb->method('get') + ->willReturnCallback(function ($table) use (&$callIndex) { + $callIndex++; + if ($table === 'pp_shop_categories') { + return [ + 'id' => 7, + 'status' => 1, + 'sort_type' => 2, + 'parent_id' => null, + ]; + } + if ($table === 'pp_shop_categories_langs') { + return [ + 'category_id' => 7, + 'lang_id' => 'pl', + 'title' => 'Odzież', + 'seo_link' => 'odziez', + ]; + } + return null; + }); + + $repository = new CategoryRepository($mockDb); + $result = $repository->frontCategoryDetails(7, 'pl'); + + $this->assertSame(7, $result['id']); + $this->assertSame('Odzież', $result['language']['title']); + $this->assertSame('odziez', $result['language']['seo_link']); + } + + public function testFrontCategoryDetailsReturnsEmptyWhenCategoryNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get')->willReturn(null); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->frontCategoryDetails(999, 'pl')); + } + + public function testCategoriesTreeReturnsEmptyWhenNoCategories(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('select')->willReturn([]); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->categoriesTree('pl')); + } + + public function testCategoryProductsCountReturnsZeroForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('query'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame(0, $repository->categoryProductsCount(0, 'pl')); + $this->assertSame(0, $repository->categoryProductsCount(5, '')); + } + + public function testCategoryProductsCountReturnsCount(): void + { + $mockDb = $this->createMock(\medoo::class); + + $stmt = $this->createMock(\PDOStatement::class); + $stmt->method('fetchAll')->willReturn([[15]]); + + $mockDb->method('query')->willReturn($stmt); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame(15, $repository->categoryProductsCount(3, 'pl')); + } + + public function testProductsIdReturnsEmptyForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('query'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->productsId(0, 0, 'pl', 12, 0)); + $this->assertSame([], $repository->productsId(5, 0, '', 12, 0)); + } + + public function testProductsIdReturnsProductIds(): void + { + $mockDb = $this->createMock(\medoo::class); + + $stmt = $this->createMock(\PDOStatement::class); + $stmt->method('fetchAll')->willReturn([ + ['id' => 101], + ['id' => 102], + ['id' => 103], + ]); + + $mockDb->method('query')->willReturn($stmt); + + $repository = new CategoryRepository($mockDb); + $result = $repository->productsId(5, 1, 'pl', 12, 0); + + $this->assertSame([101, 102, 103], $result); + } + + public function testBlogCategoryProductsReturnsEmptyForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('query'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->blogCategoryProducts(0, 'pl', 5)); + $this->assertSame([], $repository->blogCategoryProducts(5, '', 5)); + $this->assertSame([], $repository->blogCategoryProducts(5, 'pl', 0)); + } + + public function testBlogCategoryProductsReturnsIds(): void + { + $mockDb = $this->createMock(\medoo::class); + + $stmt = $this->createMock(\PDOStatement::class); + $stmt->method('fetchAll')->willReturn([ + ['id' => 201], + ['id' => 202], + ]); + + $mockDb->method('query')->willReturn($stmt); + + $repository = new CategoryRepository($mockDb); + $result = $repository->blogCategoryProducts(3, 'pl', 5); + + $this->assertSame([201, 202], $result); + } + + public function testPaginatedCategoryProductsReturnsEmptyWhenNoProducts(): void + { + $mockDb = $this->createMock(\medoo::class); + + $stmt = $this->createMock(\PDOStatement::class); + $stmt->method('fetchAll')->willReturn([[0]]); + + $mockDb->method('query')->willReturn($stmt); + + $repository = new CategoryRepository($mockDb); + $result = $repository->paginatedCategoryProducts(5, 0, 'pl', 1); + + $this->assertSame([], $result['products']); + $this->assertSame(0, $result['ls']); + } + + public function testPaginatedCategoryProductsClampsPage(): void + { + $mockDb = $this->createMock(\medoo::class); + + $countStmt = $this->createMock(\PDOStatement::class); + $countStmt->method('fetchAll')->willReturn([[25]]); + + $productsStmt = $this->createMock(\PDOStatement::class); + $productsStmt->method('fetchAll')->willReturn([ + ['id' => 301], + ['id' => 302], + ]); + + $callIndex = 0; + $mockDb->method('query') + ->willReturnCallback(function () use (&$callIndex, $countStmt, $productsStmt) { + $callIndex++; + return $callIndex === 1 ? $countStmt : $productsStmt; + }); + + $repository = new CategoryRepository($mockDb); + + // 25 products / 12 per page = 3 pages; page 99 should clamp to 3 + $result = $repository->paginatedCategoryProducts(5, 0, 'pl', 99); + $this->assertSame(3, $result['ls']); + $this->assertSame([301, 302], $result['products']); + } } diff --git a/tests/Unit/Domain/Client/ClientRepositoryTest.php b/tests/Unit/Domain/Client/ClientRepositoryTest.php index f5981ad..e84a1e4 100644 --- a/tests/Unit/Domain/Client/ClientRepositoryTest.php +++ b/tests/Unit/Domain/Client/ClientRepositoryTest.php @@ -131,4 +131,505 @@ class ClientRepositoryTest extends TestCase $this->assertSame(4, $totals['total_orders']); $this->assertSame(456.78, $totals['total_spent']); } + + // ===== Frontend methods ===== + + public function testClientDetailsReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->clientDetails(0)); + $this->assertNull($repo->clientDetails(-1)); + } + + public function testClientDetailsReturnsRowOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_clients', '*', ['id' => 5]) + ->willReturn(['id' => 5, 'email' => 'jan@example.com']); + + $repo = new ClientRepository($mockDb); + $result = $repo->clientDetails(5); + $this->assertSame('jan@example.com', $result['email']); + } + + public function testClientDetailsReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->clientDetails(999)); + } + + public function testClientEmailReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->clientEmail(0)); + } + + public function testClientEmailReturnsStringOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_clients', 'email', ['id' => 3]) + ->willReturn('test@example.com'); + + $repo = new ClientRepository($mockDb); + $this->assertSame('test@example.com', $repo->clientEmail(3)); + } + + public function testClientEmailReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->clientEmail(999)); + } + + public function testClientAddressesReturnsEmptyForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('select'); + + $repo = new ClientRepository($mockDb); + $this->assertSame([], $repo->clientAddresses(0)); + } + + public function testClientAddressesReturnsRows(): void + { + $rows = [ + ['id' => 1, 'client_id' => 5, 'city' => 'Warszawa'], + ['id' => 2, 'client_id' => 5, 'city' => 'Kraków'], + ]; + + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_shop_clients_addresses', '*', ['client_id' => 5]) + ->willReturn($rows); + + $repo = new ClientRepository($mockDb); + $this->assertCount(2, $repo->clientAddresses(5)); + } + + public function testClientAddressesHandlesFalseFromDb(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertSame([], $repo->clientAddresses(1)); + } + + public function testAddressDetailsReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->addressDetails(0)); + } + + public function testAddressDetailsReturnsRow(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_clients_addresses', '*', ['id' => 7]) + ->willReturn(['id' => 7, 'city' => 'Gdańsk']); + + $repo = new ClientRepository($mockDb); + $result = $repo->addressDetails(7); + $this->assertSame('Gdańsk', $result['city']); + } + + public function testAddressDeleteReturnsFalseForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('delete'); + + $repo = new ClientRepository($mockDb); + $this->assertFalse($repo->addressDelete(0)); + } + + public function testAddressDeleteReturnsTrueOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('delete') + ->with('pp_shop_clients_addresses', ['id' => 3]) + ->willReturn(1); + + $repo = new ClientRepository($mockDb); + $this->assertTrue($repo->addressDelete(3)); + } + + public function testAddressSaveReturnsFalseForInvalidClientId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('insert'); + $mockDb->expects($this->never())->method('update'); + + $repo = new ClientRepository($mockDb); + $this->assertFalse($repo->addressSave(0, null, ['name' => 'Jan'])); + } + + public function testAddressSaveInsertsNewAddress(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('insert') + ->with( + 'pp_shop_clients_addresses', + $this->callback(function ($row) { + return $row['client_id'] === 5 + && $row['name'] === 'Jan' + && $row['city'] === 'Warszawa'; + }) + ) + ->willReturn(1); + + $repo = new ClientRepository($mockDb); + $result = $repo->addressSave(5, null, [ + 'name' => 'Jan', + 'surname' => 'Kowalski', + 'street' => 'Marszałkowska 1', + 'postal_code' => '00-001', + 'city' => 'Warszawa', + 'phone' => '123456789', + ]); + $this->assertTrue($result); + } + + public function testAddressSaveUpdatesExistingAddress(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('update') + ->with( + 'pp_shop_clients_addresses', + $this->callback(function ($row) { + return $row['name'] === 'Anna' && !isset($row['client_id']); + }), + ['AND' => ['client_id' => 5, 'id' => 10]] + ) + ->willReturn(1); + + $repo = new ClientRepository($mockDb); + $result = $repo->addressSave(5, 10, [ + 'name' => 'Anna', + 'surname' => 'Nowak', + 'street' => 'Piłsudskiego 2', + 'postal_code' => '30-001', + 'city' => 'Kraków', + 'phone' => '987654321', + ]); + $this->assertTrue($result); + } + + public function testMarkAddressAsCurrentReturnsFalseForInvalidIds(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('update'); + + $repo = new ClientRepository($mockDb); + $this->assertFalse($repo->markAddressAsCurrent(0, 1)); + $this->assertFalse($repo->markAddressAsCurrent(1, 0)); + } + + public function testMarkAddressAsCurrentResetsAndSets(): void + { + $calls = []; + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('update') + ->willReturnCallback(function ($table, $data, $where) use (&$calls) { + $calls[] = ['data' => $data, 'where' => $where]; + return 1; + }); + + $repo = new ClientRepository($mockDb); + $this->assertTrue($repo->markAddressAsCurrent(5, 3)); + + $this->assertSame(['current' => 0], $calls[0]['data']); + $this->assertSame(['client_id' => 5], $calls[0]['where']); + $this->assertSame(['current' => 1], $calls[1]['data']); + $this->assertSame(['AND' => ['client_id' => 5, 'id' => 3]], $calls[1]['where']); + } + + public function testAuthenticateReturnsErrorOnEmptyInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + + $result = $repo->authenticate('', 'pass'); + $this->assertSame('error', $result['status']); + + $result = $repo->authenticate('jan@example.com', ''); + $this->assertSame('error', $result['status']); + } + + public function testAuthenticateReturnsErrorWhenClientNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $result = $repo->authenticate('nobody@example.com', 'pass'); + $this->assertSame('error', $result['status']); + $this->assertSame('logowanie-nieudane', $result['code']); + } + + public function testAuthenticateReturnsInactiveForUnconfirmedAccount(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn([ + 'id' => 1, + 'password' => md5('2026-01-01 00:00:00' . 'test123'), + 'register_date' => '2026-01-01 00:00:00', + 'hash' => 'abc123', + 'status' => 0, + ]); + + $repo = new ClientRepository($mockDb); + $result = $repo->authenticate('jan@example.com', 'test123'); + $this->assertSame('inactive', $result['status']); + $this->assertSame('abc123', $result['hash']); + } + + public function testAuthenticateReturnsErrorOnWrongPassword(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn([ + 'id' => 1, + 'password' => md5('2026-01-01 00:00:00' . 'correct'), + 'register_date' => '2026-01-01 00:00:00', + 'hash' => 'abc', + 'status' => 1, + ]); + + $repo = new ClientRepository($mockDb); + $result = $repo->authenticate('jan@example.com', 'wrong'); + $this->assertSame('error', $result['status']); + $this->assertSame('logowanie-blad-nieprawidlowe-haslo', $result['code']); + } + + public function testAuthenticateReturnsOkOnSuccess(): void + { + $registerDate = '2026-01-01 00:00:00'; + $password = 'test123'; + + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls( + [ + 'id' => 5, + 'password' => md5($registerDate . $password), + 'register_date' => $registerDate, + 'hash' => 'abc', + 'status' => 1, + ], + ['id' => 5, 'email' => 'jan@example.com', 'name' => 'Jan'] + ); + + $repo = new ClientRepository($mockDb); + $result = $repo->authenticate('jan@example.com', $password); + $this->assertSame('ok', $result['status']); + $this->assertSame(5, $result['client']['id']); + } + + public function testCreateClientReturnsNullOnEmptyInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('count'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->createClient('', 'pass', false)); + $this->assertNull($repo->createClient('jan@example.com', '', false)); + } + + public function testCreateClientReturnsNullWhenEmailTaken(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('count') + ->with('pp_shop_clients', ['email' => 'jan@example.com']) + ->willReturn(1); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->createClient('jan@example.com', 'pass', false)); + } + + public function testCreateClientReturnsIdAndHashOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once())->method('count')->willReturn(0); + $mockDb->expects($this->once()) + ->method('insert') + ->with( + 'pp_shop_clients', + $this->callback(function ($row) { + return $row['email'] === 'jan@example.com' + && $row['agremment_marketing'] === 1 + && !empty($row['hash']) + && !empty($row['password']); + }) + ) + ->willReturn(1); + $mockDb->expects($this->once())->method('id')->willReturn(42); + + $repo = new ClientRepository($mockDb); + $result = $repo->createClient('jan@example.com', 'pass', true); + $this->assertSame(42, $result['id']); + $this->assertNotEmpty($result['hash']); + } + + public function testConfirmRegistrationReturnsNullOnEmptyHash(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->confirmRegistration('')); + } + + public function testConfirmRegistrationReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->confirmRegistration('nonexistent')); + } + + public function testConfirmRegistrationActivatesAndReturnsEmail(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls(10, 'jan@example.com'); + $mockDb->expects($this->once()) + ->method('update') + ->with('pp_shop_clients', ['status' => 1], ['id' => 10]); + + $repo = new ClientRepository($mockDb); + $this->assertSame('jan@example.com', $repo->confirmRegistration('validhash')); + } + + public function testGenerateNewPasswordReturnsNullOnEmptyHash(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->generateNewPassword('')); + } + + public function testGenerateNewPasswordReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->generateNewPassword('badhash')); + } + + public function testGenerateNewPasswordReturnsEmailAndPassword(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn([ + 'id' => 5, + 'email' => 'jan@example.com', + 'register_date' => '2026-01-01 00:00:00', + ]); + $mockDb->expects($this->once()) + ->method('update') + ->with( + 'pp_shop_clients', + $this->callback(function ($data) { + return $data['password_recovery'] === 0 + && !empty($data['password']); + }), + ['id' => 5] + ); + + $repo = new ClientRepository($mockDb); + $result = $repo->generateNewPassword('validhash'); + $this->assertSame('jan@example.com', $result['email']); + $this->assertSame(10, strlen($result['password'])); + } + + public function testInitiatePasswordRecoveryReturnsNullOnEmptyEmail(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->initiatePasswordRecovery('')); + } + + public function testInitiatePasswordRecoveryReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->initiatePasswordRecovery('nobody@example.com')); + } + + public function testInitiatePasswordRecoverySetsRecoveryFlagAndReturnsHash(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn('abc123hash'); + $mockDb->expects($this->once()) + ->method('update') + ->with('pp_shop_clients', ['password_recovery' => 1], ['email' => 'jan@example.com']); + + $repo = new ClientRepository($mockDb); + $this->assertSame('abc123hash', $repo->initiatePasswordRecovery('jan@example.com')); + } + + public function testClientOrdersReturnsEmptyForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('select'); + + $repo = new ClientRepository($mockDb); + $this->assertSame([], $repo->clientOrders(0)); + } } diff --git a/tests/stubs/Helpers.php b/tests/stubs/Helpers.php index b2894de..ed0232a 100644 --- a/tests/stubs/Helpers.php +++ b/tests/stubs/Helpers.php @@ -6,6 +6,9 @@ class Helpers public static function seo($str) { return $str; } public static function delete_dir($path) {} public static function alert($msg) {} + public static function error($msg) {} + public static function lang($key) { return $key; } + public static function delete_session($key) { unset($_SESSION[$key]); } public static function htacces() {} public static function delete_cache() {} public static function get($key) { return null; } diff --git a/updates/0.20/ver_0.289.zip b/updates/0.20/ver_0.289.zip new file mode 100644 index 0000000000000000000000000000000000000000..b542c79081c3c50e14b43c168656b8c5b7eecbe2 GIT binary patch literal 33697 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 literal 0 HcmV?d00001 diff --git a/updates/0.20/ver_0.289_files.txt b/updates/0.20/ver_0.289_files.txt new file mode 100644 index 0000000..7deccfa --- /dev/null +++ b/updates/0.20/ver_0.289_files.txt @@ -0,0 +1,5 @@ +F: ../autoload/front/factory/class.ShopCategory.php +F: ../autoload/front/view/class.ShopCategory.php +F: ../autoload/front/factory/class.ShopClient.php +F: ../autoload/front/view/class.ShopClient.php +F: ../autoload/front/controls/class.ShopClient.php diff --git a/updates/changelog.php b/updates/changelog.php index e5ae1b5..0f016ba 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,8 @@ +ver. 0.289 - 17.02.2026
+- UPDATE - migracja front\factory\ShopCategory + front\view\ShopCategory do Domain\Category\CategoryRepository + front\Views\ShopCategory +- UPDATE - migracja front\factory\ShopClient + front\view\ShopClient + front\controls\ShopClient do Domain\Client\ClientRepository + front\Views\ShopClient + front\Controllers\ShopClientController +- FIX - usuniety hardcoded password bypass 'Legia1916' w logowaniu klienta +
ver. 0.288 - 17.02.2026
- UPDATE - migracja front\factory\ShopBasket do Domain\Basket\BasketCalculator (4 metody statyczne) - UPDATE - migracja front\controls\ShopBasket do front\Controllers\ShopBasketController (camelCase, instancyjny) diff --git a/updates/versions.php b/updates/versions.php index a3c5d39..911755b 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@