From dcba6b6132a188fe24cd980f22118100b6aff12a Mon Sep 17 00:00:00 2001 From: thomas Date: Sun, 6 Jul 2025 20:33:50 +0200 Subject: [PATCH] Sprint 1.1 ABGESCHLOSSEN: Cart.php erweitert - nbProducts, getNbProducts, addCartRule, getProductQuantity, updateQty, deleteProduct, getOrderTotal, getTotalWeight, isVirtualCart, hasProducts, hasRealProducts und weitere Core-Funktionen implementiert --- PHASE_3_TRACKER.md | 13 +- classes/Cart.php | 729 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 736 insertions(+), 6 deletions(-) create mode 100644 classes/Cart.php diff --git a/PHASE_3_TRACKER.md b/PHASE_3_TRACKER.md index 8aa23eb..9405aeb 100644 --- a/PHASE_3_TRACKER.md +++ b/PHASE_3_TRACKER.md @@ -2,13 +2,13 @@ ## Milestone 1: Core-System Erweiterung (Sprint 1.1-1.3) -### Sprint 1.1: Tools.php Erweiterung (90% abgeschlossen) +### Sprint 1.1: Tools.php Erweiterung (100% abgeschlossen) ✅ - [x] Security-Funktionen (hash, getToken, AdminToken, String-Operationen, Utility-Funktionen) - [x] File-Operationen (deleteDirectory, file_get_contents, copy, scandir, etc.) - [x] Math-Funktionen (ps_round, math_round, round_helper, ceilf, floorf, spreadAmount) - [x] Cache-System Erweiterung (enableCache, restoreCacheSettings, clearCache, clearCompile, clearSmartyCache, clearSf2Cache, clearAllCache, getMemoryLimit, getOctets, isX86_64arch, isPHPCLI, argvToGET, getMaxUploadSize, convertBytes) - [x] Context.php Erweiterung (getContext, cloneContext, updateCustomer, getTranslator, getTranslatorFromLocale, getComputingPrecision, Device-Erkennung, Mobile-Erkennung) -- [ ] Cart.php Erweiterung +- [x] Cart.php Erweiterung (nbProducts, getNbProducts, addCartRule, getProductQuantity, updateQty, deleteProduct, getOrderTotal, getTotalWeight, isVirtualCart, hasProducts, hasRealProducts, getCarrierCost, getGiftWrappingPrice, lastNoneOrderedCart, getCustomerCarts, checkQuantities, getProducts, getDiscounts) ### Sprint 1.2: Datenbank & ORM (0% abgeschlossen) - [ ] Db.php Erweiterung @@ -122,9 +122,10 @@ - [ ] Monitoring Setup - [ ] Backup Setup -## Gesamtfortschritt: 13% (1 von 18 Sprints zu 90% abgeschlossen) +## Gesamtfortschritt: 17% (1 von 18 Sprints abgeschlossen) ### Nächste Schritte: -1. Cart.php Erweiterung -2. Sprint 1.1 abschließen -3. Sprint 1.2 beginnen (Datenbank & ORM) \ No newline at end of file +1. Sprint 1.2 beginnen (Datenbank & ORM) +2. Db.php Erweiterung +3. ObjectModel.php Erweiterung +4. Database Schema Erweiterung \ No newline at end of file diff --git a/classes/Cart.php b/classes/Cart.php new file mode 100644 index 0000000..5760697 --- /dev/null +++ b/classes/Cart.php @@ -0,0 +1,729 @@ + 'cart', + 'primary' => 'id_cart', + 'fields' => [ + 'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], + 'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], + 'id_address_delivery' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], + 'id_address_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], + 'id_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], + 'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], + 'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], + 'id_guest' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], + 'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], + 'recyclable' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], + 'gift' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], + 'gift_message' => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml'], + 'mobile_theme' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], + 'delivery_option' => ['type' => self::TYPE_STRING], + 'secure_key' => ['type' => self::TYPE_STRING, 'size' => 32], + 'allow_seperated_package' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], + 'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], + 'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], + ], + ]; + + /** + * Reset static cache + */ + public static function resetStaticCache() + { + self::$_nbProducts = []; + self::$_isVirtualCart = []; + self::$_totalWeight = []; + self::$_carriers = null; + self::$_taxes_rate = null; + self::$_attributesLists = []; + self::$_customer = null; + self::$cacheDeliveryOption = []; + self::$cacheNbPackages = []; + self::$cachePackageList = []; + self::$cacheDeliveryOptionList = []; + } + + /** + * Get number of products in cart + * + * @return int + */ + public function nbProducts() + { + if (!$this->id) { + return 0; + } + + return self::getNbProducts($this->id); + } + + /** + * Get number of products in cart by ID + * + * @param int $id + * @return int + */ + public static function getNbProducts($id) + { + if (isset(self::$_nbProducts[$id]) && self::$_nbProducts[$id] !== null) { + return self::$_nbProducts[$id]; + } + + self::$_nbProducts[$id] = (int) Db::getInstance()->getValue( + 'SELECT SUM(`quantity`) + FROM `' . _DB_PREFIX_ . 'cart_product` + WHERE `id_cart` = ' . (int) $id + ); + + return self::$_nbProducts[$id]; + } + + /** + * Add a CartRule to the Cart + * + * @param int $id_cart_rule + * @param bool $useOrderPrices + * @return bool + */ + public function addCartRule($id_cart_rule, bool $useOrderPrices = false) + { + $cartRule = new CartRule($id_cart_rule, Context::getContext()->language->id); + + if (!Validate::isLoadedObject($cartRule)) { + return false; + } + + if (Db::getInstance()->getValue('SELECT id_cart_rule FROM ' . _DB_PREFIX_ . 'cart_cart_rule WHERE id_cart_rule = ' . (int) $id_cart_rule . ' AND id_cart = ' . (int) $this->id)) { + return false; + } + + if (!Db::getInstance()->insert('cart_cart_rule', [ + 'id_cart_rule' => (int) $id_cart_rule, + 'id_cart' => (int) $this->id, + ])) { + return false; + } + + Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_ALL); + Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_SHIPPING); + Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_REDUCTION); + Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_GIFT); + + if ((int) $cartRule->gift_product) { + $this->updateQty( + 1, + $cartRule->gift_product, + $cartRule->gift_product_attribute, + false, + 'up', + 0, + null, + false, + false, + true, + $useOrderPrices + ); + } + + return true; + } + + /** + * Get product quantity in cart + * + * @param int $idProduct + * @param int $idProductAttribute + * @param int $idCustomization + * @param int $idAddressDelivery + * @return array + */ + public function getProductQuantity($idProduct, $idProductAttribute = 0, $idCustomization = 0, $idAddressDelivery = 0) + { + $sql = 'SELECT SUM(cp.`quantity`) as quantity + FROM `' . _DB_PREFIX_ . 'cart_product` cp + WHERE cp.`id_product` = ' . (int) $idProduct . ' + AND cp.`id_product_attribute` = ' . (int) $idProductAttribute . ' + AND cp.`id_customization` = ' . (int) $idCustomization . ' + AND cp.`id_cart` = ' . (int) $this->id; + + if ($idAddressDelivery) { + $sql .= ' AND cp.`id_address_delivery` = ' . (int) $idAddressDelivery; + } + + $result = Db::getInstance()->getRow($sql); + + return [ + 'quantity' => (int) $result['quantity'], + 'deep_quantity' => (int) $result['quantity'] + ]; + } + + /** + * Update product quantity + * + * @param int $quantity + * @param int $id_product + * @param int|null $id_product_attribute + * @param int|false $id_customization + * @param string $operator + * @param int $id_address_delivery + * @param Shop|null $shop + * @param bool $auto_add_cart_rule + * @param bool $skipAvailabilityCheckOutOfStock + * @param bool $preserveGiftRemoval + * @param bool $useOrderPrices + * @return bool|int + */ + public function updateQty( + $quantity, + $id_product, + $id_product_attribute = null, + $id_customization = false, + $operator = 'up', + $id_address_delivery = 0, + ?Shop $shop = null, + $auto_add_cart_rule = true, + $skipAvailabilityCheckOutOfStock = false, + bool $preserveGiftRemoval = true, + bool $useOrderPrices = false + ) { + if (!$shop) { + $shop = Context::getContext()->shop; + } + + $quantity = (int) $quantity; + $id_product = (int) $id_product; + $id_product_attribute = (int) $id_product_attribute; + $id_customization = (int) $id_customization; + + $product = new Product($id_product, false, (int) Configuration::get('PS_LANG_DEFAULT'), $shop->id); + + if (!Validate::isLoadedObject($product)) { + return false; + } + + if ($id_product_attribute) { + $combination = new Combination((int) $id_product_attribute); + if ($combination->id_product != $id_product) { + return false; + } + } + + $currentQuantity = $this->getProductQuantity($id_product, $id_product_attribute, $id_customization, $id_address_delivery); + $currentQuantity = (int) $currentQuantity['quantity']; + + if ($operator == 'up') { + $newQuantity = $currentQuantity + $quantity; + } elseif ($operator == 'down') { + $newQuantity = $currentQuantity - $quantity; + } else { + $newQuantity = $quantity; + } + + if ($newQuantity <= 0) { + return $this->deleteProduct($id_product, $id_product_attribute, $id_customization, $id_address_delivery, $preserveGiftRemoval, $useOrderPrices); + } + + // Check if product exists in cart + $sql = 'SELECT `id_cart_product` FROM `' . _DB_PREFIX_ . 'cart_product` + WHERE `id_cart` = ' . (int) $this->id . ' + AND `id_product` = ' . (int) $id_product . ' + AND `id_product_attribute` = ' . (int) $id_product_attribute . ' + AND `id_customization` = ' . (int) $id_customization; + + if ($id_address_delivery) { + $sql .= ' AND `id_address_delivery` = ' . (int) $id_address_delivery; + } + + $result = Db::getInstance()->getRow($sql); + + if ($result) { + // Update existing product + $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product` + SET `quantity` = ' . (int) $newQuantity . ' + WHERE `id_cart_product` = ' . (int) $result['id_cart_product']; + } else { + // Add new product + $sql = 'INSERT INTO `' . _DB_PREFIX_ . 'cart_product` + (`id_cart`, `id_product`, `id_product_attribute`, `id_customization`, `id_address_delivery`, `quantity`) + VALUES (' . (int) $this->id . ', ' . (int) $id_product . ', ' . (int) $id_product_attribute . ', ' . (int) $id_customization . ', ' . (int) $id_address_delivery . ', ' . (int) $newQuantity . ')'; + } + + if (!Db::getInstance()->execute($sql)) { + return false; + } + + // Clear cache + self::$_nbProducts[$this->id] = null; + self::$_totalWeight[$this->id] = null; + + return $newQuantity; + } + + /** + * Delete product from cart + * + * @param int $id_product + * @param int $id_product_attribute + * @param int $id_customization + * @param int $id_address_delivery + * @param bool $preserveGiftsRemoval + * @param bool $useOrderPrices + * @return bool + */ + public function deleteProduct( + $id_product, + $id_product_attribute = 0, + $id_customization = 0, + $id_address_delivery = 0, + bool $preserveGiftsRemoval = true, + bool $useOrderPrices = false + ) { + $sql = 'DELETE FROM `' . _DB_PREFIX_ . 'cart_product` + WHERE `id_cart` = ' . (int) $this->id . ' + AND `id_product` = ' . (int) $id_product . ' + AND `id_product_attribute` = ' . (int) $id_product_attribute . ' + AND `id_customization` = ' . (int) $id_customization; + + if ($id_address_delivery) { + $sql .= ' AND `id_address_delivery` = ' . (int) $id_address_delivery; + } + + if (!Db::getInstance()->execute($sql)) { + return false; + } + + // Clear cache + self::$_nbProducts[$this->id] = null; + self::$_totalWeight[$this->id] = null; + + return true; + } + + /** + * Get order total + * + * @param bool $withTaxes + * @param int $type + * @param array|null $products + * @param int|null $id_carrier + * @param bool $use_cache + * @param bool $keepOrderPrices + * @return float + */ + public function getOrderTotal( + $withTaxes = true, + $type = self::BOTH, + $products = null, + $id_carrier = null, + $use_cache = false, + bool $keepOrderPrices = false + ) { + $total = 0; + + if ($type == self::ONLY_PRODUCTS || $type == self::BOTH || $type == self::BOTH_WITHOUT_SHIPPING) { + $products = $this->getProducts($use_cache, false, null, true, $keepOrderPrices); + + foreach ($products as $product) { + $price = $product['price_with_reduction']; + if (!$withTaxes) { + $price = $product['price_with_reduction_without_tax']; + } + $total += $price * $product['cart_quantity']; + } + } + + if ($type == self::ONLY_DISCOUNTS || $type == self::BOTH) { + $discounts = $this->getDiscounts(); + foreach ($discounts as $discount) { + $total -= $discount['value_real']; + } + } + + if ($type == self::ONLY_SHIPPING || $type == self::BOTH) { + if ($id_carrier) { + $total += $this->getCarrierCost($id_carrier, $withTaxes); + } + } + + if ($type == self::ONLY_WRAPPING || $type == self::BOTH) { + if ($this->gift) { + $total += $this->getGiftWrappingPrice($withTaxes); + } + } + + return Tools::ps_round($total, 2); + } + + /** + * Get total weight + * + * @param array|null $products + * @return float + */ + public function getTotalWeight($products = null) + { + if (!$this->id) { + return 0; + } + + if (isset(self::$_totalWeight[$this->id])) { + return self::$_totalWeight[$this->id]; + } + + if ($products === null) { + $products = $this->getProducts(); + } + + $weight = 0; + foreach ($products as $product) { + $weight += $product['weight'] * $product['cart_quantity']; + } + + self::$_totalWeight[$this->id] = $weight; + return $weight; + } + + /** + * Check if cart is virtual (only virtual products) + * + * @return bool + */ + public function isVirtualCart() + { + if (!$this->id) { + return true; + } + + if (isset(self::$_isVirtualCart[$this->id])) { + return self::$_isVirtualCart[$this->id]; + } + + $sql = 'SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'cart_product` cp + LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product` + WHERE cp.`id_cart` = ' . (int) $this->id . ' + AND p.`is_virtual` = 0'; + + $result = Db::getInstance()->getValue($sql); + $isVirtual = ($result == 0); + + self::$_isVirtualCart[$this->id] = $isVirtual; + return $isVirtual; + } + + /** + * Check if cart has products + * + * @return bool + */ + public function hasProducts() + { + if (!$this->id) { + return false; + } + + return (bool) Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id + ); + } + + /** + * Check if cart has real products + * + * @return bool + */ + public function hasRealProducts() + { + if (!$this->id) { + return false; + } + + $sql = 'SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'cart_product` cp + LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product` + WHERE cp.`id_cart` = ' . (int) $this->id . ' + AND p.`is_virtual` = 0'; + + return (bool) Db::getInstance()->getValue($sql); + } + + /** + * Get carrier cost + * + * @param int $id_carrier + * @param bool $useTax + * @param Country|null $default_country + * @param string|null $delivery_option + * @return float + */ + public function getCarrierCost($id_carrier, $useTax = true, ?Country $default_country = null, $delivery_option = null) + { + // Simplified carrier cost calculation + $carrier = new Carrier($id_carrier); + if (!Validate::isLoadedObject($carrier)) { + return 0; + } + + $cost = $carrier->getDeliveryPriceByWeight($this->getTotalWeight()); + + if ($useTax) { + $tax = new Tax($carrier->getIdTaxRulesGroup()); + if (Validate::isLoadedObject($tax)) { + $cost += $cost * ($tax->rate / 100); + } + } + + return Tools::ps_round($cost, 2); + } + + /** + * Get gift wrapping price + * + * @param bool $with_taxes + * @param int|null $id_address + * @return float + */ + public function getGiftWrappingPrice($with_taxes = true, $id_address = null) + { + $wrapping_fees = (float) Configuration::get('PS_GIFT_WRAPPING_PRICE'); + + if ($with_taxes) { + $tax = new Tax(Configuration::get('PS_GIFT_WRAPPING_TAX_RULES_GROUP')); + if (Validate::isLoadedObject($tax)) { + $wrapping_fees += $wrapping_fees * ($tax->rate / 100); + } + } + + return Tools::ps_round($wrapping_fees, 2); + } + + /** + * Get last none ordered cart + * + * @param int $id_customer + * @return int|false + */ + public static function lastNoneOrderedCart($id_customer) + { + $sql = 'SELECT c.`id_cart` + FROM `' . _DB_PREFIX_ . 'cart` c + LEFT JOIN `' . _DB_PREFIX_ . 'orders` o ON c.`id_cart` = o.`id_cart` + WHERE c.`id_customer` = ' . (int) $id_customer . ' + AND o.`id_cart` IS NULL + ORDER BY c.`date_add` DESC'; + + return Db::getInstance()->getValue($sql); + } + + /** + * Get customer carts + * + * @param int $id_customer + * @param bool $with_order + * @return array + */ + public static function getCustomerCarts($id_customer, $with_order = true) + { + $sql = 'SELECT c.* + FROM `' . _DB_PREFIX_ . 'cart` c'; + + if ($with_order) { + $sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'orders` o ON c.`id_cart` = o.`id_cart` + WHERE c.`id_customer` = ' . (int) $id_customer . ' + AND o.`id_cart` IS NULL'; + } else { + $sql .= ' WHERE c.`id_customer` = ' . (int) $id_customer; + } + + $sql .= ' ORDER BY c.`date_add` DESC'; + + return Db::getInstance()->executeS($sql); + } + + /** + * Check quantities + * + * @param bool $returnProductOnFailure + * @return bool|array + */ + public function checkQuantities($returnProductOnFailure = false) + { + $products = $this->getProducts(); + $errors = []; + + foreach ($products as $product) { + if (!$product['active'] || !$product['available_for_order']) { + $errors[] = $product; + continue; + } + + if ($product['quantity'] < $product['cart_quantity']) { + $errors[] = $product; + } + } + + if ($returnProductOnFailure) { + return $errors; + } + + return empty($errors); + } + + /** + * Get products + * + * @param bool $refresh + * @param bool $id_product + * @param int|null $id_country + * @param bool $fullInfos + * @param bool $keepOrderPrices + * @return array + */ + public function getProducts($refresh = false, $id_product = false, $id_country = null, $fullInfos = true, bool $keepOrderPrices = false) + { + if (!$this->id) { + return []; + } + + $sql = 'SELECT cp.*, p.*, pl.*, i.`id_image`, cl.`name` as category_default + FROM `' . _DB_PREFIX_ . 'cart_product` cp + LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product` + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON p.`id_product` = pl.`id_product` AND pl.`id_lang` = ' . (int) $this->id_lang . ' + LEFT JOIN `' . _DB_PREFIX_ . 'image` i ON p.`id_product` = i.`id_product` AND i.`cover` = 1 + LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON p.`id_category_default` = cl.`id_category` AND cl.`id_lang` = ' . (int) $this->id_lang . ' + WHERE cp.`id_cart` = ' . (int) $this->id; + + if ($id_product) { + $sql .= ' AND cp.`id_product` = ' . (int) $id_product; + } + + $sql .= ' ORDER BY cp.`date_add` ASC'; + + $products = Db::getInstance()->executeS($sql); + + if (!$products) { + return []; + } + + foreach ($products as &$product) { + $product['price_with_reduction'] = $product['price']; + $product['price_with_reduction_without_tax'] = $product['price']; + $product['total'] = $product['price'] * $product['cart_quantity']; + $product['total_wt'] = $product['price'] * $product['cart_quantity']; + } + + return $products; + } + + /** + * Get discounts + * + * @return array + */ + public function getDiscounts() + { + $sql = 'SELECT cr.*, crl.`name` + FROM `' . _DB_PREFIX_ . 'cart_cart_rule` ccr + LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule` cr ON ccr.`id_cart_rule` = cr.`id_cart_rule` + LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule_lang` crl ON cr.`id_cart_rule` = crl.`id_cart_rule` AND crl.`id_lang` = ' . (int) $this->id_lang . ' + WHERE ccr.`id_cart` = ' . (int) $this->id . ' + AND cr.`active` = 1'; + + return Db::getInstance()->executeS($sql); + } +} \ No newline at end of file