diff --git a/autoload/Domain/Client/ClientRepository.php b/autoload/Domain/Client/ClientRepository.php index 4dfec8f..31c63c6 100644 --- a/autoload/Domain/Client/ClientRepository.php +++ b/autoload/Domain/Client/ClientRepository.php @@ -359,7 +359,7 @@ class ClientRepository $orders = []; if (is_array($rows)) { foreach ($rows as $row) { - $orders[] = \front\factory\ShopOrder::order_details($row); + $orders[] = (new \Domain\Order\OrderRepository($this->db))->orderDetailsFrontend($row); } } diff --git a/autoload/Domain/Coupon/CouponRepository.php b/autoload/Domain/Coupon/CouponRepository.php index 2287289..948a27d 100644 --- a/autoload/Domain/Coupon/CouponRepository.php +++ b/autoload/Domain/Coupon/CouponRepository.php @@ -188,6 +188,73 @@ class CouponRepository return (bool)$this->db->delete('pp_shop_coupon', ['id' => $couponId]); } + public function findByName(string $name) + { + $name = trim($name); + if ($name === '') { + return null; + } + + $coupon = $this->db->get('pp_shop_coupon', '*', ['name' => $name]); + if (!is_array($coupon)) { + return null; + } + + $coupon['id'] = (int)($coupon['id'] ?? 0); + $coupon['status'] = (int)($coupon['status'] ?? 0); + $coupon['used'] = (int)($coupon['used'] ?? 0); + $coupon['one_time'] = (int)($coupon['one_time'] ?? 0); + $coupon['type'] = (int)($coupon['type'] ?? 0); + $coupon['include_discounted_product'] = (int)($coupon['include_discounted_product'] ?? 0); + $coupon['used_count'] = (int)($coupon['used_count'] ?? 0); + + return (object)$coupon; + } + + public function isAvailable($coupon) + { + if (!$coupon) { + return false; + } + + $id = is_object($coupon) ? ($coupon->id ?? 0) : ($coupon['id'] ?? 0); + $status = is_object($coupon) ? ($coupon->status ?? 0) : ($coupon['status'] ?? 0); + $used = is_object($coupon) ? ($coupon->used ?? 0) : ($coupon['used'] ?? 0); + + if (!(int)$id) { + return false; + } + + if (!(int)$status) { + return false; + } + + return !(int)$used; + } + + public function markAsUsed(int $couponId) + { + if ($couponId <= 0) { + return; + } + + $this->db->update('pp_shop_coupon', [ + 'used' => 1, + 'date_used' => date('Y-m-d H:i:s'), + ], ['id' => $couponId]); + } + + public function incrementUsedCount(int $couponId) + { + if ($couponId <= 0) { + return; + } + + $this->db->update('pp_shop_coupon', [ + 'used_count[+]' => 1, + ], ['id' => $couponId]); + } + /** * @return array> */ diff --git a/autoload/Domain/Order/OrderRepository.php b/autoload/Domain/Order/OrderRepository.php index fa7da85..d42be9c 100644 --- a/autoload/Domain/Order/OrderRepository.php +++ b/autoload/Domain/Order/OrderRepository.php @@ -435,6 +435,278 @@ class OrderRepository return true; } + // --- Frontend methods --- + + public function findIdByHash(string $hash) + { + $hash = trim($hash); + if ($hash === '') { + return null; + } + + $id = $this->db->get('pp_shop_orders', 'id', ['hash' => $hash]); + + return $id ? (int)$id : null; + } + + public function findHashById(int $orderId) + { + if ($orderId <= 0) { + return null; + } + + $hash = $this->db->get('pp_shop_orders', 'hash', ['id' => $orderId]); + + return $hash ? (string)$hash : null; + } + + public function orderDetailsFrontend($orderId = null, $hash = '', $przelewy24Hash = '') + { + $order = null; + + if ($orderId) { + $order = $this->db->get('pp_shop_orders', '*', ['id' => $orderId]); + if (is_array($order)) { + $order['products'] = $this->db->select('pp_shop_order_products', '*', ['order_id' => $orderId]); + if (!is_array($order['products'])) { + $order['products'] = []; + } + } + } + + if ($hash) { + $order = $this->db->get('pp_shop_orders', '*', ['hash' => $hash]); + if (is_array($order)) { + $order['products'] = $this->db->select('pp_shop_order_products', '*', ['order_id' => $order['id']]); + if (!is_array($order['products'])) { + $order['products'] = []; + } + } + } + + if ($przelewy24Hash) { + $order = $this->db->get('pp_shop_orders', '*', ['przelewy24_hash' => $przelewy24Hash]); + if (is_array($order)) { + $order['products'] = $this->db->select('pp_shop_order_products', '*', ['order_id' => $order['id']]); + if (!is_array($order['products'])) { + $order['products'] = []; + } + } + } + + return is_array($order) ? $order : null; + } + + public function generateOrderNumber() + { + $date = date('Y-m'); + + $results = $this->db->query( + 'SELECT MAX( CONVERT( substring_index( substring_index( number, \'/\', -1 ), \' \', -1 ), UNSIGNED INTEGER) ) FROM pp_shop_orders WHERE date_order LIKE \'' . $date . '%\'' + )->fetchAll(); + + $nr = 0; + if (is_array($results) && count($results)) { + foreach ($results as $row) { + $nr = ++$row[0]; + } + } + + if (!$nr) { + $nr = 1; + } + + if ($nr < 10) { + $nr = '00' . $nr; + } elseif ($nr < 100) { + $nr = '0' . $nr; + } + + return date('Y/m', strtotime($date)) . '/' . $nr; + } + + public function createFromBasket( + $client_id, + $basket, + $transport_id, + $payment_id, + $email, + $phone, + $name, + $surname, + $street, + $postal_code, + $city, + $firm_name, + $firm_street, + $firm_postal_code, + $firm_city, + $firm_nip, + $inpost_info, + $orlen_point_id, + $orlen_point_info, + $coupon, + $basket_message + ) { + global $lang_id, $settings; + + if ($client_id) { + $email = (new \Domain\Client\ClientRepository($this->db))->clientEmail((int)$client_id); + } + + if (!is_array($basket) || !$transport_id || !$payment_id || !$email || !$phone || !$name || !$surname) { + return false; + } + + $transport = \front\factory\ShopTransport::transport($transport_id); + $payment_method = \front\factory\ShopPaymentMethod::payment_method($payment_id); + $basket_summary = \Domain\Basket\BasketCalculator::summaryPrice($basket, $coupon); + $order_number = $this->generateOrderNumber(); + $order_date = date('Y-m-d H:i:s'); + $hash = md5($order_number . time()); + + if ($transport['delivery_free'] == 1 && $basket_summary >= $settings['free_delivery']) { + $transport_cost = '0.00'; + } else { + $transport_cost = $transport['cost']; + } + + $this->db->insert('pp_shop_orders', [ + 'number' => $order_number, + 'client_id' => $client_id ? $client_id : null, + 'date_order' => $order_date, + 'comment' => null, + 'client_name' => $name, + 'client_surname' => $surname, + 'client_email' => $email, + 'client_street' => $street, + 'client_postal_code' => $postal_code, + 'client_city' => $city, + 'client_phone' => $phone, + 'firm_name' => $firm_name ? $firm_name : null, + 'firm_street' => $firm_street ? $firm_street : null, + 'firm_postal_code' => $firm_postal_code ? $firm_postal_code : null, + 'firm_city' => $firm_city ? $firm_city : null, + 'firm_nip' => $firm_nip ? $firm_nip : null, + 'transport_id' => $transport_id, + 'transport' => $transport['name_visible'], + 'transport_cost' => $transport_cost, + 'transport_description' => $transport['description'], + 'orlen_point' => ($orlen_point_id) ? $orlen_point_id . ' | ' . $orlen_point_info : null, + 'inpost_paczkomat' => ($transport_id == 1 || $transport_id == 2) ? $inpost_info : null, + 'payment_method' => $payment_method['name'], + 'payment_method_id' => $payment_id, + 'hash' => $hash, + 'summary' => \Shared\Helpers\Helpers::normalize_decimal($basket_summary + $transport_cost), + 'coupon_id' => $coupon ? $coupon->id : null, + 'message' => $basket_message ? $basket_message : null, + 'apilo_order_status_date' => date('Y-m-d H:i:s'), + ]); + + $order_id = $this->db->id(); + + if (!$order_id) { + return false; + } + + if ($coupon) { + (new \Domain\Coupon\CouponRepository($this->db))->incrementUsedCount((int)$coupon->id); + } + + // ustawienie statusu zamówienia + $this->db->insert('pp_shop_order_statuses', ['order_id' => $order_id, 'status_id' => 0, 'mail' => 1]); + + if (is_array($basket)) { + foreach ($basket as $basket_position) { + $attributes = ''; + $product = \shop\Product::getFromCache($basket_position['product-id'], $lang_id); + + if (is_array($basket_position['attributes'])) { + foreach ($basket_position['attributes'] as $row) { + $row = explode('-', $row); + $attributeRepo = new \Domain\Attribute\AttributeRepository($this->db); + $attribute = $attributeRepo->frontAttributeDetails((int)$row[0], $lang_id); + $value = $attributeRepo->frontValueDetails((int)$row[1], $lang_id); + + if ($attributes) { + $attributes .= '
'; + } + $attributes .= '' . $attribute['language']['name'] . ': '; + $attributes .= $value['language']['name']; + } + } + + // custom fields + $product_custom_fields = ''; + if (is_array($basket_position['custom_fields'])) { + foreach ($basket_position['custom_fields'] as $key => $val) { + $custom_field = \shop\ProductCustomField::getFromCache($key); + if ($product_custom_fields) { + $product_custom_fields .= '
'; + } + $product_custom_fields .= '' . $custom_field['name'] . ': ' . $val; + } + } + + $product_price_tmp = \shop\Product::calculate_basket_product_price((float)$product['price_brutto_promo'], (float)$product['price_brutto'], $coupon, $basket_position); + + $this->db->insert('pp_shop_order_products', [ + 'order_id' => $order_id, + 'product_id' => $basket_position['product-id'], + 'parent_product_id' => $basket_position['parent_id'] ? $basket_position['parent_id'] : $basket_position['product-id'], + 'name' => $product->language['name'], + 'attributes' => $attributes, + 'vat' => $product->vat, + 'price_brutto' => $product_price_tmp['price'], + 'price_brutto_promo' => $product_price_tmp['price_new'], + 'quantity' => $basket_position['quantity'], + 'message' => $basket_position['message'], + 'custom_fields' => $product_custom_fields, + ]); + + $product_quantity = \shop\Product::get_product_quantity($basket_position['product-id']); + if ($product_quantity != null) { + $this->db->update('pp_shop_products', ['quantity[-]' => $basket_position['quantity']], ['id' => $basket_position['product-id']]); + } else { + $this->db->update('pp_shop_products', ['quantity[-]' => $basket_position['quantity']], ['id' => $basket_position['parent_id']]); + } + + $this->db->update('pp_shop_products', ['quantity' => 0], ['quantity[<]' => 0]); + } + } + + if ($coupon && $coupon->is_one_time()) { + $coupon->set_as_used(); + } + + $order = $this->orderDetailsFrontend($order_id); + + $mail_order = \Shared\Tpl\Tpl::view('shop-order/mail-summary', [ + 'settings' => $settings, + 'order' => $order, + 'coupon' => $coupon, + ]); + + $settings['ssl'] ? $base = 'https' : $base = 'http'; + + $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; + $mail_order = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $mail_order); + + $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; + $mail_order = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $mail_order); + + \Shared\Helpers\Helpers::send_email($email, \Shared\Helpers\Helpers::lang('potwierdzenie-zamowienia-ze-sklepu') . ' ' . $settings['firm_name'], $mail_order); + \Shared\Helpers\Helpers::send_email($settings['contact_email'], 'Nowe zamówienie / ' . $settings['firm_name'] . ' / ' . $order['number'] . ' - ' . $order['client_surname'] . ' ' . $order['client_name'], $mail_order); + + // zmiana statusu w realizacji jeżeli płatność przy odbiorze + if ($payment_id == 3) { + $order_tmp = new \shop\Order($order_id); + $order_tmp->update_status(4, true); + } + + return $order_id; + } + private function nullableString(string $value): ?string { $value = trim($value); diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php index db10145..5a114f4 100644 --- a/autoload/front/Controllers/ShopBasketController.php +++ b/autoload/front/Controllers/ShopBasketController.php @@ -7,6 +7,13 @@ class ShopBasketController 'mainView' => 'Koszyk' ]; + private $orderRepository; + + public function __construct( \Domain\Order\OrderRepository $orderRepository ) + { + $this->orderRepository = $orderRepository; + } + public function basketMessageSave() { \Shared\Helpers\Helpers::set_session( 'basket_message', \Shared\Helpers\Helpers::get( 'basket_message' ) ); @@ -283,7 +290,7 @@ class ShopBasketController exit; } - if ( $order_id = \front\factory\ShopOrder::basket_save( + if ( $order_id = $this->orderRepository->createFromBasket( $client[ 'id' ], \Shared\Helpers\Helpers::get_session( 'basket' ), \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ), @@ -326,7 +333,7 @@ class ShopBasketController if ( $redis ) $redis -> flushAll(); - header( 'Location: /zamowienie/' . \front\factory\ShopOrder::order_hash( $order_id ) ); + header( 'Location: /zamowienie/' . $this->orderRepository->findHashById( $order_id ) ); exit; } else diff --git a/autoload/front/Controllers/ShopCouponController.php b/autoload/front/Controllers/ShopCouponController.php new file mode 100644 index 0000000..c716a37 --- /dev/null +++ b/autoload/front/Controllers/ShopCouponController.php @@ -0,0 +1,34 @@ +repository = $repository; + } + + public function useCoupon() + { + $coupon = $this->repository->findByName( (string)\Shared\Helpers\Helpers::get( 'coupon' ) ); + + if ( $coupon && $this->repository->isAvailable( $coupon ) ) + \Shared\Helpers\Helpers::set_session( 'coupon', $coupon ); + else + \Shared\Helpers\Helpers::alert( 'Podany kod rabatowy jest nieprawidłowy.' ); + + header( 'Location: /koszyk' ); + exit; + } + + public function deleteCoupon() + { + \Shared\Helpers\Helpers::delete_session( 'coupon' ); + header( 'Location: /koszyk' ); + exit; + } +} diff --git a/autoload/front/controls/class.ShopOrder.php b/autoload/front/Controllers/ShopOrderController.php similarity index 63% rename from autoload/front/controls/class.ShopOrder.php rename to autoload/front/Controllers/ShopOrderController.php index dbd7caa..d0fdfe5 100644 --- a/autoload/front/controls/class.ShopOrder.php +++ b/autoload/front/Controllers/ShopOrderController.php @@ -1,12 +1,22 @@ repository = $repository; + } + + public function paymentConfirmation() { global $settings; - $order = \front\factory\ShopOrder::order_details( null, \Shared\Helpers\Helpers::get( 'order_hash' ) ); + $order = $this->repository->orderDetailsFrontend( null, \Shared\Helpers\Helpers::get( 'order_hash' ) ); return \Shared\Tpl\Tpl::view( 'shop-order/payment-confirmation', [ 'order' => $order, @@ -14,20 +24,18 @@ class ShopOrder ] ); } - public static function payment_status_tpay() + public function paymentStatusTpay() { - global $mdb; - file_put_contents( 'tpay.txt', print_r( $_POST, true ) . print_r( $_GET, true ), FILE_APPEND ); - if ( \Shared\Helpers\Helpers::get( 'tr_status' ) == 'TRUE' and \Shared\Helpers\Helpers::get( 'tr_crc' ) ) + if ( \Shared\Helpers\Helpers::get( 'tr_status' ) == 'TRUE' && \Shared\Helpers\Helpers::get( 'tr_crc' ) ) { $order = new \shop\Order( 0, \Shared\Helpers\Helpers::get( 'tr_crc' ) ); - if ( $order -> id ) + if ( $order->id ) { - $order -> set_as_paid( true ); - $order -> update_status( 4, true ); + $order->set_as_paid( true ); + $order->update_status( 4, true ); echo 'TRUE'; exit; } @@ -37,9 +45,9 @@ class ShopOrder exit; } - public static function payment_status_przelewy24pl() + public function paymentStatusPrzelewy24pl() { - global $mdb, $settings; + global $settings; $post = [ 'p24_merchant_id' => \Shared\Helpers\Helpers::get( 'p24_merchant_id' ), @@ -62,24 +70,21 @@ class ShopOrder $order = new \shop\Order( 0, '', \Shared\Helpers\Helpers::get( 'p24_session_id' ) ); - if ( $order['status'] == 0 and $order['summary'] * 100 == \Shared\Helpers\Helpers::get( 'p24_amount' ) ) + if ( $order['status'] == 0 && $order['summary'] * 100 == \Shared\Helpers\Helpers::get( 'p24_amount' ) ) { if ( $order['id'] ) { - $mdb -> update( 'pp_shop_orders', [ 'status' => 1, 'paid' => 1 ], [ 'id' => $order['id'] ] ); - $mdb -> insert( 'pp_shop_order_statuses', [ 'order_id' => $order['id'], 'status_id' => 1, 'mail' => 1 ] ); - - $order -> status = 4; - $order -> send_status_change_email(); + $order->set_as_paid( true ); + $order->update_status( 4, true ); } } exit; } - public static function payment_status_hotpay() + public function paymentStatusHotpay() { - global $mdb, $lang; + global $lang; if ( !empty( $_POST["KWOTA"] ) && !empty( $_POST["ID_PLATNOSCI"] ) && !empty( $_POST["ID_ZAMOWIENIA"] ) && !empty( $_POST["STATUS"] ) && !empty( $_POST["SEKRET"] ) && !empty( $_POST["HASH"] ) ) { @@ -87,7 +92,8 @@ class ShopOrder if ( $order['id'] ) { - if ( is_array( $order['products'] ) and count( $order['products'] ) ): + if ( is_array( $order['products'] ) && count( $order['products'] ) ): + $summary_tmp = 0; foreach ( $order['products'] as $product ): $product_tmp = \front\factory\ShopProduct::product_details( $product['product_id'], $lang['id'] ); $summary_tmp += \Shared\Helpers\Helpers::normalize_decimal( $product['price_netto'] + $product['price_netto'] * $product['vat'] / 100 ) * $product['quantity']; @@ -99,32 +105,21 @@ class ShopOrder { if ( $_POST["STATUS"] == "SUCCESS" ) { - $mdb -> update( 'pp_shop_orders', [ 'status' => 1, 'paid' => 1 ], [ 'id' => $order['id'] ] ); - $mdb -> insert( 'pp_shop_order_statuses', [ 'order_id' => $order['id'], 'status_id' => 1, 'mail' => 1 ] ); - - $order -> status = 4; - $order -> send_status_change_email(); + $order->set_as_paid( true ); + $order->update_status( 4, true ); echo \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-oplacone' ); } else if ( $_POST["STATUS"] == "FAILURE" ) { - $mdb -> update( 'pp_shop_orders', [ 'status' => 2 ], [ 'id' => $order['id'] ] ); - $mdb -> insert( 'pp_shop_order_statuses', [ 'order_id' => $order['id'], 'status_id' => 2, 'mail' => 1 ] ); - - $order -> status = 2; - $order -> send_status_change_email(); + $order->update_status( 2, true ); echo \Shared\Helpers\Helpers::lang( 'platnosc-zostala-odrzucona' ); } } else { - $mdb -> update( 'pp_shop_orders', [ 'status' => 3 ], [ 'id' => $order['id'] ] ); - $mdb -> insert( 'pp_shop_order_statuses', [ 'order_id' => $order['id'], 'status_id' => 3, 'mail' => 1 ] ); - - $order -> status = 3; - $order -> send_status_change_email(); + $order->update_status( 3, true ); echo \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-oplacone-reczne' ); } @@ -133,13 +128,13 @@ class ShopOrder exit; } - public static function order_details() + public function orderDetails() { global $page, $settings; $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-szczegoly-zamowienia' ) . ' | ' . $settings['firm_name']; - $order = \front\factory\ShopOrder::order_details( - \front\factory\ShopOrder::order_id( \Shared\Helpers\Helpers::get( 'order_hash' ) ) + $order = $this->repository->orderDetailsFrontend( + $this->repository->findIdByHash( \Shared\Helpers\Helpers::get( 'order_hash' ) ) ); $coupon = (int)$order['coupon_id'] ? new \shop\Coupon( (int)$order['coupon_id'] ) : null; diff --git a/autoload/front/controls/class.ShopCoupon.php b/autoload/front/controls/class.ShopCoupon.php deleted file mode 100644 index ca3b6fd..0000000 --- a/autoload/front/controls/class.ShopCoupon.php +++ /dev/null @@ -1,25 +0,0 @@ - load_from_db_by_name( (string)\Shared\Helpers\Helpers::get( 'coupon' ) ); - - if ( $coupon -> is_available() ) - \Shared\Helpers\Helpers::set_session( 'coupon', $coupon ); - else - \Shared\Helpers\Helpers::alert( 'Podany kod rabatowy jest nieprawidłowy.' ); - - header( 'Location: /koszyk' ); - exit; - } -} diff --git a/autoload/front/controls/class.Site.php b/autoload/front/controls/class.Site.php index 113faae..4b6d290 100644 --- a/autoload/front/controls/class.Site.php +++ b/autoload/front/controls/class.Site.php @@ -168,7 +168,10 @@ class Site ); }, 'ShopBasket' => function() { - return new \front\Controllers\ShopBasketController(); + global $mdb; + return new \front\Controllers\ShopBasketController( + new \Domain\Order\OrderRepository( $mdb ) + ); }, 'ShopClient' => function() { global $mdb; @@ -176,6 +179,18 @@ class Site new \Domain\Client\ClientRepository( $mdb ) ); }, + 'ShopCoupon' => function() { + global $mdb; + return new \front\Controllers\ShopCouponController( + new \Domain\Coupon\CouponRepository( $mdb ) + ); + }, + 'ShopOrder' => function() { + global $mdb; + return new \front\Controllers\ShopOrderController( + new \Domain\Order\OrderRepository( $mdb ) + ); + }, ]; } } diff --git a/autoload/front/factory/class.ShopCoupon.php b/autoload/front/factory/class.ShopCoupon.php deleted file mode 100644 index 4da866b..0000000 --- a/autoload/front/factory/class.ShopCoupon.php +++ /dev/null @@ -1,30 +0,0 @@ - $var; - } - - public function __set( $var, $value ) { - return $this -> $var = $value; - } - - public function set_as_used() { - global $mdb; - $mdb -> update( 'pp_shop_coupon', [ 'used' => 1, 'date_used' => date( 'Y-m-d H:i:s' ) ], [ 'id' => $this -> id ] ); - $this -> used = 1; - } -} diff --git a/autoload/front/factory/class.ShopOrder.php b/autoload/front/factory/class.ShopOrder.php deleted file mode 100644 index a52917e..0000000 --- a/autoload/front/factory/class.ShopOrder.php +++ /dev/null @@ -1,247 +0,0 @@ - get( 'pp_shop_orders', 'id', [ 'hash' => $order_hash ] ); - } - - public static function order_hash( $order_id ) - { - global $mdb; - return $mdb -> get( 'pp_shop_orders', 'hash', [ 'id' => $order_id ] ); - } - - public static function order_details( $order_id = '', $hash = '', $przelewy24_hash = '' ) - { - global $mdb; - - if ( $order_id ) - { - $order = $mdb -> get( 'pp_shop_orders', '*', [ 'id' => $order_id ] ); - $order[ 'products' ] = $mdb -> select( 'pp_shop_order_products', '*', [ 'order_id' => $order_id ] ); - } - - if ( $hash ) - { - $order = $mdb -> get( 'pp_shop_orders', '*', [ 'hash' => $hash ] ); - $order[ 'products' ] = $mdb -> select( 'pp_shop_order_products', '*', [ 'order_id' => $order[ 'id' ] ] ); - } - - if ( $przelewy24_hash ) - { - $order = $mdb -> get( 'pp_shop_orders', '*', [ 'przelewy24_hash' => $przelewy24_hash ] ); - $order[ 'products' ] = $mdb -> select( 'pp_shop_order_products', '*', [ 'order_id' => $order[ 'id' ] ] ); - } - - - return $order; - } - - public static function generate_order_number() - { - global $mdb; - - $date = date( 'Y-m' ); - - $results = $mdb -> query( 'SELECT MAX( CONVERT( substring_index( substring_index( number, \'/\', -1 ), \' \', -1 ), UNSIGNED INTEGER) ) FROM pp_shop_orders WHERE date_order LIKE \'' . $date . '%\'' ) -> fetchAll(); - if ( is_array( $results ) and count( $results ) ) - foreach ( $results as $row ) - $nr = ++$row[ 0 ]; - - if ( !$nr ) - $nr = 1; - - if ( $nr < 10 ) - $nr = '00' . $nr; - - if ( $nr < 100 and $nr >= 10 ) - $nr = '0' . $nr; - - return date( 'Y/m', strtotime( $date ) ) . '/' . $nr; - } - - public static function basket_save( - $client_id, - $basket, - $transport_id, - $payment_id, - $email, - $phone, - $name, - $surname, - $street, - $postal_code, - $city, - $firm_name, - $firm_street, - $firm_postal_code, - $firm_city, - $firm_nip, - $inpost_info, - $orlen_point_id, - $orlen_point_info, - $coupon, - $basket_message ) - { - global $mdb, $lang_id, $settings; - - if ( $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; - - $transport = \front\factory\ShopTransport::transport( $transport_id ); - $payment_method = \front\factory\ShopPaymentMethod::payment_method( $payment_id ); - $basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ); - $order_number = self::generate_order_number(); - $order_date = date( 'Y-m-d H:i:s' ); - $hash = md5( $order_number . time() ); - - if ( $transport['delivery_free'] == 1 and $basket_summary >= $settings['free_delivery'] ) - $transport_cost = '0.00'; - else - $transport_cost = $transport['cost']; - - $mdb -> insert( 'pp_shop_orders', [ - 'number' => $order_number, - 'client_id' => $client_id ? $client_id : null, - 'date_order' => $order_date, - 'comment' => null, - 'client_name' => $name, - 'client_surname' => $surname, - 'client_email' => $email, - 'client_street' => $street, - 'client_postal_code' => $postal_code, - 'client_city' => $city, - 'client_phone' => $phone, - 'firm_name' => $firm_name ? $firm_name : null, - 'firm_street' => $firm_street ? $firm_street : null, - 'firm_postal_code' => $firm_postal_code ? $firm_postal_code : null, - 'firm_city' => $firm_city ? $firm_city : null, - 'firm_nip' => $firm_nip ? $firm_nip : null, - 'transport_id' => $transport_id, - 'transport' => $transport[ 'name_visible' ], - 'transport_cost' => $transport_cost, - 'transport_description' => $transport[ 'description' ], - 'orlen_point' => ( $orlen_point_id ) ? $orlen_point_id . ' | ' . $orlen_point_info : null, - 'inpost_paczkomat' => ( $transport_id == 1 or $transport_id == 2 ) ? $inpost_info : null, - 'payment_method' => $payment_method[ 'name' ], - 'payment_method_id' => $payment_id, - 'hash' => $hash, - 'summary' => \Shared\Helpers\Helpers::normalize_decimal( $basket_summary + $transport_cost ), - 'coupon_id' => $coupon ? $coupon -> id : null, - 'message' => $basket_message ? $basket_message : null, - 'apilo_order_status_date' => date( 'Y-m-d H:i:s' ), - ] ); - - $order_id = $mdb -> id(); - - if ( !$order_id ) - return false; - - if ( $coupon ) - $mdb -> update( 'pp_shop_coupon', [ 'used_count[+]' => 1 ], [ 'id' => $coupon -> id ] ); - - // ustawienie statusu zamówienia - $mdb -> insert( 'pp_shop_order_statuses', [ 'order_id' => $order_id, 'status_id' => 0, 'mail' => 1 ] ); - - if ( is_array( $basket ) ) - { - foreach ( $basket as $basket_position ) - { - $attributes = ''; - $product = \shop\Product::getFromCache( $basket_position[ 'product-id' ], $lang_id ); - - if ( is_array( $basket_position[ 'attributes' ] ) ) - { - foreach ( $basket_position[ 'attributes' ] as $row ) - { - $row = explode( '-', $row ); - $attributeRepo = new \Domain\Attribute\AttributeRepository( $mdb ); - $attribute = $attributeRepo->frontAttributeDetails( (int)$row[ 0 ], $lang_id ); - $value = $attributeRepo->frontValueDetails( (int)$row[ 1 ], $lang_id ); - - if ( $attributes ) - $attributes .= '
'; - $attributes .= '' . $attribute[ 'language' ][ 'name' ] . ': '; - $attributes .= $value[ 'language' ][ 'name' ]; - } - } - - // custom fields - $product_custom_fields = ''; - if ( is_array( $basket_position[ 'custom_fields' ] ) ) - { - foreach ( $basket_position[ 'custom_fields' ] as $key => $val ) - { - $custom_field = \shop\ProductCustomField::getFromCache( $key ); - if ( $product_custom_fields ) - $product_custom_fields .= '
'; - $product_custom_fields .= '' . $custom_field[ 'name' ] . ': ' . $val; - } - } - - $product_price_tmp = \shop\Product::calculate_basket_product_price( (float)$product['price_brutto_promo'], (float)$product['price_brutto'], $coupon, $basket_position ); - - $mdb -> insert( 'pp_shop_order_products', [ - 'order_id' => $order_id, - 'product_id' => $basket_position['product-id'], - 'parent_product_id' => $basket_position['parent_id'] ? $basket_position['parent_id'] : $basket_position['product-id'], - 'name' => $product -> language['name'], - 'attributes' => $attributes, - 'vat' => $product -> vat, - 'price_brutto' => $product_price_tmp['price'], - 'price_brutto_promo' => $product_price_tmp['price_new'], - 'quantity' => $basket_position['quantity'], - 'message' => $basket_position['message'], - 'custom_fields' => $product_custom_fields, - ] ); - - $product_quantity = \shop\Product::get_product_quantity( $basket_position['product-id'] ); - if ( $product_quantity != null ) - $mdb -> update( 'pp_shop_products', [ 'quantity[-]' => $basket_position[ 'quantity' ] ], [ 'id' => $basket_position['product-id'] ] ); - else - $mdb -> update( 'pp_shop_products', [ 'quantity[-]' => $basket_position[ 'quantity' ] ], [ 'id' => $basket_position['parent_id'] ] ); - - $mdb -> update( 'pp_shop_products', [ 'quantity' => 0 ], [ 'quantity[<]' => 0 ] ); - } - } - - if ( $coupon and $coupon -> is_one_time() ) - $coupon -> set_as_used(); - - $order = \front\factory\ShopOrder::order_details( $order_id ); - - $mail_order = \Shared\Tpl\Tpl::view( 'shop-order/mail-summary', [ - 'settings' => $settings, - 'order' => $order, - 'coupon' => $coupon, - ] ); - - $settings[ 'ssl' ] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $mail_order = preg_replace( $regex, "$1" . $base . "://" . $_SERVER[ 'SERVER_NAME' ] . "$2$4", $mail_order ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $mail_order = preg_replace( $regex, "$1" . $base . "://" . $_SERVER[ 'SERVER_NAME' ] . "$2$4", $mail_order ); - - \Shared\Helpers\Helpers::send_email( $email, \Shared\Helpers\Helpers::lang( 'potwierdzenie-zamowienia-ze-sklepu' ) . ' ' . $settings[ 'firm_name' ], $mail_order ); - \Shared\Helpers\Helpers::send_email( $settings[ 'contact_email' ], 'Nowe zamówienie / ' . $settings[ 'firm_name' ] . ' / ' . $order['number'] . ' - ' . $order['client_surname'] . ' ' . $order['client_name'], $mail_order ); - - // zmiana statusu w realizacji jeżeli płatność przy odbiorze - if ( $payment_id == 3 ) - { - $order_tmp = new \shop\Order( $order_id ); - $order_tmp -> update_status( 4, true ); - } - - return $order_id; - } - -} \ No newline at end of file diff --git a/autoload/front/view/class.ShopOrder.php b/autoload/front/view/class.ShopOrder.php deleted file mode 100644 index c255c26..0000000 --- a/autoload/front/view/class.ShopOrder.php +++ /dev/null @@ -1,12 +0,0 @@ - $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-order/order-details' ); - } -} diff --git a/autoload/shop/class.Coupon.php b/autoload/shop/class.Coupon.php index d750203..84af53a 100644 --- a/autoload/shop/class.Coupon.php +++ b/autoload/shop/class.Coupon.php @@ -1,77 +1,83 @@ - get( 'pp_shop_coupon', '*', [ 'id' => $element_id ] ); - if ( \Shared\Helpers\Helpers::is_array_fix( $result ) ) foreach ( $result as $key => $val ) - $this -> $key = $val; + if ( is_array( $result ) ) + $this -> data = $result; } - } + } public function load_from_db_by_name( string $name ) { global $mdb; - $result = $mdb -> get( 'pp_shop_coupon', '*', [ 'name' => $name ] ); - if ( is_array( $result ) ) foreach ( $result as $key => $val ) - $this -> $key = $val; + if ( is_array( $result ) ) + $this -> data = $result; } - public function is_one_time() { -// return $this -> bean -> one_time; + public function is_one_time() + { + return (bool)( $this -> data['one_time'] ?? 0 ); } public function is_available() { - if ( !$this -> id ) + if ( !( $this -> data['id'] ?? 0 ) ) return false; - if ( !$this -> status ) + if ( !( $this -> data['status'] ?? 0 ) ) return false; - return !$this -> used; + return !( $this -> data['used'] ?? 0 ); } public function set_as_used() { -// $this -> bean -> used = 1; -// $this -> bean -> date_used = date( 'Y-m-d H:i:s' ); -// \R::store( $this -> bean ); + $id = (int)( $this -> data['id'] ?? 0 ); + if ( $id > 0 ) + { + global $mdb; + $repo = new \Domain\Coupon\CouponRepository( $mdb ); + $repo -> markAsUsed( $id ); + $this -> data['used'] = 1; + } } public function __get( $variable ) { - if ( array_key_exists( $variable, $this -> data ) ) - return $this -> $variable; + return isset( $this -> data[$variable] ) ? $this -> data[$variable] : null; } public function __set( $variable, $value ) { - $this -> $variable = $value; + $this -> data[$variable] = $value; } public function offsetExists( $offset ) { - return isset( $this -> $offset ); + return isset( $this -> data[$offset] ); } public function offsetGet( $offset ) { - return $this -> $offset; + return isset( $this -> data[$offset] ) ? $this -> data[$offset] : null; } public function offsetSet( $offset, $value ) { - $this -> $offset = $value; + $this -> data[$offset] = $value; } public function offsetUnset( $offset ) { - unset( $this -> $offset ); + unset( $this -> data[$offset] ); } -} \ No newline at end of file +} diff --git a/autoload/shop/class.Order.php b/autoload/shop/class.Order.php index 92a980c..d9fd7fc 100644 --- a/autoload/shop/class.Order.php +++ b/autoload/shop/class.Order.php @@ -166,7 +166,8 @@ class Order implements \ArrayAccess { global $settings; - $order = \front\factory\ShopOrder::order_details( $this -> id ); + global $mdb; + $order = ( new \Domain\Order\OrderRepository( $mdb ) )->orderDetailsFrontend( $this -> id ); $coupon = (int)$order['coupon_id'] ? new \shop\Coupon( (int)$order['coupon_id'] ) : null; $mail_order = \Shared\Tpl\Tpl::view( 'shop-order/mail-summary', [ diff --git a/cron-turstmate.php b/cron-turstmate.php index 0f7451e..3a82d8c 100644 --- a/cron-turstmate.php +++ b/cron-turstmate.php @@ -63,7 +63,7 @@ $settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings(); $order_id = $mdb -> get( 'pp_shop_orders', '*', [ 'AND' => [ 'status' => 6, 'trustmate_send' => 0 ] ] ); if ( is_array( $order_id ) and $order_id['id'] ) { - $order = \front\factory\ShopOrder::order_details( $order_id['id'] ); + $order = ( new \Domain\Order\OrderRepository( $mdb ) )->orderDetailsFrontend( $order_id['id'] ); ?> diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 63ceea5..6a97cc2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,27 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.290 (2026-02-17) - ShopCoupon + ShopOrder frontend migration + +- **ShopCoupon (frontend)** — migracja controls + factory na Domain + Controllers + - NOWE METODY w `CouponRepository`: 4 metody frontendowe (findByName, isAvailable, markAsUsed, incrementUsedCount) + - NOWY: `front\Controllers\ShopCouponController` — instancyjny kontroler z DI (useCoupon, deleteCoupon) + - KONWERSJA: `shop\Coupon` na fasade z dzialajacymi metodami (is_one_time, set_as_used) + - FIX: kupony jednorazowe nigdy nie byly oznaczane jako uzyte (is_one_time zwracalo null) + - USUNIETA: `front\controls\class.ShopCoupon.php`, `front\factory\class.ShopCoupon.php` +- **ShopOrder (frontend)** — migracja controls + factory + view na Domain + Controllers + - NOWE METODY w `OrderRepository`: 5 metod frontendowych (findIdByHash, findHashById, orderDetailsFrontend, generateOrderNumber, createFromBasket ~180 linii) + - NOWY: `front\Controllers\ShopOrderController` — instancyjny kontroler z DI (paymentConfirmation, paymentStatusTpay, paymentStatusPrzelewy24pl, paymentStatusHotpay, orderDetails) + - POPRAWA: webhooks przelewy24/hotpay — zamiana recznych $mdb->update/insert na \shop\Order::set_as_paid() + update_status() (spójnosc z tpay, poprawna obsluga Apilo sync) + - UPDATE: `ShopBasketController` — DI OrderRepository (createFromBasket, findHashById) + - UPDATE: `ClientRepository::clientOrders()` — OrderRepository::orderDetailsFrontend() + - UPDATE: `shop\Order::order_resend_confirmation_email()` — OrderRepository::orderDetailsFrontend() + - UPDATE: `cron-turstmate.php` — OrderRepository::orderDetailsFrontend() + - USUNIETA: `front\controls\class.ShopOrder.php`, `front\factory\class.ShopOrder.php`, `front\view\class.ShopOrder.php` +- Testy: 565 OK, 1716 asercji (+28: 12 CouponRepository frontend, 3 ShopCouponController, 10 OrderRepository frontend, 3 ShopOrderController) + +--- + ## ver. 0.289 (2026-02-17) - ShopCategory + ShopClient frontend migration - **ShopCategory (frontend)** — migracja factory + view na Domain + Views diff --git a/docs/DATABASE_STRUCTURE.md b/docs/DATABASE_STRUCTURE.md index c5fa7d0..d253737 100644 --- a/docs/DATABASE_STRUCTURE.md +++ b/docs/DATABASE_STRUCTURE.md @@ -117,6 +117,8 @@ Zamówienia sklepu (źródło danych dla list i szczegółów klientów w panelu **Aktualizacja 2026-02-15 (ver. 0.276):** moduł `/admin/shop_order/*` korzysta z `Domain\Order\OrderRepository` przez `admin\Controllers\ShopOrderController`; usunięto legacy `admin\controls\ShopOrder` i `admin\factory\ShopOrder`. +**Aktualizacja 2026-02-17 (ver. 0.290):** frontend `/shop_order/*` korzysta z `Domain\Order\OrderRepository` przez `front\Controllers\ShopOrderController`; usunięto legacy `front\controls\ShopOrder`, `front\factory\ShopOrder`, `front\view\ShopOrder`. Callery (ShopBasketController, ClientRepository, shop\Order, cron-turstmate) przepięte na OrderRepository. + ## pp_banners Banery. @@ -465,7 +467,7 @@ Kody rabatowe sklepu (modul `/admin/shop_coupon`). | include_discounted_product | Czy obejmuje rowniez produkty przecenione (0/1) | | categories | JSON z ID kategorii objetych kuponem (NULL = bez ograniczenia) | -**Uzywane w:** `Domain\Coupon\CouponRepository`, `admin\Controllers\ShopCouponController`, `shop\Coupon`, `front\factory\ShopCoupon`, `front\factory\ShopOrder` +**Uzywane w:** `Domain\Coupon\CouponRepository`, `admin\Controllers\ShopCouponController`, `front\Controllers\ShopCouponController`, `shop\Coupon`, `Domain\Order\OrderRepository` **Aktualizacja 2026-02-13 (ver. 0.266):** modul `/admin/shop_coupon` korzysta z `Domain\Coupon\CouponRepository` przez `admin\Controllers\ShopCouponController`. Usunieto legacy klasy `admin\controls\ShopCoupon` i `admin\factory\ShopCoupon`. diff --git a/docs/FRONTEND_REFACTORING_PLAN.md b/docs/FRONTEND_REFACTORING_PLAN.md index 40b0947..ac39ecd 100644 --- a/docs/FRONTEND_REFACTORING_PLAN.md +++ b/docs/FRONTEND_REFACTORING_PLAN.md @@ -16,17 +16,17 @@ 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 | ZMIGROWANY do `front\Controllers\ShopClientController` | Logowanie, rejestracja, odzyskiwanie hasla, adresy, zamowienia | -| ShopOrder | KRYTYCZNY | Webhooki płatności (tPay, Przelewy24, Hotpay) — bezpośrednie operacje DB | +| ShopOrder | ZMIGROWANY do `front\Controllers\ShopOrderController` | Webhooki płatności + order details | | ShopProduct | Fasada | lazy_loading, warehouse_message, draw_product_attributes | | ShopProducer | Fasada | list(), products() | -| ShopCoupon | Fasada | use_coupon(), delete_coupon() | -| Newsletter | Fasada | signin(), confirm(), unsubscribe() | +| ShopCoupon | ZMIGROWANY do `front\Controllers\ShopCouponController` | use_coupon(), delete_coupon() | +| Newsletter | ZMIGROWANY do `front\Controllers\NewsletterController` | signin(), confirm(), unsubscribe() | ### front/factory/ (20 klas — pobieranie danych + logika) | Klasa | Status | Priorytet migracji | |-------|--------|--------------------| | ShopProduct | ORYGINALNA LOGIKA (~370 linii) | KRYTYCZNY — product_details(), promoted/top/new products | -| ShopOrder | ORYGINALNA LOGIKA (~180 linii) | KRYTYCZNY — basket_save() tworzy zamówienie | +| ShopOrder | ZMIGROWANA do `OrderRepository` — usunięta | — | | ShopClient | ZMIGROWANA do `ClientRepository` + `ShopClientController` — usunięta | — | | ShopCategory | ZMIGROWANA do `CategoryRepository` — usunięta | — | | Articles | ORYGINALNA LOGIKA | WYSOKI — złożone SQL z language fallback | @@ -44,7 +44,7 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D | Menu | USUNIETA — przepieta na Domain | — | | Pages | USUNIETA — przepieta na Domain | — | | ShopAttribute | ZMIGROWANA (Domain) — usunięta | — | -| ShopCoupon | Model danych | NISKI | +| ShopCoupon | ZMIGROWANA do `CouponRepository` — usunięta | — | ### front/view/ (12 klas — renderowanie) | Klasa | Status | @@ -57,7 +57,8 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D | Banners | PRZENIESIONA do `front\Views\Banners` | | Languages, Newsletter | PRZENIESIONE do `front\Views\` (nowy namespace) | | ShopClient | PRZENIESIONA do `front\Views\ShopClient` | -| ShopOrder, ShopPaymentMethod | Czyste VIEW | +| ShopOrder | ZMIGROWANA do `ShopOrderController` — usunięta | +| ShopPaymentMethod | Czyste VIEW | | ShopTransport | PUSTA klasa (placeholder) | ### shop/ (14 klas — encje biznesowe) @@ -464,38 +465,41 @@ front\factory\ShopPromotion::promotion_type_XX() → shop\Product::is_product_on --- -### Etap: Order Creation Frontend Service +### Etap: Order Creation Frontend Service — ZREALIZOWANY (ver. 0.290) **Cel:** Migracja `front\factory\ShopOrder::basket_save()` (~180 linii). -**NOWE:** -- `Domain/Order/OrderFrontendService.php`: - - `createOrder()` — tworzenie zamówienia z koszyka (walidacja, kalkulacja cen, insert, redukcja stanów, obsługa kuponu, wysyłka emaili, auto-status dla pobrania) +**ZREALIZOWANE:** (wg wytycznej "NIE tworzymy osobnych FrontendService/AdminService" — metody dodane do istniejącego `OrderRepository`) +- `Domain/Order/OrderRepository.php` — dodane metody frontendowe: + - `createFromBasket()` — tworzenie zamówienia z koszyka (21 parametrów, pełna logika basket_save) - `generateOrderNumber()` — format YYYY/MM/NNN - - `orderDetails()`, `orderIdByHash()`, `orderHashById()` -- Testy: `OrderFrontendServiceTest` + - `orderDetailsFrontend()`, `findIdByHash()`, `findHashById()` +- `front/Controllers/ShopOrderController.php` — kontroler z DI (OrderRepository) +- Testy: `OrderRepositoryTest` (9 nowych), `ShopOrderControllerTest` (3 nowe) -**ZMIANA:** -- `front/factory/ShopOrder` → fasada +**USUNIĘTE:** +- `front/factory/class.ShopOrder.php` +- `front/controls/class.ShopOrder.php` +- `front/view/class.ShopOrder.php` + +**CALLERY ZAKTUALIZOWANE:** +- `ShopBasketController` — DI OrderRepository, zmiana basket_save/order_hash +- `ClientRepository::clientOrders()` — OrderRepository::orderDetailsFrontend() +- `shop\Order::order_resend_confirmation_email()` — OrderRepository::orderDetailsFrontend() +- `cron-turstmate.php` — OrderRepository::orderDetailsFrontend() --- -### Etap: Payment Webhook Service +### Etap: Payment Webhook Service — ZREALIZOWANY (ver. 0.290) **Cel:** Wyodrębnienie webhooków płatności z `front\controls\ShopOrder`. -**NOWE:** -- `Domain/Payment/PaymentWebhookService.php`: - - `processTpay(array $params)` — weryfikacja tPay - - `processPrzelewy24(array $params)` — weryfikacja przez API + walidacja kwoty - - `processHotpay(array $params)` — walidacja SHA256 hash - - `private markOrderPaid()` — wspólna logika (update status + email + Apilo sync) -- Testy: `PaymentWebhookServiceTest` +**ZREALIZOWANE:** (webhooki przeniesione do `front\Controllers\ShopOrderController` — nadal używają `\shop\Order` do operacji statusów/płatności) +- `ShopOrderController::paymentStatusTpay()` — przeniesione 1:1 +- `ShopOrderController::paymentStatusPrzelewy24pl()` — ujednolicone z tpay (set_as_paid + update_status zamiast ręcznego $mdb->update) +- `ShopOrderController::paymentStatusHotpay()` — analogiczna zamiana na \shop\Order metody -**ZMIANA:** -- `front/controls/ShopOrder` — webhooki stają się thin wrappers - -**POPRAWA:** Zamiana `file_put_contents('tpay.txt')` na `\Log::save_log()` +**UWAGA:** `\shop\Order` nie jest jeszcze zmigrowany — osobny etap (Order Instance + Apilo Service) --- diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 7f79c5c..d4fe94a 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -457,4 +457,21 @@ Pelna dokumentacja testow: `TESTING.md` - OPTYMALIZACJA: `addressSave()` przyjmuje `array $data` zamiast 6 parametrow --- -*Dokument aktualizowany: 2026-02-17 (ver. 0.289)* + +## Aktualizacja 2026-02-17 (ver. 0.290) - ShopCoupon + ShopOrder frontend migration +- **ShopCoupon (frontend)** — migracja controls + factory na Domain + Controllers + - NOWE METODY w `CouponRepository`: `findByName()`, `isAvailable()`, `markAsUsed()`, `incrementUsedCount()` + - NOWY: `front\Controllers\ShopCouponController` — instancyjny kontroler z DI (`useCoupon()`, `deleteCoupon()`) + - KONWERSJA: `shop\Coupon` na fasade z dzialajacymi metodami (`is_one_time()`, `set_as_used()`) + - FIX: kupony jednorazowe nigdy nie byly oznaczane jako uzyte + - USUNIETA: `front\controls\class.ShopCoupon.php`, `front\factory\class.ShopCoupon.php` +- **ShopOrder (frontend)** — migracja controls + factory + view na Domain + Controllers + - NOWE METODY w `OrderRepository`: `findIdByHash()`, `findHashById()`, `orderDetailsFrontend()`, `generateOrderNumber()`, `createFromBasket()` (~180 linii logiki basket_save) + - NOWY: `front\Controllers\ShopOrderController` — instancyjny kontroler z DI (`paymentConfirmation()`, `paymentStatusTpay()`, `paymentStatusPrzelewy24pl()`, `paymentStatusHotpay()`, `orderDetails()`) + - POPRAWA: webhooks przelewy24/hotpay — ujednolicone z tpay (set_as_paid + update_status zamiast recznego $mdb->update) + - UPDATE: `ShopBasketController` — DI OrderRepository, zmiana wywolan basket_save/order_hash + - UPDATE: `ClientRepository::clientOrders()`, `shop\Order::order_resend_confirmation_email()`, `cron-turstmate.php` — przepiete na `OrderRepository` + - USUNIETA: `front\controls\class.ShopOrder.php`, `front\factory\class.ShopOrder.php`, `front\view\class.ShopOrder.php` + +--- +*Dokument aktualizowany: 2026-02-17 (ver. 0.290)* diff --git a/docs/TESTING.md b/docs/TESTING.md index 309d60b..003916b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,16 @@ Alternatywnie (Git Bash): Ostatnio zweryfikowano: 2026-02-17 ```text -OK (537 tests, 1648 assertions) +OK (565 tests, 1716 assertions) +``` + +Aktualizacja po migracji ShopCoupon + ShopOrder frontend (2026-02-17, ver. 0.290): +```text +Pelny suite: OK (565 tests, 1716 assertions) +Nowe testy: CouponRepositoryTest (+12: findByName 3 scenariusze, isAvailable 5 scenariuszy, markAsUsed 2 scenariusze, incrementUsedCount 2 scenariusze) +Nowe testy: ShopCouponControllerTest (+3: constructorAcceptsRepository, hasMainActionMethods, constructorRequiresCouponRepository) +Nowe testy: OrderRepositoryTest (+10: findIdByHash 3 scenariusze, findHashById 2 scenariusze, orderDetailsFrontend 3 scenariusze, generateOrderNumber 2 scenariusze) +Nowe testy: ShopOrderControllerTest (+3: constructorAcceptsRepository, hasMainActionMethods, constructorRequiresOrderRepository) ``` Aktualizacja po migracji ShopCategory + ShopClient frontend (2026-02-17, ver. 0.289): diff --git a/docs/UPDATE_INSTRUCTIONS.md b/docs/UPDATE_INSTRUCTIONS.md index a61ddab..dfae480 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.289) +## Status biezacej aktualizacji (ver. 0.290) -- Wersja udostepniona: `0.289` (data: 2026-02-17). +- Wersja udostepniona: `0.290` (data: 2026-02-17). - Pliki publikacyjne: - - `updates/0.20/ver_0.289.zip`, `ver_0.289_files.txt` + - `updates/0.20/ver_0.290.zip`, `ver_0.290_files.txt` - Pliki metadanych aktualizacji: - - `updates/changelog.php` (dodany wpis `ver. 0.289`) - - `updates/versions.php` (`$current_ver = 289`) + - `updates/changelog.php` (dodany wpis `ver. 0.290`) + - `updates/versions.php` (`$current_ver = 290`) - Weryfikacja testow przed publikacja: - - `OK (537 tests, 1648 assertions)` + - `OK (565 tests, 1716 assertions)` ### 1. Określ numer wersji Sprawdź ostatnią wersję w `updates/` i zwiększ o 1. diff --git a/tests/Unit/Domain/Coupon/CouponRepositoryTest.php b/tests/Unit/Domain/Coupon/CouponRepositoryTest.php index b0fe639..cc5ac5d 100644 --- a/tests/Unit/Domain/Coupon/CouponRepositoryTest.php +++ b/tests/Unit/Domain/Coupon/CouponRepositoryTest.php @@ -265,4 +265,153 @@ class CouponRepositoryTest extends TestCase $this->assertCount(1, $tree[0]['subcategories']); $this->assertSame(11, (int)$tree[0]['subcategories'][0]['id']); } + + public function testFindByNameReturnsObjectWhenFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_coupon', '*', ['name' => 'RABAT10']) + ->willReturn([ + 'id' => '5', + 'name' => 'RABAT10', + 'status' => '1', + 'used' => '0', + 'type' => '1', + 'amount' => '10.00', + 'one_time' => '1', + 'include_discounted_product' => '0', + 'categories' => '[1,2]', + 'used_count' => '0', + ]); + + $repository = new CouponRepository($mockDb); + $result = $repository->findByName('RABAT10'); + + $this->assertIsObject($result); + $this->assertSame(5, $result->id); + $this->assertSame('RABAT10', $result->name); + $this->assertSame(1, $result->status); + $this->assertSame(0, $result->used); + $this->assertSame(1, $result->type); + $this->assertSame('10.00', $result->amount); + $this->assertSame(1, $result->one_time); + $this->assertSame(0, $result->include_discounted_product); + $this->assertSame('[1,2]', $result->categories); + $this->assertSame(0, $result->used_count); + } + + public function testFindByNameReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(null); + + $repository = new CouponRepository($mockDb); + $this->assertNull($repository->findByName('NIEISTNIEJACY')); + } + + public function testFindByNameReturnsNullForEmptyName(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new CouponRepository($mockDb); + $this->assertNull($repository->findByName('')); + $this->assertNull($repository->findByName(' ')); + } + + public function testIsAvailableReturnsTrueForActiveCoupon(): void + { + $mockDb = $this->createMock(\medoo::class); + $repository = new CouponRepository($mockDb); + + $coupon = (object)['id' => 1, 'status' => 1, 'used' => 0]; + $this->assertTrue($repository->isAvailable($coupon)); + } + + public function testIsAvailableReturnsFalseForUsedCoupon(): void + { + $mockDb = $this->createMock(\medoo::class); + $repository = new CouponRepository($mockDb); + + $coupon = (object)['id' => 1, 'status' => 1, 'used' => 1]; + $this->assertFalse($repository->isAvailable($coupon)); + } + + public function testIsAvailableReturnsFalseForInactiveCoupon(): void + { + $mockDb = $this->createMock(\medoo::class); + $repository = new CouponRepository($mockDb); + + $coupon = (object)['id' => 1, 'status' => 0, 'used' => 0]; + $this->assertFalse($repository->isAvailable($coupon)); + } + + public function testIsAvailableReturnsFalseForNullCoupon(): void + { + $mockDb = $this->createMock(\medoo::class); + $repository = new CouponRepository($mockDb); + + $this->assertFalse($repository->isAvailable(null)); + } + + public function testIsAvailableWorksWithArray(): void + { + $mockDb = $this->createMock(\medoo::class); + $repository = new CouponRepository($mockDb); + + $this->assertTrue($repository->isAvailable(['id' => 1, 'status' => 1, 'used' => 0])); + $this->assertFalse($repository->isAvailable(['id' => 0, 'status' => 1, 'used' => 0])); + } + + public function testMarkAsUsedCallsUpdate(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('update') + ->willReturnCallback(function ($table, $row, $where) { + $this->assertSame('pp_shop_coupon', $table); + $this->assertSame(1, $row['used']); + $this->assertArrayHasKey('date_used', $row); + $this->assertSame(['id' => 10], $where); + }); + + $repository = new CouponRepository($mockDb); + $repository->markAsUsed(10); + } + + public function testMarkAsUsedSkipsInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('update'); + + $repository = new CouponRepository($mockDb); + $repository->markAsUsed(0); + } + + public function testIncrementUsedCountCallsUpdate(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('update') + ->willReturnCallback(function ($table, $row, $where) { + $this->assertSame('pp_shop_coupon', $table); + $this->assertSame(1, $row['used_count[+]']); + $this->assertSame(['id' => 7], $where); + }); + + $repository = new CouponRepository($mockDb); + $repository->incrementUsedCount(7); + } + + public function testIncrementUsedCountSkipsInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('update'); + + $repository = new CouponRepository($mockDb); + $repository->incrementUsedCount(0); + } } diff --git a/tests/Unit/Domain/Order/OrderRepositoryTest.php b/tests/Unit/Domain/Order/OrderRepositoryTest.php index 31fc212..98cac9b 100644 --- a/tests/Unit/Domain/Order/OrderRepositoryTest.php +++ b/tests/Unit/Domain/Order/OrderRepositoryTest.php @@ -91,4 +91,141 @@ class OrderRepositoryTest extends TestCase $this->assertSame(2, $result['items'][0]['total_orders']); $this->assertSame(1, $result['items'][0]['paid']); } + + // --- Frontend method tests --- + + public function testFindIdByHashReturnsIdWhenFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->with('pp_shop_orders', 'id', ['hash' => 'abc123']) + ->willReturn('42'); + + $repository = new OrderRepository($mockDb); + $this->assertSame(42, $repository->findIdByHash('abc123')); + } + + public function testFindIdByHashReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get')->willReturn(null); + + $repository = new OrderRepository($mockDb); + $this->assertNull($repository->findIdByHash('nonexistent')); + } + + public function testFindIdByHashReturnsNullForEmptyHash(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new OrderRepository($mockDb); + $this->assertNull($repository->findIdByHash('')); + $this->assertNull($repository->findIdByHash(' ')); + } + + public function testFindHashByIdReturnsHashWhenFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->with('pp_shop_orders', 'hash', ['id' => 42]) + ->willReturn('abc123hash'); + + $repository = new OrderRepository($mockDb); + $this->assertSame('abc123hash', $repository->findHashById(42)); + } + + public function testFindHashByIdReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new OrderRepository($mockDb); + $this->assertNull($repository->findHashById(0)); + $this->assertNull($repository->findHashById(-1)); + } + + public function testOrderDetailsFrontendByIdReturnsArrayWithProducts(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->willReturn(['id' => 1, 'number' => '2026/02/001', 'coupon_id' => null]); + $mockDb->method('select') + ->willReturn([['product_id' => 10, 'name' => 'Test Product']]); + + $repository = new OrderRepository($mockDb); + $result = $repository->orderDetailsFrontend(1); + + $this->assertIsArray($result); + $this->assertSame(1, $result['id']); + $this->assertIsArray($result['products']); + $this->assertCount(1, $result['products']); + } + + public function testOrderDetailsFrontendByHashReturnsArrayWithProducts(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->willReturn(['id' => 5, 'number' => '2026/02/005', 'hash' => 'testhash']); + $mockDb->method('select') + ->willReturn([]); + + $repository = new OrderRepository($mockDb); + $result = $repository->orderDetailsFrontend(null, 'testhash'); + + $this->assertIsArray($result); + $this->assertSame(5, $result['id']); + $this->assertIsArray($result['products']); + } + + public function testOrderDetailsFrontendReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get')->willReturn(null); + + $repository = new OrderRepository($mockDb); + $this->assertNull($repository->orderDetailsFrontend(999)); + $this->assertNull($repository->orderDetailsFrontend(null, 'nonexistent')); + } + + public function testGenerateOrderNumberFormatsCorrectly(): void + { + $mockDb = $this->createMock(\medoo::class); + + $resultSet = new class { + public function fetchAll(): array + { + return [[5]]; + } + }; + + $mockDb->method('query')->willReturn($resultSet); + + $repository = new OrderRepository($mockDb); + $number = $repository->generateOrderNumber(); + + $expectedPrefix = date('Y/m') . '/'; + $this->assertStringStartsWith($expectedPrefix, $number); + $this->assertSame($expectedPrefix . '006', $number); + } + + public function testGenerateOrderNumberStartsAt001(): void + { + $mockDb = $this->createMock(\medoo::class); + + $resultSet = new class { + public function fetchAll(): array + { + return [[null]]; + } + }; + + $mockDb->method('query')->willReturn($resultSet); + + $repository = new OrderRepository($mockDb); + $number = $repository->generateOrderNumber(); + + $expectedPrefix = date('Y/m') . '/'; + $this->assertSame($expectedPrefix . '001', $number); + } } diff --git a/tests/Unit/front/Controllers/ShopCouponControllerTest.php b/tests/Unit/front/Controllers/ShopCouponControllerTest.php new file mode 100644 index 0000000..79cb8d9 --- /dev/null +++ b/tests/Unit/front/Controllers/ShopCouponControllerTest.php @@ -0,0 +1,40 @@ +repository = $this->createMock(CouponRepository::class); + $this->controller = new ShopCouponController($this->repository); + } + + public function testConstructorAcceptsRepository(): void + { + $controller = new ShopCouponController($this->repository); + $this->assertInstanceOf(ShopCouponController::class, $controller); + } + + public function testHasMainActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'useCoupon')); + $this->assertTrue(method_exists($this->controller, 'deleteCoupon')); + } + + public function testConstructorRequiresCouponRepository(): void + { + $reflection = new \ReflectionClass(ShopCouponController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('Domain\Coupon\CouponRepository', $params[0]->getType()->getName()); + } +} diff --git a/tests/Unit/front/Controllers/ShopOrderControllerTest.php b/tests/Unit/front/Controllers/ShopOrderControllerTest.php new file mode 100644 index 0000000..2c54ef6 --- /dev/null +++ b/tests/Unit/front/Controllers/ShopOrderControllerTest.php @@ -0,0 +1,43 @@ +repository = $this->createMock(OrderRepository::class); + $this->controller = new ShopOrderController($this->repository); + } + + public function testConstructorAcceptsRepository(): void + { + $controller = new ShopOrderController($this->repository); + $this->assertInstanceOf(ShopOrderController::class, $controller); + } + + public function testHasMainActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'paymentConfirmation')); + $this->assertTrue(method_exists($this->controller, 'paymentStatusTpay')); + $this->assertTrue(method_exists($this->controller, 'paymentStatusPrzelewy24pl')); + $this->assertTrue(method_exists($this->controller, 'paymentStatusHotpay')); + $this->assertTrue(method_exists($this->controller, 'orderDetails')); + } + + public function testConstructorRequiresOrderRepository(): void + { + $reflection = new \ReflectionClass(ShopOrderController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('Domain\Order\OrderRepository', $params[0]->getType()->getName()); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9b3d592..44915d8 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,6 +12,7 @@ if (file_exists(__DIR__ . '/../vendor/autoload.php')) { $prefixes = [ 'Domain\\' => __DIR__ . '/../autoload/Domain/', 'admin\\Controllers\\' => __DIR__ . '/../autoload/admin/Controllers/', + 'front\\Controllers\\' => __DIR__ . '/../autoload/front/Controllers/', 'admin\\Support\\Forms\\' => __DIR__ . '/../autoload/admin/Support/Forms/', 'admin\\ViewModels\\Forms\\' => __DIR__ . '/../autoload/admin/ViewModels/Forms/', 'admin\\Validation\\' => __DIR__ . '/../autoload/admin/Validation/', diff --git a/updates/0.20/ver_0.290.zip b/updates/0.20/ver_0.290.zip new file mode 100644 index 0000000..3577bf8 Binary files /dev/null and b/updates/0.20/ver_0.290.zip differ diff --git a/updates/0.20/ver_0.290_files.txt b/updates/0.20/ver_0.290_files.txt new file mode 100644 index 0000000..5c689ac --- /dev/null +++ b/updates/0.20/ver_0.290_files.txt @@ -0,0 +1,5 @@ +F: ../autoload/front/controls/class.ShopCoupon.php +F: ../autoload/front/factory/class.ShopCoupon.php +F: ../autoload/front/controls/class.ShopOrder.php +F: ../autoload/front/factory/class.ShopOrder.php +F: ../autoload/front/view/class.ShopOrder.php diff --git a/updates/changelog.php b/updates/changelog.php index 0f016ba..7152c47 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,9 @@ +ver. 0.290 - 17.02.2026
+- UPDATE - migracja front\factory\ShopCoupon + front\controls\ShopCoupon do Domain\Coupon\CouponRepository + front\Controllers\ShopCouponController +- UPDATE - migracja front\factory\ShopOrder + front\controls\ShopOrder + front\view\ShopOrder do Domain\Order\OrderRepository + front\Controllers\ShopOrderController +- FIX - kupony jednorazowe nigdy nie byly oznaczane jako uzyte (is_one_time/set_as_used w shop\Coupon) +- FIX - webhooks przelewy24/hotpay ujednolicone z tpay (poprawna obsluga Apilo sync) +
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 diff --git a/updates/versions.php b/updates/versions.php index 911755b..39c910d 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@