diff --git a/api2/ProductCollection.php b/api2/ProductCollection.php new file mode 100644 index 0000000..0e8b41d --- /dev/null +++ b/api2/ProductCollection.php @@ -0,0 +1,2097 @@ +arrData['uniqid'] = $this->generateUniqueId(); + + // Do not use __destruct, because Database object might be destructed first + // see http://github.com/contao/core/issues/2236 + if ('FE' === TL_MODE) { + register_shutdown_function(array($this, 'updateDatabase'), false); + } + } + + /** + * Prevent cloning because we can't copy items etc. + * + * @throws \LogicException because ProductCollection cannot be cloned + */ + /** @noinspection MagicMethodsValidityInspection */ + public function __clone() + { + throw new \LogicException( + 'Product collections can\'t be cloned, you should probably use ProductCollection::createFromCollection' + ); + } + + /** + * Shutdown function to update prices of items and collection + * + * @param boolean $blnCreate If true create Model even if not in registry or not saved at all + */ + public function updateDatabase($blnCreate = true) + { + if (!$this->blnPreventSaving + && !$this->isLocked() + && (Registry::getInstance()->isRegistered($this) || $blnCreate) + ) { + foreach ($this->getItems() as $objItem) { + if (!$objItem->hasProduct()) { + continue; + } + + $objItem->price = $objItem->getPrice(); + $objItem->tax_free_price = $objItem->getTaxFreePrice(); + $objItem->save(); + } + + // First call to __set for tstamp will truncate the cache + $this->tstamp = time(); + $this->subtotal = $this->getSubtotal(); + $this->tax_free_subtotal = $this->getTaxFreeSubtotal(); + $this->total = $this->getTotal(); + $this->tax_free_total = $this->getTaxFreeTotal(); + $this->currency = (string) $this->getConfig()->currency; + + $this->save(); + } + } + + /** + * Mark a field as modified + * + * @param string $strKey The field key + */ + public function markModified($strKey) + { + if ('locked' === $strKey) { + throw new \InvalidArgumentException('Cannot change lock status of collection'); + } + + if ('document_number' === $strKey) { + throw new \InvalidArgumentException( + 'Cannot change document number of a collection, must be generated using generateDocumentNumber()' + ); + } + + $this->clearCache(); + + parent::markModified($strKey); + } + + /** + * @inheritdoc + */ + public function getId() + { + return (int) $this->id; + } + + /** + * @inheritdoc + */ + public function getUniqueId() + { + return $this->uniqid; + } + + /** + * @inheritdoc + */ + public function getMember() + { + if (0 === (int) $this->member) { + return null; + } + + return MemberModel::findByPk($this->member); + } + + /** + * @inheritdoc + */ + public function getStoreId() + { + return (int) $this->store_id; + } + + /** + * @inheritdoc + */ + public function getConfig() + { + try { + return $this->getRelated('config_id'); + } catch (\Exception $e) { + return null; + } + } + + /** + * @inheritdoc + */ + public function isLocked() + { + return null !== $this->locked; + } + + /** + * @inheritdoc + */ + public function getLockTime() + { + return $this->locked; + } + + + /** + * @inheritdoc + */ + public function isEmpty() + { + return 0 === \count($this->getItems()); + } + + /** + * Return payment method for this collection + * + * @return IsotopePayment|null + */ + public function getPaymentMethod() + { + if (false === $this->objPayment) { + try { + $this->objPayment = $this->getRelated('payment_id'); + } catch (\Exception $e) { + $this->objPayment = null; + } + } + + return $this->objPayment; + } + + /** + * Set payment method for this collection + * + * @param IsotopePayment $objPayment + */ + public function setPaymentMethod(IsotopePayment $objPayment = null) + { + $this->payment_id = (null === $objPayment ? 0 : $objPayment->getId()); + $this->objPayment = $objPayment; + } + + /** + * Return surcharge for current payment method + * + * @return ProductCollectionSurcharge|null + */ + public function getPaymentSurcharge() + { + return $this->hasPayment() ? $this->getPaymentMethod()->getSurcharge($this) : null; + } + + /** + * Return boolean whether collection has payment + * + * @return bool + */ + public function hasPayment() + { + return null !== $this->getPaymentMethod(); + } + + /** + * Return boolean whether collection requires payment + * + * @return bool + */ + public function requiresPayment() + { + return $this->getTotal() > 0; + } + + /** + * Return shipping method for this collection + * + * @return IsotopeShipping|null + */ + public function getShippingMethod() + { + if (false === $this->objShipping) { + try { + $this->objShipping = $this->getRelated('shipping_id'); + } catch (\Exception $e) { + $this->objShipping = null; + } + } + + return $this->objShipping; + } + + /** + * Set shipping method for this collection + * + * @param IsotopeShipping $objShipping + */ + public function setShippingMethod(IsotopeShipping $objShipping = null) + { + $this->shipping_id = (null === $objShipping ? 0 : $objShipping->getId()); + $this->objShipping = $objShipping; + } + + /** + * Return surcharge for current shipping method + * + * @return ProductCollectionSurcharge|null + */ + public function getShippingSurcharge() + { + return $this->hasShipping() ? $this->getShippingMethod()->getSurcharge($this) : null; + } + + /** + * Return boolean whether collection has shipping + * + * @return bool + */ + public function hasShipping() + { + return null !== $this->getShippingMethod(); + } + + /** + * Return boolean whether collection requires shipping + * + * @return bool + */ + public function requiresShipping() + { + if (!isset($this->arrCache['requiresShipping'])) { + $this->arrCache['requiresShipping'] = false; + $arrItems = $this->getItems(); + + foreach ($arrItems as $objItem) { + if ($objItem->hasProduct() && !$objItem->getProduct()->isExemptFromShipping()) { + $this->arrCache['requiresShipping'] = true; + break; + } + } + } + + return $this->arrCache['requiresShipping']; + } + + /** + * Get billing address for collection + * + * @return \Isotope\Model\Address|null + */ + public function getBillingAddress() + { + if (!$this->billing_address_id) { + return null; + } + + return $this->getRelated('billing_address_id'); + } + + /** + * Set billing address for collection + * + * @param Address $objAddress + */ + public function setBillingAddress(Address $objAddress = null) + { + if (null === $objAddress || $objAddress->id < 1) { + $this->billing_address_id = 0; + } else { + $this->billing_address_id = $objAddress->id; + } + } + + /** + * Return boolean whether collection requires a shipping address + * + * @return bool + */ + public function requiresShippingAddress() + { + if (!$this->requiresShipping()) { + return false; + } + + if (!isset($this->arrCache['requiresShippingAddress'])) { + $this->arrCache['requiresShippingAddress'] = true; + $arrItems = $this->getItems(); + + foreach ($arrItems as $objItem) { + $product = $objItem->getProduct(); + if ($product instanceof IsotopeProduct && \method_exists($product, 'isPickupOnly') && $product->isPickupOnly()) { + $this->arrCache['requiresShippingAddress'] = false; + break; + } + } + } + + return $this->arrCache['requiresShippingAddress']; + } + + /** + * Get shipping address for collection + * + * @return Address|null + */ + public function getShippingAddress() + { + if (!$this->shipping_address_id || !$this->requiresShippingAddress()) { + return null; + } + + return $this->getRelated('shipping_address_id'); + } + + /** + * Set shipping address for collection + * + * @param Address $objAddress + */ + public function setShippingAddress(Address $objAddress = null) + { + if (null === $objAddress || $objAddress->id < 1) { + $this->shipping_address_id = 0; + } else { + $this->shipping_address_id = $objAddress->id; + } + } + + /** + * Returns the generated document number or empty string if not available. + * + * @return string + */ + public function getDocumentNumber() + { + return (string) $this->arrData['document_number']; + } + + /** + * Return customer email address for the collection + * + * @return string + */ + public function getEmailRecipient() + { + $strName = ''; + $strEmail = ''; + $objBillingAddress = $this->getBillingAddress(); + $objShippingAddress = $this->getShippingAddress(); + + if ($objBillingAddress->email != '') { + $strName = $objBillingAddress->firstname . ' ' . $objBillingAddress->lastname; + $strEmail = $objBillingAddress->email; + } elseif ($objShippingAddress->email != '') { + $strName = $objShippingAddress->firstname . ' ' . $objShippingAddress->lastname; + $strEmail = $objShippingAddress->email; + } elseif ($this->member > 0 + && ($objMember = MemberModel::findByPk($this->member)) !== null + && $objMember->email != '' + ) { + $strName = $objMember->firstname . ' ' . $objMember->lastname; + $strEmail = $objMember->email; + } + + if (trim($strName) != '') { + // Romanize friendly name to prevent email issues + $strName = html_entity_decode($strName, ENT_QUOTES, $GLOBALS['TL_CONFIG']['characterSet']); + $strName = StringUtil::stripInsertTags($strName); + $strName = utf8_romanize($strName); + $strName = preg_replace('/[^A-Za-z0-9.!#$%&\'*+-\/=?^_ `{|}~]+/i', '_', $strName); + + $strEmail = sprintf('"%s" <%s>', $strName, $strEmail); + } + + // !HOOK: determine email recipient for collection + if (isset($GLOBALS['ISO_HOOKS']['emailRecipientForCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['emailRecipientForCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['emailRecipientForCollection'] as $callback) { + $strEmail = System::importStatic($callback[0])->{$callback[1]}($strEmail, $this); + } + } + + return $strEmail; + } + + /** + * Return number of items in the collection + * + * @return int + */ + public function countItems() + { + if (!isset($this->arrCache['countItems'])) { + $this->arrCache['countItems'] = ProductCollectionItem::countBy('pid', (int) $this->id); + } + + return $this->arrCache['countItems']; + } + + /** + * Return summary of item quantity in collection + * + * @return int + */ + public function sumItemsQuantity() + { + if (!isset($this->arrCache['sumItemsQuantity'])) { + $this->arrCache['sumItemsQuantity'] = ProductCollectionItem::sumBy('quantity', 'pid', (int) $this->id); + } + + return $this->arrCache['sumItemsQuantity']; + } + + /** + * Load settings from database field + * + * @param array $arrData + * + * @return $this + */ + public function setRow(array $arrData) + { + parent::setRow($arrData); + + // Merge settings into arrData, save() will move the values back + $this->arrData = array_merge(StringUtil::deserialize($arrData['settings'] ?? [], true), $this->arrData); + + return $this; + } + + /** + * Save all non-database fields in the settings array + * + * @return $this + */ + public function save() + { + // The instance cannot be saved + if ($this->blnPreventSaving) { + throw new \LogicException('The model instance has been detached and cannot be saved'); + } + + // !HOOK: additional functionality when saving a collection + if (isset($GLOBALS['ISO_HOOKS']['saveCollection']) && \is_array($GLOBALS['ISO_HOOKS']['saveCollection'])) { + foreach ($GLOBALS['ISO_HOOKS']['saveCollection'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($this); + } + } + + $arrDbFields = Database::getInstance()->getFieldNames(static::$strTable); + $arrModified = array_diff_key($this->arrModified, array_flip($arrDbFields)); + + if (!empty($arrModified)) { + $arrSettings = StringUtil::deserialize($this->settings, true); + $arrSettings = array_merge($arrSettings, array_intersect_key($this->arrData, $arrModified)); + + $this->settings = serialize($arrSettings); + } + + return parent::save(); + } + + /** + * Also delete child table records when dropping this collection + * + * @param bool $blnForce Force to delete the collection even if it's locked + * + * @return int Number of rows affected + * + * @throws \BadMethodCallException if the product collection is locked. + */ + public function delete($blnForce = false) + { + if (!$blnForce) { + $this->ensureNotLocked(); + + // !HOOK: additional functionality when deleting a collection + if (isset($GLOBALS['ISO_HOOKS']['deleteCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['deleteCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['deleteCollection'] as $callback) { + $blnRemove = System::importStatic($callback[0])->{$callback[1]}($this); + + if ($blnRemove === false) { + return 0; + } + } + } + } + + $intPid = $this->id; + $intAffectedRows = parent::delete(); + + if ($intAffectedRows > 0 && $intPid > 0) { + Database::getInstance()->query(" + DELETE FROM tl_iso_product_collection_download + WHERE pid IN (SELECT id FROM tl_iso_product_collection_item WHERE pid=$intPid) + "); + Database::getInstance()->query( + "DELETE FROM tl_iso_product_collection_item WHERE pid=$intPid" + ); + Database::getInstance()->query( + "DELETE FROM tl_iso_product_collection_surcharge WHERE pid=$intPid" + ); + Database::getInstance()->query( + "DELETE FROM tl_iso_address WHERE ptable='" . static::$strTable . "' AND pid=$intPid" + ); + } + + $this->arrCache = array(); + $this->arrItems = null; + $this->arrSurcharges = null; + + // !HOOK: additional functionality when deleting a collection + if (isset($GLOBALS['ISO_HOOKS']['postDeleteCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['postDeleteCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['postDeleteCollection'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($this, $intPid); + } + } + + return $intAffectedRows; + } + + /** + * Delete all products in the collection + * + * @throws \BadMethodCallException if the product collection is locked. + */ + public function purge() + { + $this->ensureNotLocked(); + + foreach ($this->getItems() as $objItem) { + $this->deleteItem($objItem); + } + + foreach ($this->getSurcharges() as $objSurcharge) { + if ($objSurcharge->id) { + $objSurcharge->delete(); + } + } + + $this->clearCache(); + } + + /** + * Lock collection from begin modified + * + * @throws \BadMethodCallException if the product collection is locked. + */ + public function lock() + { + $this->ensureNotLocked(); + + global $objPage; + $time = time(); + + $this->pageId = (int) $objPage->id; + $this->language = (string) $GLOBALS['TL_LANGUAGE']; + + $this->createPrivateAddresses(); + $this->updateDatabase(); + + // Add surcharges to the collection + $sorting = 128; + foreach ($this->getSurcharges() as $objSurcharge) { + $objSurcharge->pid = $this->id; + $objSurcharge->tstamp = $time; + $objSurcharge->sorting = $sorting; + $objSurcharge->save(); + + $sorting += 128; + } + + // Add downloads from products to the collection + foreach (ProductCollectionDownload::createForProductsInCollection($this) as $objDownload) { + $objDownload->save(); + } + + // Can't use model, it would not save as soon as it's locked + Database::getInstance()->query( + "UPDATE tl_iso_product_collection SET locked=$time WHERE id=" . $this->id + ); + $this->arrData['locked'] = $time; + + // !HOOK: pre-process checkout + if (isset($GLOBALS['ISO_HOOKS']['collectionLocked']) && \is_array($GLOBALS['ISO_HOOKS']['collectionLocked'])) { + foreach ($GLOBALS['ISO_HOOKS']['collectionLocked'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($this); + } + } + + $this->clearCache(); + } + + /** + * Sum total price of all items in the collection + * + * @return float + */ + public function getSubtotal() + { + if ($this->isLocked()) { + return $this->subtotal; + } + + if (!isset($this->arrCache['subtotal'])) { + $fltAmount = 0; + $arrItems = $this->getItems(); + + foreach ($arrItems as $objItem) { + $varPrice = $objItem->getTotalPrice(); + + if ($varPrice !== null) { + $fltAmount += $varPrice; + } + } + + $this->arrCache['subtotal'] = $fltAmount; + } + + return $this->arrCache['subtotal']; + } + + /** + * Sum total tax free price of all items in the collection + * + * @return float + */ + public function getTaxFreeSubtotal() + { + if ($this->isLocked()) { + return $this->tax_free_subtotal; + } + + if (!isset($this->arrCache['taxFreeSubtotal'])) { + $fltAmount = 0; + $arrItems = $this->getItems(); + + foreach ($arrItems as $objItem) { + $varPrice = $objItem->getTaxFreeTotalPrice(); + + if ($varPrice !== null) { + $fltAmount += $varPrice; + } + } + + $this->arrCache['taxFreeSubtotal'] = $fltAmount; + } + + return $this->arrCache['taxFreeSubtotal']; + } + + /** + * Sum total price of items and surcharges + * + * @return float + */ + public function getTotal() + { + if ($this->isLocked()) { + return $this->total; + } + + if (!isset($this->arrCache['total'])) { + $fltAmount = $this->getSubtotal(); + $arrSurcharges = $this->getSurcharges(); + + foreach ($arrSurcharges as $objSurcharge) { + if ($objSurcharge->addToTotal) { + $fltAmount += $objSurcharge->total_price; + } + } + + $this->arrCache['total'] = $fltAmount > 0 ? $fltAmount : 0; + } + + return $this->arrCache['total']; + } + + /** + * Sum tax free total of items and surcharges + * + * @return float + */ + public function getTaxFreeTotal() + { + if ($this->isLocked()) { + return $this->tax_free_total; + } + + if (!isset($this->arrCache['taxFreeTotal'])) { + $arrSurcharges = $this->getSurcharges(); + + if (Config::PRICE_DISPLAY_GROSS === $this->getConfig()->priceDisplay) { + $fltAmount = $this->getTotal(); + + foreach ($arrSurcharges as $objSurcharge) { + if ($objSurcharge instanceof Tax) { + $fltAmount -= $objSurcharge->total_price; + } + } + } else { + $fltAmount = $this->getTaxFreeSubtotal(); + + foreach ($arrSurcharges as $objSurcharge) { + if ($objSurcharge->addToTotal) { + $fltAmount += $objSurcharge->tax_free_total_price; + } + } + } + + $this->arrCache['taxFreeTotal'] = $fltAmount > 0 ? Isotope::roundPrice($fltAmount) : 0; + } + + return $this->arrCache['taxFreeTotal']; + } + + /** + * @inheritdoc + */ + public function getCurrency() + { + return $this->currency; + } + + /** + * Return the item with the latest timestamp (e.g. the latest added item) + * + * @return ProductCollectionItem|null + */ + public function getLatestItem() + { + if (!isset($this->arrCache['latestItem'])) { + $latest = 0; + $arrItems = $this->getItems(); + + foreach ($arrItems as $objItem) { + if ($objItem->tstamp > $latest) { + $this->arrCache['latestItem'] = $objItem; + $latest = $objItem->tstamp; + } + } + } + + return $this->arrCache['latestItem']; + } + + /** + * Return timestamp when this collection was created + * This is relevant for price calculation + * + * @return int + */ + public function getLastModification() + { + if ($this->isLocked()) { + return $this->locked; + } + + return $this->tstamp ? : time(); + } + + /** + * Return all items in the collection + * + * @param callable $varCallable + * @param bool $blnNoCache + * + * @return ProductCollectionItem[] + */ + public function getItems($varCallable = null, $blnNoCache = false) + { + if (null === $this->arrItems || $blnNoCache) { + $this->arrItems = array(); + + if (($objItems = ProductCollectionItem::findBy('pid', $this->id)) !== null) { + /** @var ProductCollectionItem $objItem */ + foreach ($objItems as $objItem) { + if ($this->isLocked()) { + $objItem->lock(); + } + + // Add error message for items no longer available + if (!$objItem->isAvailable() && !$objItem->hasErrors()) { + $objItem->addError($GLOBALS['TL_LANG']['ERR']['collectionItemNotAvailable']); + } + + $this->arrItems[$objItem->id] = $objItem; + } + } + } + + if ($varCallable === null) { + return $this->arrItems; + } + + // not allowed to chance items + $arrItems = $this->arrItems; + + return \call_user_func($varCallable, $arrItems); + } + + /** + * Search item for a specific product + * + * @param IsotopeProduct $objProduct + * + * @return ProductCollectionItem|null + */ + public function getItemForProduct(IsotopeProduct $objProduct) + { + $strClass = array_search(\get_class($objProduct), Product::getModelTypes(), true); + + $objItem = ProductCollectionItem::findOneBy( + array('pid=?', 'type=?', 'product_id=?', 'configuration=?'), + array($this->id, $strClass, $objProduct->getId(), serialize($objProduct->getOptions())) + ); + + return $objItem; + } + + /** + * Gets the product collection with given ID if it belongs to this collection. + * + * @param int $id + * + * @return ProductCollectionItem|null + */ + public function getItemById($id) + { + $items = $this->getItems(); + + if (!isset($items[$id])) { + return null; + } + + return $items[$id]; + } + + /** + * Check if a given product is already in the collection + * + * @param IsotopeProduct $objProduct + * @param bool $blnIdentical + * + * @return bool + */ + public function hasProduct(IsotopeProduct $objProduct, $blnIdentical = true) + { + if (true === $blnIdentical) { + return null !== $this->getItemForProduct($objProduct); + } + + $intId = $objProduct->getProductId(); + + foreach ($this->getItems() as $objItem) { + if ($objItem->hasProduct() + && ($objItem->getProduct()->getId() == $intId || $objItem->getProduct()->getProductId() == $intId) + ) { + return true; + } + } + + return false; + } + + /** + * Add a product to the collection + * + * @param IsotopeProduct $objProduct + * @param int $intQuantity + * @param array $arrConfig + * + * @return ProductCollectionItem|false + */ + public function addProduct(IsotopeProduct $objProduct, $intQuantity, array $arrConfig = array()) + { + // !HOOK: additional functionality when adding product to collection + if (isset($GLOBALS['ISO_HOOKS']['addProductToCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['addProductToCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['addProductToCollection'] as $callback) { + $intQuantity = System::importStatic($callback[0])->{$callback[1]}($objProduct, $intQuantity, $this, $arrConfig); + } + } + + if ($intQuantity == 0) { + return false; + } + + $time = time(); + $this->tstamp = $time; + + // Make sure collection is in DB before adding product + if (!Registry::getInstance()->isRegistered($this)) { + $this->save(); + } + + // Remove uploaded files from session so they are not added to the next product (see #646) + unset($_SESSION['FILES']); + + $objItem = $this->getItemForProduct($objProduct); + $intMinimumQuantity = $objProduct->getMinimumQuantity(); + + if (null !== $objItem) { + if (($objItem->quantity + $intQuantity) < $intMinimumQuantity) { + Message::addInfo(sprintf( + $GLOBALS['TL_LANG']['ERR']['productMinimumQuantity'], + $objProduct->getName(), + $intMinimumQuantity + )); + $intQuantity = $intMinimumQuantity - $objItem->quantity; + } + + $objItem->increaseQuantityBy($intQuantity); + } else { + if ($intQuantity < $intMinimumQuantity) { + Message::addInfo(sprintf( + $GLOBALS['TL_LANG']['ERR']['productMinimumQuantity'], + $objProduct->getName(), + $intMinimumQuantity + )); + $intQuantity = $intMinimumQuantity; + } + + $objItem = new ProductCollectionItem(); + $objItem->pid = $this->id; + $objItem->jumpTo = isset($arrConfig['jumpTo']) ? (int) $arrConfig['jumpTo']->id : 0; + + $this->setProductForItem($objProduct, $objItem, $intQuantity); + $objItem->save(); + + // Add the new item to our cache + $this->arrItems[$objItem->id] = $objItem; + } + + // !HOOK: additional functionality when adding product to collection + if (isset($GLOBALS['ISO_HOOKS']['postAddProductToCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['postAddProductToCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['postAddProductToCollection'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($objItem, $intQuantity, $this, $arrConfig); + } + } + + return $objItem; + } + + /** + * Update product details for a collection item. + * + * @param IsotopeProduct $objProduct + * @param ProductCollectionItem $objItem + * + * @return bool + */ + public function updateProduct(IsotopeProduct $objProduct, ProductCollectionItem $objItem) + { + if ($objItem->pid != $this->id) { + throw new \InvalidArgumentException('Item does not belong to this collection'); + } + + // !HOOK: additional functionality when updating product in collection + if (isset($GLOBALS['ISO_HOOKS']['updateProductInCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['updateProductInCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['updateProductInCollection'] as $callback) { + if (false === System::importStatic($callback[0])->{$callback[1]}($objProduct, $objItem, $this)) { + return false; + } + } + } + + $this->setProductForItem($objProduct, $objItem, $objItem->quantity); + $objItem->save(); + + // !HOOK: additional functionality when adding product to collection + if (isset($GLOBALS['ISO_HOOKS']['postUpdateProductInCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['postUpdateProductInCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['postUpdateProductInCollection'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($objProduct, $objItem, $this); + } + } + + return true; + } + + /** + * Update a product collection item + * + * @param ProductCollectionItem $objItem The product object + * @param array $arrSet The property(ies) to adjust + * + * @return bool + */ + public function updateItem(ProductCollectionItem $objItem, $arrSet) + { + return $this->updateItemById($objItem->id, $arrSet); + } + + /** + * Update product collection item with given ID + * + * @param int $intId + * @param array $arrSet + * + * @return bool + */ + public function updateItemById($intId, $arrSet) + { + $this->ensureNotLocked(); + + $arrItems = $this->getItems(); + + if (!isset($arrItems[$intId])) { + return false; + } + + /** @var ProductCollectionItem $objItem */ + $objItem = $arrItems[$intId]; + + // !HOOK: additional functionality when updating a product in the collection + if (isset($GLOBALS['ISO_HOOKS']['updateItemInCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['updateItemInCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['updateItemInCollection'] as $callback) { + $arrSet = System::importStatic($callback[0])->{$callback[1]}($objItem, $arrSet, $this); + + if (!\is_array($arrSet) || 0 === \count($arrSet)) { + return false; + } + } + } + + // Quantity set to 0, delete item + if (isset($arrSet['quantity']) && $arrSet['quantity'] == 0) { + return $this->deleteItemById($intId); + } + + if (isset($arrSet['quantity']) && $objItem->hasProduct()) { + + // Set product quantity so we can determine the correct minimum price + $objProduct = $objItem->getProduct(); + $intMinimumQuantity = $objProduct->getMinimumQuantity(); + + if ($arrSet['quantity'] < $intMinimumQuantity) { + Message::addInfo(sprintf( + $GLOBALS['TL_LANG']['ERR']['productMinimumQuantity'], + $objProduct->getName(), + $intMinimumQuantity + )); + $arrSet['quantity'] = $intMinimumQuantity; + } + } + + $arrSet['tstamp'] = time(); + + foreach ($arrSet as $k => $v) { + $objItem->$k = $v; + } + + $objItem->save(); + $this->tstamp = time(); + + // !HOOK: additional functionality when adding product to collection + if (isset($GLOBALS['ISO_HOOKS']['postUpdateItemInCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['postUpdateItemInCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['postUpdateItemInCollection'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($objItem, $arrSet['quantity'], $this); + } + } + + return true; + } + + /** + * Remove item from collection + * + * @param ProductCollectionItem $objItem + * + * @return bool + */ + public function deleteItem(ProductCollectionItem $objItem) + { + return $this->deleteItemById($objItem->id); + } + + /** + * Remove item with given ID from collection + * + * @param int $intId + * + * @return bool + * + * @throws \BadMethodCallException if the product collection is locked. + */ + public function deleteItemById($intId) + { + $this->ensureNotLocked(); + + $arrItems = $this->getItems(); + + if (!isset($arrItems[$intId])) { + return false; + } + + $objItem = $arrItems[$intId]; + + // !HOOK: additional functionality when a product is removed from the collection + if (isset($GLOBALS['ISO_HOOKS']['deleteItemFromCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['deleteItemFromCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['deleteItemFromCollection'] as $callback) { + $blnRemove = System::importStatic($callback[0])->{$callback[1]}($objItem, $this); + + if ($blnRemove === false) { + return false; + } + } + } + + $objItem->delete(); + + unset($this->arrItems[$intId]); + + $this->tstamp = time(); + + // !HOOK: additional functionality when adding product to collection + if (isset($GLOBALS['ISO_HOOKS']['postDeleteItemFromCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['postDeleteItemFromCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['postDeleteItemFromCollection'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($objItem, $this); + } + } + + return true; + } + + /** + * Find surcharges for the current collection + * + * @return ProductCollectionSurcharge[] + */ + public function getSurcharges() + { + if (null === $this->arrSurcharges) { + if ($this->isLocked()) { + $this->arrSurcharges = []; + + if (($objSurcharges = ProductCollectionSurcharge::findBy('pid', $this->id)) !== null) { + $this->arrSurcharges = $objSurcharges->getModels(); + } + } else { + $this->arrSurcharges = ProductCollectionSurcharge::findForCollection($this); + } + } + + return $this->arrSurcharges; + } + + /** + * Copy product collection items from another collection to this one (e.g. Cart to Order) + * + * @param IsotopeProductCollection $objSource + * + * @return int[] + * + * @throws \BadMethodCallException if the product collection is locked. + */ + public function copyItemsFrom(IsotopeProductCollection $objSource) + { + $this->ensureNotLocked(); + + $this->updateDatabase(); + + // Make sure database table has the latest prices + $objSource->updateDatabase(); + + $time = time(); + $arrIds = []; + $arrOldItems = $objSource->getItems(); + + foreach ($arrOldItems as $objOldItem) { + + // !HOOK: additional functionality when copying product to collection + if (isset($GLOBALS['ISO_HOOKS']['copyCollectionItem']) + && \is_array($GLOBALS['ISO_HOOKS']['copyCollectionItem']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['copyCollectionItem'] as $callback) { + if (System::importStatic($callback[0])->{$callback[1]}($objOldItem, $objSource, $this) === false) { + continue; + } + } + } + + if ($objOldItem->hasProduct() && $this->hasProduct($objOldItem->getProduct())) { + + $objNewItem = $this->getItemForProduct($objOldItem->getProduct()); + $objNewItem->increaseQuantityBy($objOldItem->quantity); + + } else { + + $objNewItem = clone $objOldItem; + $objNewItem->pid = $this->id; + $objNewItem->tstamp = $time; + $objNewItem->save(); + } + + $arrIds[$objOldItem->id] = $objNewItem->id; + } + + if (\count($arrIds) > 0) { + $this->tstamp = $time; + } + + // !HOOK: additional functionality when adding product to collection + if (isset($GLOBALS['ISO_HOOKS']['copiedCollectionItems']) + && \is_array($GLOBALS['ISO_HOOKS']['copiedCollectionItems']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['copiedCollectionItems'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($objSource, $this, $arrIds); + } + } + + $this->clearCache(); + + return $arrIds; + } + + /** + * Copy product collection surcharges from another collection to this one (e.g. Cart to Order) + * + * @param IsotopeProductCollection $objSource + * @param array $arrItemMap + * + * @return int[] + * + * @deprecated Deprecated since version 2.2, to be removed in 3.0. + * Surcharges are calculated on the fly, so it does not make sense to copy them from another one. + * + * @throws \BadMethodCallException if the product collection is locked. + */ + public function copySurchargesFrom(IsotopeProductCollection $objSource, array $arrItemMap = array()) + { + $this->ensureNotLocked(); + + $arrIds = array(); + $time = time(); + $sorting = 128; + + foreach ($objSource->getSurcharges() as $objSourceSurcharge) { + $objSurcharge = clone $objSourceSurcharge; + $objSurcharge->pid = $this->id; + $objSurcharge->tstamp = $time; + $objSurcharge->sorting = $sorting; + + // Convert surcharge amount for individual product IDs + $objSurcharge->convertCollectionItemIds($arrItemMap); + + $objSurcharge->save(); + + $arrIds[$sorting] = $objSurcharge->id; + + $sorting += 128; + } + + // Empty cache + $this->arrSurcharges = null; + $this->arrCache = null; + + return $arrIds; + } + + /** + * @inheritdoc + */ + public function addToScale(Scale $objScale = null) + { + if (null === $objScale) { + $objScale = new Scale(); + } + + foreach ($this->getItems() as $objItem) { + if (!$objItem->hasProduct()) { + continue; + } + + $objProduct = $objItem->getProduct(); + + if ($objProduct instanceof WeightAggregate) { + $objWeight = $objProduct->getWeight(); + + if (null !== $objWeight) { + for ($i = 0; $i < $objItem->quantity; $i++) { + $objScale->add($objWeight); + } + } + + } elseif ($objProduct instanceof Weighable) { + for ($i = 0; $i < $objItem->quantity; $i++) { + $objScale->add($objProduct); + } + } + } + + return $objScale; + } + + /** + * @inheritdoc + */ + public function addToTemplate(Template $objTemplate, array $arrConfig = []) + { + $arrGalleries = array(); + $objConfig = $this->getRelated('config_id') ?: Isotope::getConfig(); + $arrItems = $this->addItemsToTemplate($objTemplate, $arrConfig['sorting']); + + $objTemplate->id = $this->id; + $objTemplate->collection = $this; + $objTemplate->config = $objConfig; + $objTemplate->surcharges = Frontend::formatSurcharges($this->getSurcharges(), $objConfig->currency); + $objTemplate->subtotal = Isotope::formatPriceWithCurrency($this->getSubtotal(), true, $objConfig->currency); + $objTemplate->total = Isotope::formatPriceWithCurrency($this->getTotal(), true, $objConfig->currency); + $objTemplate->tax_free_subtotal = Isotope::formatPriceWithCurrency($this->getTaxFreeSubtotal(), true, $objConfig->currency); + $objTemplate->tax_free_total = Isotope::formatPriceWithCurrency($this->getTaxFreeTotal(), true, $objConfig->currency); + + $objTemplate->hasAttribute = function ($strAttribute, ProductCollectionItem $objItem) { + if (!$objItem->hasProduct()) { + return false; + } + + $objProduct = $objItem->getProduct(); + + return \in_array($strAttribute, $objProduct->getAttributes(), true) + || \in_array($strAttribute, $objProduct->getVariantAttributes(), true); + }; + + $objTemplate->generateAttribute = function ( + $strAttribute, + ProductCollectionItem $objItem, + array $arrOptions = array() + ) { + if (!$objItem->hasProduct()) { + return ''; + } + + $objAttribute = $GLOBALS['TL_DCA']['tl_iso_product']['attributes'][$strAttribute]; + + if (!($objAttribute instanceof IsotopeAttribute)) { + throw new \InvalidArgumentException($strAttribute . ' is not a valid attribute'); + } + + return $objAttribute->generate($objItem->getProduct(), $arrOptions); + }; + + $objTemplate->getGallery = function ( + $strAttribute, + ProductCollectionItem $objItem + ) use ( + $arrConfig, + &$arrGalleries + ) { + if (!$objItem->hasProduct()) { + return new StandardGallery(); + } + + $strCacheKey = 'product' . $objItem->product_id . '_' . $strAttribute; + $arrConfig['jumpTo'] = $objItem->getRelated('jumpTo'); + + if (!isset($arrGalleries[$strCacheKey])) { + $arrGalleries[$strCacheKey] = Gallery::createForProductAttribute( + $objItem->getProduct(), + $strAttribute, + $arrConfig + ); + } + + return $arrGalleries[$strCacheKey]; + }; + + $objTemplate->attributeLabel = function ($name, array $options = []) { + /** @var Attribute $attribute */ + $attribute = $GLOBALS['TL_DCA']['tl_iso_product']['attributes'][$name] ?? null; + + if (!$attribute instanceof IsotopeAttribute) { + return Format::dcaLabel('tl_iso_product', $name); + } + + return $attribute->getLabel($options); + }; + + $objTemplate->attributeValue = function ($name, $value, array $options = []) { + /** @var Attribute $attribute */ + $attribute = $GLOBALS['TL_DCA']['tl_iso_product']['attributes'][$name] ?? null; + + if (!$attribute instanceof IsotopeAttribute) { + return Format::dcaValue('tl_iso_product', $name, $value); + } + + return $attribute->generateValue($value, $options); + }; + + // !HOOK: allow overriding of the template + if (isset($GLOBALS['ISO_HOOKS']['addCollectionToTemplate']) + && \is_array($GLOBALS['ISO_HOOKS']['addCollectionToTemplate']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['addCollectionToTemplate'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($objTemplate, $arrItems, $this, $arrConfig); + } + } + } + + /** + * @inheritdoc + */ + public function addError($message) + { + $this->arrErrors[] = $message; + } + + /** + * @inheritdoc + */ + public function hasErrors() + { + if (\count($this->arrErrors) > 0) { + return true; + } + + foreach ($this->getItems() as $objItem) { + if ($objItem->hasErrors()) { + return true; + } + } + + return false; + } + + /** + * @inheritdoc + */ + public function getErrors() + { + $arrErrors = $this->arrErrors; + + foreach ($this->getItems() as $objItem) { + if ($objItem->hasErrors()) { + array_unshift($arrErrors, $this->getMessageIfErrorsInItems()); + break; + } + } + + return $arrErrors; + } + + /** + * Loop over items and add them to template + * + * @param Callable $varCallable + * + * @return array + */ + protected function addItemsToTemplate(Template $objTemplate, $varCallable = null) + { + $taxIds = array(); + $arrItems = array(); + + foreach ($this->getItems($varCallable) as $objItem) { + $item = $this->generateItem($objItem); + + $taxIds[] = $item['tax_id']; + $arrItems[] = $item; + } + + RowClass::withKey('rowClass')->addCount('row_')->addFirstLast('row_')->addEvenOdd('row_')->applyTo($arrItems); + + $objTemplate->items = $arrItems; + $objTemplate->total_tax_ids = \count(array_unique($taxIds)); + + return $arrItems; + } + + /** + * Generate item array for template + * + * @param ProductCollectionItem $objItem + * + * @return array + */ + protected function generateItem(ProductCollectionItem $objItem) + { + $blnHasProduct = $objItem->hasProduct(); + $objProduct = $objItem->getProduct(); + $objConfig = $this->getRelated('config_id') ?: Isotope::getConfig(); + $arrCSS = ($blnHasProduct ? StringUtil::deserialize($objProduct->cssID, true) : array()); + + // Set the active product for insert tags replacement + if ($blnHasProduct) { + Product::setActive($objProduct); + } + + $arrItem = array( + 'id' => $objItem->id, + 'sku' => $objItem->getSku(), + 'name' => $objItem->getName(), + 'options' => Isotope::formatOptions($objItem->getOptions()), + 'configuration' => $objItem->getConfiguration(), + 'attributes' => $objItem->getAttributes(), + 'quantity' => $objItem->quantity, + 'price' => Isotope::formatPriceWithCurrency($objItem->getPrice(), true, $objConfig->currency), + 'tax_free_price' => Isotope::formatPriceWithCurrency($objItem->getTaxFreePrice(), true, $objConfig->currency), + 'original_price' => Isotope::formatPriceWithCurrency($objItem->getOriginalPrice(), true, $objConfig->currency), + 'total' => Isotope::formatPriceWithCurrency($objItem->getTotalPrice(), true, $objConfig->currency), + 'tax_free_total' => Isotope::formatPriceWithCurrency($objItem->getTaxFreeTotalPrice(), true, $objConfig->currency), + 'original_total' => Isotope::formatPriceWithCurrency($objItem->getTotalOriginalPrice(), true, $objConfig->currency), + 'tax_id' => $objItem->tax_id, + 'href' => false, + 'hasProduct' => $blnHasProduct, + 'product' => $objProduct, + 'item' => $objItem, + 'raw' => $objItem->row(), + 'rowClass' => trim('product ' . (($blnHasProduct && $objProduct->isNew()) ? 'new ' : '') . ($arrCSS[1] ?? '')) + ); + + if ($blnHasProduct && null !== $objItem->getRelated('jumpTo') && $objProduct->isAvailableInFrontend()) { + $arrItem['href'] = $objProduct->generateUrl($objItem->getRelated('jumpTo')); + } + + Product::unsetActive(); + + return $arrItem; + } + + /** + * Get a collection-specific error message for items with errors + * + * @return string + */ + protected function getMessageIfErrorsInItems() + { + return $GLOBALS['TL_LANG']['ERR']['collectionErrorInItems']; + } + + /** + * Generate the next higher Document Number based on existing records + * + * @param string $strPrefix + * @param int $intDigits + * + * @return string + * @throws \Exception + */ + protected function generateDocumentNumber($strPrefix, $intDigits) + { + if ($this->arrData['document_number'] != '') { + return $this->arrData['document_number']; + } + + // !HOOK: generate a custom order ID + if (isset($GLOBALS['ISO_HOOKS']['generateDocumentNumber']) + && \is_array($GLOBALS['ISO_HOOKS']['generateDocumentNumber']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['generateDocumentNumber'] as $callback) { + $strOrderId = System::importStatic($callback[0])->{$callback[1]}($this, $strPrefix, $intDigits); + + if ($strOrderId !== false) { + $this->arrData['document_number'] = $strOrderId; + break; + } + } + } + + try { + if ($this->arrData['document_number'] == '') { + $strPrefix = Controller::replaceInsertTags($strPrefix, false); + $intPrefix = utf8_strlen($strPrefix); + + // Lock tables so no other order can get the same ID + Database::getInstance()->lockTables(array(static::$strTable => 'WRITE')); + + $prefixCondition = ($strPrefix != '' ? " AND document_number LIKE '$strPrefix%'" : ''); + + // Retrieve the highest available order ID + $objMax = Database::getInstance() + ->prepare(" + SELECT document_number + FROM tl_iso_product_collection + WHERE + type=? + $prefixCondition + AND store_id=? + ORDER BY CAST(" . ($strPrefix != '' ? 'SUBSTRING(document_number, ' . ($intPrefix + 1) . ')' : 'document_number') . ' AS UNSIGNED) DESC + ') + ->limit(1) + ->execute( + array_search(\get_called_class(), static::getModelTypes(), true), + $this->store_id + ) + ; + + $intMax = (int) substr($objMax->document_number, $intPrefix); + + $this->arrData['document_number'] = $strPrefix . str_pad($intMax + 1, $intDigits, '0', STR_PAD_LEFT); + } + + Database::getInstance() + ->prepare('UPDATE tl_iso_product_collection SET document_number=? WHERE id=?') + ->execute($this->arrData['document_number'], $this->id) + ; + + Database::getInstance()->unlockTables(); + + } catch (\Exception $e) { + // Make sure tables are always unlocked + Database::getInstance()->unlockTables(); + + throw $e; + } + + return $this->arrData['document_number']; + } + + /** + * Generate a unique ID for this collection + * + * @return string + */ + protected function generateUniqueId() + { + if (!empty($this->arrData['uniqid'])) { + return $this->arrData['uniqid']; + } + + return uniqid('', true); + } + + /** + * Prevent modifying a locked collection + * + * @throws \BadMethodCallException if the collection is locked. + */ + protected function ensureNotLocked() + { + if ($this->isLocked()) { + throw new \BadMethodCallException('Product collection is locked'); + } + } + + /** + * Make sure the addresses belong to this collection only, so they will never be modified + * + * @throws \UnderflowException if collection is not saved (not in DB) + * @throws \BadMethodCallException if the product collection is locked. + */ + protected function createPrivateAddresses() + { + $this->ensureNotLocked(); + + if (!$this->id) { + throw new \UnderflowException('Product collection must be saved before creating unique addresses.'); + } + + $canSkip = StringUtil::deserialize($this->iso_checkout_skippable, true); + $objBillingAddress = $this->getBillingAddress(); + $objShippingAddress = $this->getShippingAddress(); + + // Store address in address book + if ($this->iso_addToAddressbook && $this->member > 0) { + if (null !== $objBillingAddress + && $objBillingAddress->ptable != MemberModel::getTable() + && !\in_array('billing_address', $canSkip, true) + ) { + $objAddress = clone $objBillingAddress; + $objAddress->pid = $this->member; + $objAddress->tstamp = time(); + $objAddress->ptable = MemberModel::getTable(); + $objAddress->store_id = $this->store_id; + $objAddress->save(); + + $this->updateDefaultAddress($objAddress); + } + + if (null !== $objBillingAddress + && null !== $objShippingAddress + && $objBillingAddress->id != $objShippingAddress->id + && $objShippingAddress->ptable != MemberModel::getTable() + && !\in_array('shipping_address', $canSkip, true) + ) { + $objAddress = clone $objShippingAddress; + $objAddress->pid = $this->member; + $objAddress->tstamp = time(); + $objAddress->ptable = MemberModel::getTable(); + $objAddress->store_id = $this->store_id; + $objAddress->save(); + + $this->updateDefaultAddress($objAddress); + } + } + + /** @var Config $config */ + $config = $this->getRelated('config_id'); + $billingFields = (null === $config) ? array() : $config->getBillingFields(); + $shippingFields = (null === $config) ? array() : $config->getShippingFields(); + + if (null !== $objBillingAddress + && ($objBillingAddress->ptable != static::$strTable || $objBillingAddress->pid != $this->id) + ) { + $arrData = array_intersect_key( + $objBillingAddress->row(), + array_merge(array_flip($billingFields), ['country' => '']) + ); + + $objNew = new Address(); + $objNew->setRow($arrData); + + $objNew->pid = $this->id; + $objNew->tstamp = time(); + $objNew->ptable = static::$strTable; + $objNew->store_id = $this->store_id; + $objNew->save(); + + $this->setBillingAddress($objNew); + + if (null !== $objShippingAddress && $objBillingAddress->id == $objShippingAddress->id) { + $this->setShippingAddress($objNew); + + // Stop here, we do not need to check shipping address + return; + } + } + + if (null !== $objShippingAddress + && ($objShippingAddress->ptable != static::$strTable || $objShippingAddress->pid != $this->id) + ) { + $arrData = array_intersect_key( + $objShippingAddress->row(), + array_merge(array_flip($shippingFields), ['country' => '']) + ); + + $objNew = new Address(); + $objNew->setRow($arrData); + + $objNew->pid = $this->id; + $objNew->tstamp = time(); + $objNew->ptable = static::$strTable; + $objNew->store_id = $this->store_id; + $objNew->save(); + + $this->setShippingAddress($objNew); + } elseif (null === $objShippingAddress) { + // Make sure to set the shipping address to null if collection has no shipping + // see isotope/core#2014 + $this->setShippingAddress(null); + } + } + + /** + * Mark existing addresses as not default if the new address is default + * + * @param Address $objAddress + */ + protected function updateDefaultAddress(Address $objAddress) + { + $arrSet = array(); + + if ($objAddress->isDefaultBilling) { + $arrSet['isDefaultBilling'] = ''; + } + + if ($objAddress->isDefaultShipping) { + $arrSet['isDefaultShipping'] = ''; + } + + if (\count($arrSet) > 0) { + Database::getInstance() + ->prepare('UPDATE tl_iso_address %s WHERE pid=? AND ptable=? AND store_id=? AND id!=?') + ->set($arrSet) + ->execute($this->member, MemberModel::getTable(), $this->store_id, $objAddress->id) + ; + } + } + + /** + * Clear all cache properties + */ + protected function clearCache() + { + $this->arrItems = null; + $this->arrSurcharges = null; + $this->arrCache = null; + $this->arrErrors = array(); + $this->objPayment = false; + $this->objShipping = false; + } + + /** + * Initialize a new collection and duplicate everything from the source + * + * @param IsotopeProductCollection $objSource + * + * @return static + */ + public static function createFromCollection(IsotopeProductCollection $objSource) + { + $objCollection = new static(); + $objConfig = $objSource->getConfig(); + + if (null === $objConfig) { + $objConfig = Isotope::getConfig(); + } + + $member = $objSource->getMember(); + + $objCollection->source_collection_id = $objSource->getId(); + $objCollection->config_id = (int) $objConfig->id; + $objCollection->store_id = (int) $objSource->getStoreId(); + $objCollection->member = (null === $member ? 0 : $member->id); + + if ($objCollection instanceof IsotopeOrderableCollection + && $objSource instanceof IsotopeOrderableCollection) + { + $objCollection->setShippingMethod($objSource->getShippingMethod()); + $objCollection->setPaymentMethod($objSource->getPaymentMethod()); + + $objCollection->setShippingAddress($objSource->getShippingAddress()); + $objCollection->setBillingAddress($objSource->getBillingAddress()); + } + + $arrItemIds = $objCollection->copyItemsFrom($objSource); + + $objCollection->updateDatabase(); + + // HOOK: order status has been updated + if (isset($GLOBALS['ISO_HOOKS']['createFromProductCollection']) + && \is_array($GLOBALS['ISO_HOOKS']['createFromProductCollection']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['createFromProductCollection'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($objCollection, $objSource, $arrItemIds); + } + } + + return $objCollection; + } + + /** + * Method that returns a closure to sort product collection items + * + * @param string $strOrderBy + * + * @return \Closure|null + */ + public static function getItemsSortingCallable($strOrderBy = 'asc_id') + { + [$direction, $attribute] = explode('_', $strOrderBy, 2) + [null, null]; + + if ('asc' === $direction) { + return function ($arrItems) use ($attribute) { + uasort($arrItems, function ($objItem1, $objItem2) use ($attribute) { + if ($objItem1->$attribute == $objItem2->$attribute) { + return 0; + } + + return $objItem1->$attribute < $objItem2->$attribute ? -1 : 1; + }); + + return $arrItems; + }; + + } + + if ('desc' === $direction) { + return function ($arrItems) use ($attribute) { + uasort($arrItems, function ($objItem1, $objItem2) use ($attribute) { + if ($objItem1->$attribute == $objItem2->$attribute) { + return 0; + } + + return $objItem1->$attribute > $objItem2->$attribute ? -1 : 1; + }); + + return $arrItems; + }; + } + + return null; + } + + /** + * @param IsotopeProduct $product + * @param ProductCollectionItem $item + * @param int $quantity + */ + private function setProductForItem(IsotopeProduct $product, ProductCollectionItem $item, $quantity) + { + $item->tstamp = time(); + $item->type = array_search(\get_class($product), Product::getModelTypes(), true); + $item->product_id = (int) $product->getId(); + $item->sku = (string) $product->getSku(); + $item->name = (string) $product->getName(); + $item->configuration = $product->getOptions(); + $item->quantity = (int) $quantity; + $item->price = (float) ($product->getPrice($this) ? $product->getPrice($this)->getAmount((int) $quantity) : 0); + $item->tax_free_price = (float) ($product->getPrice($this) ? $product->getPrice($this)->getNetAmount((int) $quantity) : 0); + } + + /** + * Check if product collection has tax + * + * @return bool + */ + public function hasTax() + { + foreach ($this->getSurcharges() as $surcharge) { + if ($surcharge instanceof Tax) { + return true; + } + } + + return false; + } +} diff --git a/api2/ProductCollectionSurcharge.php b/api2/ProductCollectionSurcharge.php new file mode 100644 index 0000000..05f3d61 --- /dev/null +++ b/api2/ProductCollectionSurcharge.php @@ -0,0 +1,621 @@ +tax_class > 0 || !empty($this->arrProducts)) ? true : false; + } + + /** + * Get tax amount for an individual collection item + * + * @param ProductCollectionItem $objItem + * + * @return float + */ + public function getAmountForCollectionItem(ProductCollectionItem $objItem) + { + if (isset($this->arrProducts[$objItem->id])) { + + return (float) $this->arrProducts[$objItem->id]; + } + + return 0; + } + + /** + * Set tax amount for a collection item + * + * @param float $fltAmount + * @param ProductCollectionItem $objItem + */ + public function setAmountForCollectionItem($fltAmount, ProductCollectionItem $objItem) + { + if ($fltAmount != 0) { + $this->arrProducts[$objItem->id] = $fltAmount; + } else { + unset($this->arrProducts[$objItem->id]); + } + } + + /** + * Update IDs of tax per product config + * + * @param array $arrIdMap + * + * @deprecated Deprecated since version 2.2, to be removed in 3.0. + * Surcharges are generated on the fly, so it does not make sense to convert item IDs + */ + public function convertCollectionItemIds($arrIdMap) + { + $arrProducts = array(); + + foreach ($this->arrProducts as $k => $v) { + if (isset($arrIdMap[$k])) { + $arrProducts[$arrIdMap[$k]] = $v; + } + } + + $this->arrProducts = $arrProducts; + } + + + /** + * Split tax amount amongst collection products + * + * @param IsotopeProductCollection $objCollection + * @param \Model $objSource + */ + public function applySplittedTax(IsotopeProductCollection $objCollection, $objSource) + { + $this->tax_class = 0; + $this->before_tax = true; + $fltTotal = 0; + + if (!$objSource->isPercentage()) { + $fltTotal = $objCollection->getTaxFreeSubtotal(); + + if ($fltTotal == 0) { + return; + } + } + + foreach ($objCollection->getItems() as $objItem) { + if ($objSource->isPercentage()) { + $fltProductPrice = $objItem->getTotalPrice() / 100 * $objSource->getPercentage(); + } else { + $fltProductPrice = $this->total_price / 100 * (100 / $fltTotal * $objItem->getTaxFreeTotalPrice()); + } + + $fltProductPrice = $fltProductPrice > 0 ? (floor($fltProductPrice * 100) / 100) : (ceil($fltProductPrice * 100) / 100); + + $this->setAmountForCollectionItem($fltProductPrice, $objItem); + } + } + + /** + * Add a tax number + * + * @param int $intId + */ + public function addTaxNumber($intId) + { + if (!\in_array($intId, $this->arrTaxIds)) { + $this->arrTaxIds[] = (int) $intId; + } + } + + /** + * Get comma separated list of tax ids + * + * @return string + */ + public function getTaxNumbers() + { + return implode(',', $this->arrTaxIds); + } + + /** + * Set the current record from an array + * + * @param array $arrData The data record + * + * @return \Model The model object + */ + public function setRow(array $arrData) + { + $this->arrProducts = StringUtil::deserialize($arrData['products']); + $this->arrTaxIds = explode(',', $arrData['tax_id']); + + if (!\is_array($this->arrProducts)) { + $this->arrProducts = array(); + } + + if (!\is_array($this->arrTaxIds)) { + $this->arrTaxIds = array(); + } + + unset($arrData['products'], $arrData['tax_id']); + + return parent::setRow($arrData); + } + + /** + * Modify the current row before it is stored in the database + * + * @param array $arrSet The data array + * + * @return array The modified data array + */ + protected function preSave(array $arrSet) + { + $arrSet['products'] = serialize($this->arrProducts); + $arrSet['tax_id'] = implode(',', $this->arrTaxIds); + + return $arrSet; + } + + + /** + * Generate surcharges for a collection + * + * Process: + * 1. Collect surcharges (e.g. shipping and billing) from Isotope core and submodules using hook + * 2. Split surcharges by "with or without tax" + * => surcharges without tax are placed after tax surcharges and ignored in the complex compilation step + * 3. Run through all product collection items and calculate their tax amount + * 4. Run through all surcharges with tax and calculate their tax amount + * + * @param IsotopeProductCollection|\Isotope\Model\ProductCollection\Order $objCollection + * + * @return array + */ + public static function findForCollection(IsotopeProductCollection $objCollection) + { + $arrPreTax = []; + $arrPostTax = []; + $arrTaxes = []; + + // !HOOK: get collection surcharges + if (isset($GLOBALS['ISO_HOOKS']['findSurchargesForCollection']) && \is_array($GLOBALS['ISO_HOOKS']['findSurchargesForCollection'])) { + foreach ($GLOBALS['ISO_HOOKS']['findSurchargesForCollection'] as $callback) { + $arrResult = System::importStatic($callback[0])->{$callback[1]}($objCollection); + + foreach ($arrResult as $objSurcharge) { + if (!($objSurcharge instanceof IsotopeProductCollectionSurcharge) || $objSurcharge instanceof Tax) { + throw new \InvalidArgumentException('Instance of ' . \get_class($objSurcharge) . ' is not a valid product collection surcharge.'); + } + + if ($objSurcharge->hasTax()) { + $arrPreTax[] = $objSurcharge; + } else { + $arrPostTax[] = $objSurcharge; + } + } + } + } + + static::addTaxesForItems($arrTaxes, $objCollection, $arrPreTax); + + static::addTaxesForSurcharges( + $arrTaxes, + $arrPreTax, + [ + 'billing' => $objCollection->getBillingAddress(), + 'shipping' => $objCollection->getShippingAddress(), + ] + ); + + return array_merge($arrPreTax, $arrTaxes, $arrPostTax); + } + + + /** + * Create a payment surcharge + * + * @param IsotopePayment $objPayment + * @param IsotopeProductCollection $objCollection + * + * @return Payment + */ + public static function createForPaymentInCollection(IsotopePayment $objPayment, IsotopeProductCollection $objCollection) + { + return static::buildSurcharge('Isotope\Model\ProductCollectionSurcharge\Payment', $GLOBALS['TL_LANG']['MSC']['paymentLabel'], $objPayment, $objCollection); + } + + /** + * Create a shipping surcharge + * + * @param IsotopeShipping $objShipping + * @param IsotopeProductCollection $objCollection + * + * @return Shipping + */ + public static function createForShippingInCollection(IsotopeShipping $objShipping, IsotopeProductCollection $objCollection) + { + return static::buildSurcharge('Isotope\Model\ProductCollectionSurcharge\Shipping', $GLOBALS['TL_LANG']['MSC']['shippingLabel'], $objShipping, $objCollection); + } + + + /** + * Build a product collection surcharge for given class type + * + * @param string $strClass + * @param string $strLabel + * @param IsotopePayment|IsotopeShipping $objSource + * @param IsotopeProductCollection $objCollection + * + * @return ProductCollectionSurcharge + */ + protected static function buildSurcharge($strClass, $strLabel, $objSource, IsotopeProductCollection $objCollection) + { + $intTaxClass = $objSource->tax_class; + + /** @var ProductCollectionSurcharge $objSurcharge */ + $objSurcharge = new $strClass(); + $objSurcharge->source_id = $objSource->id; + $objSurcharge->label = sprintf($strLabel, $objSource->getLabel()); + $objSurcharge->price = ($objSource->isPercentage() ? $objSource->getPercentage() . '%' : ' '); + $objSurcharge->total_price = $objSource->getPrice(); + $objSurcharge->tax_free_total_price = $objSurcharge->total_price; + $objSurcharge->tax_class = $intTaxClass; + $objSurcharge->before_tax = ($intTaxClass ? true : false); + $objSurcharge->addToTotal = true; + + if ($intTaxClass == -1) { + $objSurcharge->applySplittedTax($objCollection, $objSource); + } elseif ($intTaxClass > 0) { + + /** @var TaxClass $objTaxClass */ + if (($objTaxClass = TaxClass::findByPk($intTaxClass)) !== null) { + + /** @var TaxRate $objIncludes */ + if (($objIncludes = $objTaxClass->getRelated('includes')) !== null) { + + $fltPrice = $objSurcharge->total_price; + $arrAddresses = array( + 'billing' => $objCollection->getBillingAddress(), + 'shipping' => $objCollection->getShippingAddress(), + ); + + if ($objIncludes->isApplicable($fltPrice, $arrAddresses)) { + $fltTax = $objIncludes->calculateAmountIncludedInPrice($fltPrice); + $objSurcharge->tax_free_total_price = $fltPrice - $fltTax; + } + } + } + } + + return $objSurcharge; + } + + /** + * Create or add taxes for each collection item + * + * @param Tax[] $arrTaxes + * @param IsotopeOrderableCollection $objCollection + * @param ProductCollectionSurcharge[] $arrSurcharges + * @param Address[] $arrAddresses + */ + private static function addTaxesForItems(array &$arrTaxes, IsotopeProductCollection $objCollection, array $arrSurcharges, array $arrAddresses = null) + { + foreach ($objCollection->getItems() as $objItem) { + + // This should never happen, but we can't calculate it + if (!$objItem->hasProduct()) { + continue; + } + + $objProduct = $objItem->getProduct(); + + /** @var TaxClass $objTaxClass */ + $objTaxClass = $objProduct->getPrice() ? $objProduct->getPrice()->getRelated('tax_class') : null; + + // Skip products without tax class + if (null === $objTaxClass) { + continue; + } + + $arrTaxIds = []; + $fltPrice = $objItem->getTotalPrice(); + + /** @var ProductCollectionSurcharge $objSurcharge */ + foreach ($arrSurcharges as $objSurcharge) { + $fltPrice += $objSurcharge->getAmountForCollectionItem($objItem); + } + + $productAddresses = $arrAddresses; + + if (null === $productAddresses) { + $productAddresses = array( + 'billing' => $objCollection->getBillingAddress(), + 'shipping' => $objProduct->isExemptFromShipping() ? $objCollection->getBillingAddress() : $objCollection->getShippingAddress(), + ); + } + + /** @var TaxRate $objIncludes */ + if (($objIncludes = $objTaxClass->getRelated('includes')) !== null + && $objIncludes->isApplicable($fltPrice, $productAddresses) + ) { + $addToTotal = static::getTaxAddState(false); + $total = $addToTotal ? $objIncludes->calculateAmountAddedToPrice($fltPrice) : $objIncludes->calculateAmountIncludedInPrice($fltPrice); + + $arrTaxIds[] = static::addTax( + $arrTaxes, + $objTaxClass->id . '_' . $objIncludes->id, + ($objTaxClass->getLabel() ?: $objIncludes->getLabel()), + $objIncludes->getAmount(), + $objIncludes->isPercentage(), + $total, + $objTaxClass->applyRoundingIncrement, + $addToTotal, + $objTaxClass->notNegative + ); + } + + /** @var TaxRate[] $objRates */ + if (($objRates = $objTaxClass->getRelated('rates')) !== null) { + foreach ($objRates as $objTaxRate) { + + if ($objTaxRate->isApplicable($fltPrice, $productAddresses)) { + $addToTotal = static::getTaxAddState(true); + $total = $addToTotal ? $objTaxRate->calculateAmountAddedToPrice($fltPrice) : $objTaxRate->calculateAmountIncludedInPrice($fltPrice); + + $arrTaxIds[] = static::addTax( + $arrTaxes, + $objTaxRate->id, + $objTaxRate->getLabel(), + $objTaxRate->getAmount(), + $objTaxRate->isPercentage(), + $total, + $objTaxClass->applyRoundingIncrement, + $addToTotal, + $objTaxClass->notNegative + ); + + if ($objTaxRate->stop) { + break; + } + } + } + } + + $strTaxId = implode(',', $arrTaxIds); + + if ($objItem->tax_id != $strTaxId) { + $objCollection->updateItem($objItem, array('tax_id' => $strTaxId)); + } + + foreach ($arrSurcharges as $objSurcharge) { + if ($objSurcharge->getAmountForCollectionItem($objItem) > 0) { + foreach ($arrTaxIds as $taxId) { + $objSurcharge->addTaxNumber($taxId); + } + } + } + } + } + + /** + * Create or add taxes for pre-tax collection surcharges + * + * @param Tax[] $arrTaxes + * @param ProductCollectionSurcharge[] $arrSurcharges + * @param Address[] $arrAddresses + */ + private static function addTaxesForSurcharges(array &$arrTaxes, array $arrSurcharges, array $arrAddresses) + { + foreach ($arrSurcharges as $objSurcharge) { + + /** @var TaxClass $objTaxClass */ + $objTaxClass = TaxClass::findByPk($objSurcharge->tax_class); + + // Skip products without tax class + if (null === $objTaxClass) { + continue; + } + + $fltPrice = $objSurcharge->total_price; + + /** @var TaxRate $objIncludes */ + if (($objIncludes = $objTaxClass->getRelated('includes')) !== null + && $objIncludes->isApplicable($fltPrice, $arrAddresses) + ) { + $addToTotal = static::getTaxAddState(false); + $fltPrice = $addToTotal ? $objIncludes->calculateAmountAddedToPrice($fltPrice) : $objIncludes->calculateAmountIncludedInPrice($fltPrice); + + $taxId = static::addTax( + $arrTaxes, + $objTaxClass->id . '_' . $objIncludes->id, + ($objTaxClass->getLabel() ?: $objIncludes->getLabel()), + $objIncludes->getAmount(), + $objIncludes->isPercentage(), + $fltPrice, + $objTaxClass->applyRoundingIncrement, + $addToTotal, + $objTaxClass->notNegative + ); + + $objSurcharge->addTaxNumber($taxId); + } + + /** @var TaxRate[] $objRates */ + if (($objRates = $objTaxClass->getRelated('rates')) !== null) { + foreach ($objRates as $objTaxRate) { + + if ($objTaxRate->isApplicable($fltPrice, $arrAddresses)) { + $addToTotal = static::getTaxAddState(true); + $fltPrice = $addToTotal ? $objTaxRate->calculateAmountAddedToPrice($fltPrice) : $objTaxRate->calculateAmountIncludedInPrice($fltPrice); + + $taxId = static::addTax( + $arrTaxes, + $objTaxRate->id, + $objTaxRate->getLabel(), + $objTaxRate->getAmount(), + $objTaxRate->isPercentage(), + $fltPrice, + $objTaxClass->applyRoundingIncrement, + $addToTotal, + $objTaxClass->notNegative + ); + + $objSurcharge->addTaxNumber($taxId); + + if ($objTaxRate->stop) { + break; + } + } + } + } + } + } + + /** + * Add tax amount to the array of taxes, creating a new instance of Tax model if necessary + * + * @param array $arrTaxes + * @param string $id + * @param string $label + * @param mixed $price + * @param bool $isPercentage + * @param float $total + * @param bool $applyRoundingIncrement + * @param bool $addToTotal + * @param bool $notNegative + * + * @return int + */ + private static function addTax(array &$arrTaxes, $id, $label, $price, $isPercentage, $total, $applyRoundingIncrement, $addToTotal, $notNegative) + { + $objTax = $arrTaxes[$id]; + + if (null === $objTax || !($objTax instanceof Tax)) { + $objTax = new Tax(); + $objTax->label = $label; + $objTax->price = $price . ($isPercentage ? '%' : ''); + $objTax->total_price = $total; + $objTax->addToTotal = $addToTotal; + $objTax->applyRoundingIncrement = $applyRoundingIncrement; + + $arrTaxes[$id] = $objTax; + } else { + $objTax->total_price = ($objTax->total_price + $total); + + if (is_numeric($objTax->price) && is_numeric($price)) { + $objTax->price += $price; + } + } + + if ($notNegative && $objTax->total_price < 0) { + $objTax->total_price = 0; + } + + $taxId = array_search($id, array_keys($arrTaxes)) + 1; + $objTax->addTaxNumber($taxId); + + return $taxId; + } + + /** + * Get "add to total" state for tax rate + * + * @param bool $default The legacy state (if tax was included in backend price) + * + * @return bool + */ + private static function getTaxAddState($default) + { + switch (Isotope::getConfig()->getPriceDisplay()) { + case Config::PRICE_DISPLAY_NET: + return true; + + case Config::PRICE_DISPLAY_GROSS: + case Config::PRICE_DISPLAY_FIXED: + return false; + + case Config::PRICE_DISPLAY_LEGACY: + default: + return $default; + } + } +} diff --git a/src/EventListener/PostCheckoutListener.php b/src/EventListener/PostCheckoutListener.php index 542a165..fe7e6a3 100644 --- a/src/EventListener/PostCheckoutListener.php +++ b/src/EventListener/PostCheckoutListener.php @@ -6,58 +6,67 @@ namespace App\EventListener; use Contao\CoreBundle\ServiceAnnotation\Hook; use Contao\FrontendTemplate; use Contao\Module; +use Contao\System; use App\ExactApi\ApiExact; use Isotope\Model\Address; use Isotope\Model\Product; use Isotope\Model\ProductCollection\Order; use Isotope\Model\ProductCollectionItem; +use Isotope\Model\ProductCollectionSurcharge; class PostCheckoutListener { const WAREHOUSE_ID = 'd8a6a9b8-d0ac-4d36-8d00-bd420d0d81f5'; + const PAYMENT_CONDITION = 'PP'; + const VAT_FACTOR = 1.19; + + private Order $order; + private ApiExact $apiExact; + private $dbProductsBySku = []; + private $salesOrderItems = []; + private $billingCustomer = null; + private $shippingCustomer = null; /** * @Hook("postCheckout") */ - // public function __invoke(int $userId, array $userData, Module $module): void public function __invoke(Order $order, $item): void { - $apiExact = new ApiExact(); + $this->order = $order; + $this->apiExact = new ApiExact(); - //ob_start(); - $salesOrderItems = []; - /** @var ProductCollectionItem $orderItem */ - foreach($order->getItems() as $orderItem) { - $salesOrderItems[] = $this->createOrderItem($orderItem); -// /** @var Product $orderItem */ -// $product = $orderItem->getProduct(); -// var_dump($orderItem->id); -// var_dump($orderItem->sku); -// var_dump($orderItem->quantity); -// var_dump($orderItem->tax_free_price); -// -// var_dump($product->id); -// var_dump($product->name); -// var_dump($product->is_set); + $this->setDbProducts(); + $this->setOrderItems(); + $this->setCustomers(); + $surCharges = []; + /** @var ProductCollectionSurcharge $orderSurcharge */ + foreach ($order->getSurcharges() as $orderSurcharge) { + $surCharges[] = $orderSurcharge; } - //var_dump($apiExact->test()); -// file_put_contents('dan.txt', ob_get_contents()); -// ob_end_clean(); - $shippingCustomer = null; -// if ($this->compareAddresses() === false) { -// $shippingCustomer = $apiExact->createCustomer( -// $this->createCustomerApiData($order->getShippingAddress()) -// ); -// } -// - $billingCustomer = $apiExact->createCustomer( - $this->createCustomerApiData($order->getBillingAddress()) - ); - $apiExact->createSalesOrder($this->createSalesOrderData($order, $billingCustomer, $shippingCustomer)); + $this->apiExact->createSalesOrder($this->createSalesOrderData()); + + } + + private function setDbProducts() + { + $db = System::getContainer()->get('database_connection'); + $sql = "SELECT tip.*, tippt.price + FROM `tl_iso_product` tip, `tl_iso_product_price` tipp, `tl_iso_product_pricetier` tippt + WHERE tipp.pid = tip.id + AND tippt.pid = tipp.id"; + $stmt = $db->query($sql); + $dbProducts = $stmt->fetchAll(\PDO::FETCH_ASSOC); + foreach ($dbProducts as $dbProduct) { + $this->dbProductsBySku[$dbProduct['sku']] = $dbProduct; + } +// ob_start(); +// var_dump($this->dbProductsBySku); +// file_put_contents('dan.txt', ob_get_contents()); +// ob_end_clean(); } @@ -89,34 +98,102 @@ class PostCheckoutListener ]; } - private function createOrderItem(ProductCollectionItem $orderItem) + private function setOrderItems() { - /** @var Product $orderItem */ - $product = $orderItem->getProduct(); - return [ - 'Description' => $product->name, - 'Item' => $product->exact_id, - 'UnitPrice' => (float) $orderItem->tax_free_price / (int) $orderItem->quantity, - 'Quantity' => $orderItem->quantity - ]; + /** @var ProductCollectionItem $orderItem */ + foreach($this->order->getItems() as $orderItem) { + /** @var Product $orderItem */ + $product = $orderItem->getProduct(); + + if ($product->is_set === null) { + $name = $product->name; + $exactId = $product->exact_id; + $unitPrice = (float) $orderItem->tax_free_price / (int) $orderItem->quantity; + $quantity = $orderItem->quantity; + $this->salesOrderItems[] = [ + 'Description' => $name, + 'Item' => $exactId, + 'UnitPrice' => $unitPrice, + 'Quantity' => $quantity + ]; + } else { + $productSet = $this->dbProductsBySku[$product->sku]; + $quantity = $orderItem->quantity; + $setProducts = []; + $setPriceSingleProducts = 0.00; + for ($i = 1; $i <= 10; $i++) { + $num = sprintf("%02d", $i); + $fieldSku = 'set_product_' . $num; + $fieldQty = 'set_product_' . $num . '_qty'; + + if ( + $productSet[$fieldSku] !== '' && + array_key_exists($productSet[$fieldSku], $this->dbProductsBySku) && + $productSet[$fieldQty] !== '' + ) { + $dbSetProduct = $this->dbProductsBySku[$productSet[$fieldSku]]; + $qty = (int) $productSet[$fieldQty]; + $netPrice = (float) $dbSetProduct['price'] / self::VAT_FACTOR; + $setPriceSingleProducts += $netPrice * $qty; + $setProducts[] = [ + 'product' => $dbSetProduct, + 'qty' => $qty, + 'netPrice' => $netPrice + ]; + } + } + $setDiscountFactor = (float) $orderItem->tax_free_price / $setPriceSingleProducts; + ob_start(); + var_dump($setProducts); + file_put_contents('flo.txt', ob_get_contents()); + ob_end_clean(); + + foreach ($setProducts as $setProduct) { + $this->salesOrderItems[] = [ + 'Description' => $setProduct['product']['name'], + 'Item' => $setProduct['product']['exact_id'], + 'UnitPrice' => round($setProduct['netPrice'] * $setDiscountFactor, 2), + 'Quantity' => $quantity * $setProduct['qty'] + ]; + } + } + } + } + + private function setCustomers() + { + if ($this->compareAddresses($this->order) === false) { + $this->shippingCustomer = $this->apiExact->createCustomer( + $this->createCustomerApiData($this->order->getShippingAddress()) + ); + } + + $this->billingCustomer = $this->apiExact->createCustomer( + $this->createCustomerApiData($this->order->getBillingAddress()) + ); } - private function createSalesOrderData(Order $order, $billingCustomer, $salesOrderItems, $shippingCustomer = null) + private function createSalesOrderData() { $date = new \DateTime('now'); $res = [ - 'Description' => $billingCustomer['Name'], + 'Description' => $this->order->id, 'OrderDate' => $date->format('m/d/Y H:i:s'), - 'OrderedBy' => $billingCustomer['ID'], + 'OrderedBy' => $this->billingCustomer->ID, 'WarehouseID' => self::WAREHOUSE_ID, - 'SalesOrderLines' => [ - $salesOrderItems - ] + 'SalesOrderLines' => $this->salesOrderItems, + 'PaymentCondition' => self::PAYMENT_CONDITION, ]; - if ($shippingCustomer !== null) { - $res['DeliverTo'] = $shippingCustomer['ID']; + if ($this->shippingCustomer !== null) { + $res['DeliverTo'] = $this->shippingCustomer->ID; } - +// if ($discount !== null) { +// $res['Discount'] = $shippingCustomer->ID; +// } + ob_start(); + var_dump($res); + file_put_contents('dan.txt', ob_get_contents()); + ob_end_clean(); return $res; } } \ No newline at end of file diff --git a/src/ExactApi/ApiExact.php b/src/ExactApi/ApiExact.php index 2bb8c5f..24eab56 100644 --- a/src/ExactApi/ApiExact.php +++ b/src/ExactApi/ApiExact.php @@ -12,16 +12,12 @@ class ApiExact const CLIENT_ID = "5a04d118-349c-4750-aac3-fa3a386999c6"; const CLIENT_SECRET = "E7Wuqcsp4Lih"; - const ACCESS_TOKEN_VALID_DURATION = 600 - 50; const ITEM_GROUP_UUID = 'df17bdaf-2af7-4e9f-8d60-326e36b57764'; const PRODUCT_CODE_PREFIX = "WR"; public function getAccessToken() { $tokenData = json_decode(file_get_contents(__DIR__ . '/tokenData', true)); - ob_start(); - var_dump($tokenData); - ob_end_clean(); if (!property_exists($tokenData, 'expiry_time') || (int)$tokenData->expiry_time < time()) { $postData = array( @@ -44,10 +40,9 @@ class ApiExact $jsonResult = json_decode($response); if (property_exists($jsonResult, 'error')) { - $msg = $jsonResult->error; - throw new \Exception($msg); + return $tokenData->access_token; } - $jsonResult->expiry_time = time() + self::ACCESS_TOKEN_VALID_DURATION; + $jsonResult->expiry_time = time() + (int) $jsonResult->expires_in; file_put_contents(__DIR__ . '/tokenData', json_encode($jsonResult)); $tokenData = json_decode(file_get_contents(__DIR__ . '/tokenData', true)); @@ -96,33 +91,15 @@ class ApiExact public function createCustomer($customerData) { $url = self::API_URL_WNR . "/crm/Accounts"; - return json_decode($this->postApiData($url, $customerData)); + $res = $this->postApiData($url, $customerData); + return json_decode($res)->d; } public function createSalesOrder($salesOrderData) { $url = self::API_URL_WNR . "/salesorder/SalesOrders"; -// $parameters = [ -// 'Description' => 'Daniel Knudsen', -// 'OrderDate' => '07/13/2022 17:00:00', -// 'OrderedBy' => '9ba706a3-d6f5-40c8-8d4f-e55a37692cef', -// 'WarehouseID' => 'd8a6a9b8-d0ac-4d36-8d00-bd420d0d81f5', -// 'SalesOrderLines' => [ -// [ -// 'Description' => 'TEST: Reinigungstuch grün', -// 'Item' => '9dcd8b50-5de6-4f15-a51a-c51282292383', -// 'UnitPrice' => '2.45', -// 'Quantity' => '3' -// ], -// [ -// 'Description' => 'TEST: McQuade´s E-Bike Kettenrückführ - Tool', -// 'Item' => '0e04113f-1299-4d3c-b65e-01e83ca5b3b2', -// 'UnitPrice' => '476.00', -// 'Quantity' => '1' -// ], -// ] -// ]; - return json_decode($this->postApiData($url, $salesOrderData)); + $res = $this->postApiData($url, $salesOrderData); + return json_decode($res)->d; } private function getApiData($url) diff --git a/src/ExactApi/IsotopeDatabaseHandler.php b/src/ExactApi/IsotopeDatabaseHandler.php index 74cb9c0..6532f6d 100644 --- a/src/ExactApi/IsotopeDatabaseHandler.php +++ b/src/ExactApi/IsotopeDatabaseHandler.php @@ -183,11 +183,12 @@ class IsotopeDatabaseHandler private function convertShippingWeight(Product $product) { - $shippingWeight = $product->getGrossWeight() !== '' ? - $product->getGrossWeight() : - $product->getNetWeight(); - return ((int) $shippingWeight === 0 || $shippingWeight === '') ? - '' : - 'a:2:{s:4:"unit";s:2:"kg";s:5:"value";s:3:"' . $shippingWeight . '";}'; + $shippingWeight = $product->getGrossWeight() ?? $product->getNetWeight(); + if ($shippingWeight !== null) { + $length = \mb_strlen( (string) $shippingWeight); + return 'a:2:{s:4:"unit";s:2:"kg";s:5:"value";s:'.$length.':"' . $shippingWeight . '";}'; + } else { + return ''; + } } } \ No newline at end of file diff --git a/src/ExactApi/apiTestGetAccessToken.php b/src/ExactApi/apiTestGetAccessToken.php new file mode 100644 index 0000000..c85a17f --- /dev/null +++ b/src/ExactApi/apiTestGetAccessToken.php @@ -0,0 +1,9 @@ +getAccessToken() . "\n"; diff --git a/src/ExactApi/apiUpdateProducts.php b/src/ExactApi/apiUpdateProducts.php deleted file mode 100644 index 624ed0d..0000000 --- a/src/ExactApi/apiUpdateProducts.php +++ /dev/null @@ -1,23 +0,0 @@ -getAccessToken() . "\n"; -$products = $apiConnector->getProducts(); -$databaseHandler->processProducts($products); diff --git a/src/ExactApi/tokenData b/src/ExactApi/tokenData index 8a2a02d..6ee179a 100644 --- a/src/ExactApi/tokenData +++ b/src/ExactApi/tokenData @@ -1 +1 @@ -{"access_token":"stampDE001.gAAAAKLeomGUJnO_59CX7KmwE4tkt1bPxLR7EfU5E2eHjj6LeU2dAPfkRstKdlJJoudf3y6ehyKwF5wQQ1SljbA5sRrGwQsw4M876o0RRtxVHwyR6_QSoM7kP3qj7HfJLEDWjY-KrlJf_rlKtZZ2sRGJkX9jbd-4XDA_uqQECkf2NnIfVAIAAIAAAAC2ADvWhSjIjtyvs4_rhH9HzqaVKUbQthUoKf6StMSX4O5GzilHhM2cQCQJly_7hbpKHJMe4o167cPYIQLTeOilQdlQh21nhGykWfOKtXG31h210WtT2bg8Kc0MtrHQGi_f161wE_xINPXr-H7GTcXLe4klIGUkv0kmmmUacNoDUDfU1fPRTCCFvmkRnE7iBzd116aY9XMVw6mDucFExI9U-dLOD8IFWCPa9U6DSd50T7WoglDHC8Enh-J92C1c8kAhCCiGyA5GHyb9S1eJAnHFDYzOl9y0pwtIHekBJq-LxpzAbVieBUZrXEsTJnD7eiHvKoy9g_Bl-jOVgPTJT29n0dFDUClE_iQ6MGydAiEVKoyhQO20W7EJg64G8l6ZHZrd8r1J36TZ4jmdZ8E8_wqw3MzYGZm4vEXK4G0Lf2-j5uD0eh3aKPQCCdn_jEiCaEABQ-bsvd9sQazNNHbDwzl82GZdlhY8sqln14NpNloahOxhY9-jKu5nT9IBN2EftT-VoLNWp_E4ydPltM_7SS0Xro9j8nf7xNjpBDa__GeR2QSg_He5VR6u2n4lUbU1Qzks2W38RiCA3jXrQ-T6FCtiiaf5rhMzudqeq2c94Ggbn1g7BD_3S9xUFeA5kmSCkn1NZ3RzSqmkMq4IVPcbsJFYeoCiuZ1gYcvSOoBMHN8mjZ8j3xpPEvzJSX39arD5IeNqrLZDXuEU5aORiRnqjN6kPC4w1GQbbMsEL94r8tYGy8VkW7bstxkar7vb2J8vOPhLamN0mcxJdTZCCJ8tj18e","token_type":"bearer","expires_in":"600","refresh_token":"stampDE001.vhK0!IAAAANKu8TC2-MBN5wFyro7uwanpxXBsRFy7YkYSPaM9jMCm8QEAAAEVEJUYfzoc3Q1hatgTMAOm__pmwjTgdkOyzb7qUj7oRvir1XftD_cb5eE7J-06Q0SxWegFPfULR65PDc6Id0pDXZbugqaKhK24Gg9Gr0x74g4EW8HMYJ0PsW1uqvpOb_ph9mQxYllDQf598_uPZvuvxAMN4NOipKiHEXn9tExbn-rmJdrV9bwa3fAm5b2ALbzfBEKCuzuwLkrjH3cheaMC2TaksJ6M2we9IeAezapsyOVEl6QtnhY67L3Irp_bUV_SCZ6H-gbPDX3mFujxyw-lvRoC-ZcgIpM8EuWC3pN24abFp7Qaj8wE2X9_oAgvoJgn42lHl-x6M-QChdf78f6wiCPquk3OwwuBYsjmiJrHFQt4a6nRS-Y_A-J8IOdwq-_KZkvNTY5UI1ZmiGH5pvIDfaXTcvPn6r3C-sQecdImJM43Y_tgZ9a6p2gwPi8zNZSkXfwQH5bfal6tBcH43_8eGSp_M-VbLb7TotZfJgEIePSlZ--VV1aTaRMJ78_JBloFWvQabvhYEvvuvRaePbdGRgXzj95KgQeW8DzBXE6_zHSmhwkUEv11xgXimH3BFToSV_F25ZND7H3OkVM1-M8Jhbvrzh4SR5wRV5WfzC83Mk58HmDtI7xR0owYAu-9mtidFsK8mRC3nJzZSOWdGenZ","expiry_time":1757726518} \ No newline at end of file +{"access_token":"stampDE001.gAAAAIvlfYa1yl6GgHBfQ7s4895yMlmx53bHpCWOLs9a0hCTX3MGOzXdRCmkw3pwnSG6nGNkl0YFbrjxlTQIvuMZOW-OQxq-4T8Yahd2f7xzfHTPqymc84Vn-H9ifgbEGNMTj-yzdQsK2RfqQOkZZstcbIJgojOdJLjUBTRxkzhVNVh9VAIAAIAAAAC9pfgy6bovfxx4UTxP4uF8qdKQy-_nZs04PaWyybqmF7SDYh015d3spn5hG3YR_yISS9qXOPI6e3NkXp6O4S51XhXw6kLAEpb8MmLpukJTclDlSWRuc4Loa8uFpkAYSXB2EDQWkQ6nb0hrNfA4QvE97b9rhkdQq3YVr2eHVyYLBWmcwE7Jp-vApO2EKsW8QtNVhRscNPexfi8vh4MxQYWupLiIhh-BVcfD1NqdmU85jWFCGcUR_UZ2qObtok3OidWavOWsQsR4PMnact3oH1SQQQawV-W73LutWflEGMBEXgMqYA7P-u-dOr1O7zOH-C-dkk6FqdG_gWL0igft8Zf5Pw5zPg3ZqYHsiN7Vl8t-rN2aKV_FlGIlFJvOVW3OrkSSaXvx41ielYg9iAFFE5N10EIoypzRHVMIhdWa3lIvwezelq0WDGzayhK-UKYNOIUOciEXMaPDWkUkOTKSd8ThOuPVtbHNWc5bB2nuGgxwBXXIMZJv0mfE0O0gxm6V6GOEfWtIsqVBjwlAzPnsw_TfLyt2CXnTmytqFdFiSz4LoMP2rnt5fw3J6wH7wUk8v04FxjZJxK4MX8CkJHXHgs4F5mdfDuaxvIMIvmpRnmAFcJDr0MMCBLUz5t5anKRDlYKvjPxqupbILIHo4YFJ-04bu8oyeXoaUYnHx8hRzGeUk6s4fKy07nTMgUSmd8tWd_bCyaFxASgxv6Lnggh7plUIt-v8er290lm6xd6_0LJk9rFWqbBkLmSwIgL-xwkMFQP2iGC4ljPoLkMRHBKdqdbI","token_type":"bearer","expires_in":"600","refresh_token":"stampDE001.vhK0!IAAAAKjrezKWeYTHhtfVcl116j1YPQQGxeoEQsp0FsJzSa2Q8QEAAAHEvZm9p-Qv7g9WYZwQ8oFXlfacvvuDKRkEwNVK6wXj3vDKUhzqH9MNZkMLlid-FO76Rjg3YYk15cYB1s6GPzuqkpVtA-tYZdQoYA8gynmgb--UaI46_LVGkyQ_GwvxA61JN69xtp24Gx62pRyPD1F3U7j4iA1DAUd9cEkTdSAUsspjLIoGPEu_BL468V0UzetjkwCNbBkl_xNjRJF9cR_klTTyfUich0H8LDhUb-1koqHJLpPMK3MfIhKDAbyKArhzSXYLGs7cNI_o9LaD7bN4cxHq21SWF-ub1hTL7q_SGW6zHGjzohLUSUuwFIa-l5BSFc1otkY-bFVBTlnAT2LFb6DPNjfaLSa9idbXCk-hthCH6WJZ7lD2eAqbG7SPKIV60OAEhIQRSrN70MCeQmbRhivNjbDyhpC7UMWYloFXltc3b-3wt6YT-uyEr89rBkA5vIA5o-AC2dYmHWZ2Vy4bWxKaX6I31worvQkEiZ21J_7rFRQOixvKTvti0wgtC9GhTi3fRoaNxt2yrP94_BNlS0MMU4dZloIWRerBzSc-2PsojQGEmeAD_s-JieoVyR3uhTjzJ9tRlW4Xnjt2vr94iy2ZD_5yV_tPqsXH8NHbJWMewTl76C0hXXYKPey-caCNyS7EEcGo3vDgYqtD5t6V","expiry_time":1658142612} \ No newline at end of file diff --git a/src/config.php b/src/config.php new file mode 100644 index 0000000..945f76e --- /dev/null +++ b/src/config.php @@ -0,0 +1,5 @@ +