diff --git a/api/IsotopeDatabaseHandler.php b/api/IsotopeDatabaseHandler.php index 6fa17ae..0a491a9 100644 --- a/api/IsotopeDatabaseHandler.php +++ b/api/IsotopeDatabaseHandler.php @@ -71,8 +71,6 @@ class IsotopeDatabaseHandler $sql = "UPDATE `tl_iso_product` SET tstamp = ".time().", - name = '".$product->getDescription()."', - description = '".$product->getExtraDescription()."', shipping_weight = '$shippingWeight' WHERE `id` = ".$productDbId; $this->dbHandle->query($sql); diff --git a/api/Product.php b/api/Product.php index bdc5e5f..6ca9620 100644 --- a/api/Product.php +++ b/api/Product.php @@ -1,6 +1,6 @@ - * @date 12.07.22 - */ namespace App\EventListener; + use Contao\CoreBundle\ServiceAnnotation\Hook; use Contao\FrontendTemplate; use Contao\Module; +use App\ExactApi\ApiExact; +use Isotope\Model\Address; +use Isotope\Model\Product; +use Isotope\Model\ProductCollection\Order; +use Isotope\Model\ProductCollectionItem; class PostCheckoutListener { + const WAREHOUSE_ID = 'd8a6a9b8-d0ac-4d36-8d00-bd420d0d81f5'; + /** * @Hook("postCheckout") */ - public function __invoke(int $userId, array $userData, Module $module): void + // public function __invoke(int $userId, array $userData, Module $module): void + public function __invoke(Order $order, $item): void + { + $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); + + } + //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)); + + } + + private function compareAddresses(Order $order) { - throw new \Exception('ARRRRRRRGGGGGGGGGG'); + return + $order->getBillingAddress()->firstname === $order->getShippingAddress()->firstname && + $order->getBillingAddress()->lastname === $order->getShippingAddress()->lastname && + $order->getBillingAddress()->street_1 === $order->getShippingAddress()->street_1 && + $order->getBillingAddress()->postal === $order->getShippingAddress()->postal && + $order->getBillingAddress()->city === $order->getShippingAddress()->city && + $order->getBillingAddress()->company === $order->getShippingAddress()->company && + $order->getBillingAddress()->phone === $order->getShippingAddress()->phone && + $order->getBillingAddress()->email === $order->getShippingAddress()->email; + } + + private function createCustomerApiData(Address $address) + { + return [ + 'Name' => $address->firstname . ' ' . $address->lastname, + 'AddressLine1' => $address->street_1, + 'AddressLine2' => $address->company, + 'City' => $address->city, + 'Postcode' => $address->postal, + 'Email' => $address->email, + 'Country' => 'DE', + 'Phone' => $address->phone, + 'Status' => 'C', + ]; + } + + private function createOrderItem(ProductCollectionItem $orderItem) + { + /** @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 + ]; + } + + private function createSalesOrderData(Order $order, $billingCustomer, $salesOrderItems, $shippingCustomer = null) + { + $date = new \DateTime('now'); + $res = [ + 'Description' => $billingCustomer['Name'], + 'OrderDate' => $date->format('m/d/Y H:i:s'), + 'OrderedBy' => $billingCustomer['ID'], + 'WarehouseID' => self::WAREHOUSE_ID, + 'SalesOrderLines' => [ + $salesOrderItems + ] + ]; + if ($shippingCustomer !== null) { + $res['DeliverTo'] = $shippingCustomer['ID']; + } + + return $res; } } \ No newline at end of file diff --git a/api/listener/tokenData b/api/listener/tokenData new file mode 100644 index 0000000..f708ca3 --- /dev/null +++ b/api/listener/tokenData @@ -0,0 +1 @@ +{"access_token":"stampDE001.gAAAADRG83iY-fhuf-2rSWvPzCjE4DL5hlcUcyDpkEk4rP6B5FF9PnD3qKjveOit6vWjnxeM0UzXu1kUOX0cR0AV0jf16XSQy_-3nQ6XsTx5wIucuNmT52WpjP7-LU2E3TzSvjVilP-ufMtgTLnmuLvtC7ALRoAcoiNrHuU6sKACxco0VAIAAIAAAABYJx_THjmOsX2jvj-lBpx0NXCBxY4506xGpS8bmvY0JoYWsebqFuORCSupfTU3VebYy-Vn_oyv7nUkBwPpsmLUVmJWJB-VX5X9dTj23-tJMYkUxjNzvdrbbTdnwuNbIFYws_5bwsJ-1NiP9nZJYllsGstqbvUyfGmsnXrBSyX6eujzHH7eWOEfL7wHIfVbl2Eg2HwP1w3vMzUgG9h-P7OCw69-EpsfJVaOKYWL_F39zic6yBf0oWDSEBvsbIeL2o4KdkCSJD0NY0bqGViDfd_vJVfXP9lossW9fu-rtQNehko5GSkEybe4x30kDbUZHqB_LhL8a5_b9zqPF79-iDxN_vuY_FDCopvBBNUjcyeDGZcn_GDpzqEhK4BxIqJFWlw2umG4qd61OB3WkSXv952cPKBsVQJWoZWrudQymHa6WLy27DXNoyS3WJiO2VnyZybszl0Mk1G6_ZHso4PmgEsOdM2RyrsGaN7_pOoIyOG7c2ZlBLJTM_q_lDVVaAfPrNcrBon2UTFDqUtexlrTpt50nEjBUlxOyrDva1KOGyfHrHdMGEpetr6YoEmAbcMM6jId72OHLy2ztfoYL91kzXScnEAJB3PL3-btD85hU8rBaFYAgZemoPY0T4g6ZqBcYLXoMDT5ltW2McI74u819Q1V7ZrAPkVB2oICaFppnKzOe9ot7DChNk5nrNFUaK49hosQpPtTUzPyfPb2oX3Z5GhjHe2X4b6wT4DiSORY6gtu7ZdPyJeDWnoyCE_c1m9PLj_v4i6cyfWMJHbIaCHLuZGU","token_type":"bearer","expires_in":"600","refresh_token":"stampDE001.vhK0!IAAAAG4XGo_RV3d60tiy1gRnVAQrHp9xnEP4_CuBLQ_D5Z0C8QEAAAFnedzeXXOKto6NMohmWMQOWm7Qptgu2o0z6LYlWxTRWf9IPpRYC-fa5XigKwDbB5lquTf8gj90Fg0YkuniSwf9jIBUjBOzul0ZdAPWdolHZLAQtHQr3a53ylG4d7yLyrQCMctPl8yUHNWk1e5PH4YCOZH9lKTW7qWlt1SJAfLcgPa2F0I-9XnS-n-AZK7bxpCL4coztVGCA6vP9igYofpArxYc7W-9rNbzTOyF-fxp_hXzTtCaPetA5Zpg8v-4sx3Mvx1AY_NscwCcG-V1mJnolxlLowgL8fBeUdv9WrWrx5uHVqcKyhAYCB7BR_QqZfgl5mlv_62hbK2s_HKNcQsG591qymyG5KYjOlpSlKyEe3h-hGYaP3PaD-xiUx87C9Z3iRPKWE79i_9B6H4rCcltIJARKp3UGUJbJtYikzeexB7wCM3uHkcljXziwXkBO9PIVWTJhsauv7BTSAmyODuuSSbL1PTGHPIJJdRCQUn8U0xOlSvWWngpLD9CenpAiMRt8-uFfC3zAOBxIHZ5FNDuNlcvKhgEG0Q-94qFySKFt1J5xCUCKpKZ1VQyDz33uylXzb30SoGLmAIPVPhMFdJo08xIAFZaPRQKsvqOEj7M4t3KeGYkehyAv7ugQcJrlWo5TD9kYlHA1CRZs5YiK2a7","expiry_time":1657816808} \ No newline at end of file diff --git a/api/tokenData b/api/tokenData index 67a3864..c674512 100644 --- a/api/tokenData +++ b/api/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":1657726518} \ No newline at end of file +{"access_token":"stampDE001.gAAAAATOqLu_FvySDkT1YxBYQOSbyTqVC-jQWfCBmfomFxKEuiSUp2H5XYFwHOjuZ_cbURYObp_CuoijjuXPHYAvgR5GjxZVLpnI33116cIBqeKkdnQ2617l-DfjsiPlSO9ilA5CtrV75A1ZhxtWJqJ5uZynH0eT0ZB8LytI81Fg2p4tVAIAAIAAAAAqO6Xcq6ab64TEJDE2QyPHpfueoxrCkLlFVkxbiXgoANGrrSTu9qQtOTYyszkuc3sGZSdg1zD8eJLx8sL0zlTy7BYRYgTmiUhLZhECjG_7PfpObMuaz_Q4NOlIeyGOX23B6p-lbSZ_HHcVx1XzDgSry7MSIcFZGIqUWqr4jWW6EDqU0Q9A1ypbkKAPxh77V68LrHAzIm8yjoQell4GBnNSVOwNWKcLgmqkznUIsywr-Kq6dvfs_Ziy0nu5DR6PbpYLIVY4g1eCE4ncfgtPYsndKKuSIOisf8pYn_7NaLfSxvYWVwFxuTHw3roRufszbE-toAsUzfBVv2mQjKdyop9xRri3Adh3vfsEAKEyksZdsduCjFEPu6KoSv_pgQfExSURkHKj0gOX8_GjPtcbT4OWZ3JuZ7n9-4U5uSV-HHOvd7T2ojgQ0lFcVukuwHq086KTMNGzcBPfM-_vcBYAx1nRRpupQL2Np0fm85iycZwycSKAFydpcMeB1VaP5PYkVLiLOsXBPy4SMM0_-c51NykUm338m8SwAVeVQXN3VYcT0kJwUsyMdOtYoDvXUL-5p_NafKwwPwdrgpIwnjjO3s37O43RsGWaY8kp42y2I4lQbMwcqTde_bOcPCRQWium4OB__fkFaiCJ5eE5EGcbPUVzjEfW_Lhr8yrkRacFfut9YqCTVuGxZepi0eyX3FHk_-qAtXTBav11EZ40G5slokVlyPu63pQfhpSrsNIGZv-j5YIsN5MqjHn7HgtbS0UMRUAQERHvDkz6ddfbkqwjOgYE","token_type":"bearer","expires_in":"600","refresh_token":"stampDE001.vhK0!IAAAAIMZSdkYxeTLXpkXHeNZCQAgV_DZ3JM_sqN9Y81kYggu8QEAAAE1Ecbn6_tJNnyZQn4ktg02ypEuBII4NQc3bgVgWB1lM0IZaxRyKB5C9AVOCqh9ewKECN3eNMP0C0P2QVzNyoBsOHrK_QHqqypFPBfs2fPG2psKUngqf4nTWfGWpjnaeevGqll88v4_Cka58o-Kmlrv2Ho_uc3Tjl4gE6GeLF870gyWnW1Qx3AJUTx4730yswUr1YwEODVTl0WBSJ2LEqELKvx4MQ603b642QkevFCrH6ttZBef-ufEKcRpT7Fic9JHcSdUEJCGrhsGe__Ah0dS2WtxNfhD3m7YgGpxBqSjCJXWvXzBpBd9UsZ6vCuF0CP2EVvSZ_HS3iTgD5UOBDoQkTn8kt65TixrpQWZE5j2-vfgoQy9CbPv8aO75XT4gZGPtjij7GAdXNc17ZH-P_3N5CFmLnycGyfPNdnS58PP4nkYcl6LzVF5pBOBj2mIZa9Ic7W1fcSWDePaGC_WjVQvuV18tjvsMxp738XNTPdZfta5V769cSq1tuRblIyLYIJhhYhmprqg1KADYkcNC7RHYSVOHbds83VORW9gvgjFLCWt15TLhJaGbydgaMMUGHlZ4gb8YFMCzbRiq_20_K8iP_J5BdP2omIeHCNnH6360wmJybbgcFlYv0FZNDXUwkRxaTsREpfSvDUzZHCsk0Od","expiry_time":1657818416} \ No newline at end of file diff --git a/api2/Address.php b/api2/Address.php new file mode 100644 index 0000000..d9b5e6c --- /dev/null +++ b/api2/Address.php @@ -0,0 +1,441 @@ +generate(); + } catch (\Exception $e) { + return ''; + } + } + + /** + * Check if the address has a valid VAT number + * + * @param Config $config + * + * @return bool + * + * @throws \LogicException if a validator does not implement the correct interface + * @throws \RuntimeException if a validators reports an error about the VAT number + */ + public function hasValidVatNo(Config $config = null) + { + if (null === $config) { + $config = Isotope::getConfig(); + } + + $validators = StringUtil::deserialize($config->vatNoValidators); + + // if no validators are enabled, the VAT No is always valid + if (!\is_array($validators) || 0 === \count($validators)) { + return true; + } + + foreach ($validators as $class) { + $service = new $class(); + + if (!($service instanceof IsotopeVatNoValidator)) { + throw new \LogicException($class . ' does not implement IsotopeVatNoValidator interface'); + } + + $result = $service->validate($this); + + if (true === $result) { + return true; + } + } + + return false; + } + + /** + * Return formatted address (hCard) + * + * @param array $arrFields + * + * @return string + * + * @throws \Exception on error parsing simple tokens + */ + public function generate($arrFields = null) + { + // We need a country to format the address, use default country if none is available + $strCountry = $this->country ?: Isotope::getConfig()->country; + + // Use generic format if no country specific format is available + $strFormat = $GLOBALS['ISO_ADR'][$strCountry] ?? $GLOBALS['ISO_ADR']['generic']; + + $arrTokens = $this->getTokens($arrFields); + + return StringUtil::parseSimpleTokens($strFormat, $arrTokens); + } + + /** + * Return this address formatted as text + * + * @param array $arrFields + * + * @return string + * + * @deprecated use Address::generate() and strip_tags + * @throws \Exception on invalid simple tokens + */ + public function generateText($arrFields = null) + { + return strip_tags($this->generate($arrFields)); + } + + /** + * Return an address formatted with HTML (hCard) + * + * @param array $arrFields + * + * @return string + * + * @deprecated use Address::generate() + * @throws \Exception on invalid simple tokens + */ + public function generateHtml($arrFields = null) + { + return $this->generate($arrFields); + } + + /** + * Compile the list of hCard tokens for this address + * + * @param array $arrFields + * + * @return array + */ + public function getTokens($arrFields = null) + { + if (!\is_array($arrFields)) { + $arrFields = Isotope::getConfig()->getBillingFieldsConfig(); + } + + $arrTokens = array('outputFormat' => 'html'); + + foreach ($arrFields as $arrField) { + $strField = $arrField['value']; + + // Set an empty value for disabled fields, otherwise the token would not be replaced + if (!$arrField['enabled']) { + $arrTokens[$strField] = ''; + continue; + } + + if ('subdivision' === $strField && $this->subdivision != '') { + [$country, $subdivision] = explode('-', $this->subdivision); + + $arrTokens['subdivision_abbr'] = $subdivision; + $arrTokens['subdivision'] = Backend::getLabelForSubdivision($country, $this->subdivision); + + continue; + } + + $arrTokens[$strField] = Format::dcaValue(static::$strTable, $strField, $this->$strField); + } + + + /** + * Generate hCard fields + * See http://microformats.org/wiki/hcard + */ + + // Set "fn" (full name) to company if no first- and lastname is given + if ($arrTokens['company'] != '') { + $fn = $arrTokens['company']; + $fnCompany = ' fn'; + } else { + $fn = trim($arrTokens['firstname'] . ' ' . $arrTokens['lastname']); + $fnCompany = ''; + } + + $street = implode('
', array_filter([$this->street_1, $this->street_2, $this->street_3])); + + $arrTokens += [ + 'hcard_fn' => $fn ? '' . $fn . '' : '', + 'hcard_n' => ($arrTokens['firstname'] || $arrTokens['lastname']) ? '1' : '', + 'hcard_honorific_prefix' => $arrTokens['salutation'] ? '' . $arrTokens['salutation'] . '' : '', + 'hcard_given_name' => $arrTokens['firstname'] ? '' . $arrTokens['firstname'] . '' : '', + 'hcard_family_name' => $arrTokens['lastname'] ? '' . $arrTokens['lastname'] . '' : '', + 'hcard_org' => $arrTokens['company'] ? '
' . $arrTokens['company'] . '
' : '', + 'hcard_email' => $arrTokens['email'] ? '' . $arrTokens['email'] . '' : '', + 'hcard_tel' => $arrTokens['phone'] ? '
' . $arrTokens['phone'] . '
' : '', + 'hcard_adr' => ($street || $arrTokens['city'] || $arrTokens['postal'] || $arrTokens['subdivision'] || $arrTokens['country']) ? '1' : '', + 'hcard_street_address' => $street ? '
' . $street . '
' : '', + 'hcard_locality' => $arrTokens['city'] ? '' . $arrTokens['city'] . '' : '', + 'hcard_region' => $arrTokens['subdivision'] ? '' . $arrTokens['subdivision'] . '' : '', + 'hcard_region_abbr' => !empty($arrTokens['subdivision_abbr']) ? '' . $arrTokens['subdivision_abbr'] . '' : '', + 'hcard_postal_code' => $arrTokens['postal'] ? '' . $arrTokens['postal'] . '' : '', + 'hcard_country_name' => $arrTokens['country'] ? '
' . $arrTokens['country'] . '
' : '', + ]; + + return $arrTokens; + } + + /** + * Find address for member, automatically checking the current store ID and tl_member parent table + * + * @param int $intMember + * @param array $arrOptions + * + * @return \Model\Collection|null + */ + public static function findForMember($intMember, array $arrOptions = array()) + { + return static::findBy( + array('pid=?', 'ptable=?', 'store_id=?'), + array($intMember, 'tl_member', Isotope::getCart()->store_id), + $arrOptions + ); + } + + /** + * Find address by ID and member, automatically checking the current store ID and tl_member parent table + * + * @param int $intId + * @param int $intMember + * @param array $arrOptions + * + * @return Address|null + */ + public static function findOneForMember($intId, $intMember, array $arrOptions = array()) + { + return static::findOneBy( + array('id=?', 'pid=?', 'ptable=?', 'store_id=?'), + array($intId, $intMember, 'tl_member', Isotope::getCart()->store_id), + $arrOptions + ); + } + + /** + * Find default billing adddress for a member, automatically checking the current store ID and tl_member parent table + * @param int + * @param array + * @return static|null + */ + public static function findDefaultBillingForMember($intMember, array $arrOptions = array()) + { + return static::findOneBy( + array('pid=?', 'ptable=?', 'store_id=?', 'isDefaultBilling=?'), + array($intMember, 'tl_member', Isotope::getCart()->store_id, '1'), + $arrOptions + ); + } + + /** + * Find default shipping adddress for a member, automatically checking the current store ID and tl_member parent table + * @param int + * @param array + * @return static|null + */ + public static function findDefaultShippingForMember($intMember, array $arrOptions = array()) + { + return static::findOneBy(array('pid=?', 'ptable=?', 'store_id=?', 'isDefaultShipping=?'), array($intMember, 'tl_member', Isotope::getCart()->store_id, '1'), $arrOptions); + } + + /** + * Find default billing address for a product collection + * + * @param int $intCollection + * @param array $arrOptions + * + * @return static|null + */ + public static function findDefaultBillingForProductCollection($intCollection, array $arrOptions = array()) + { + return static::findOneBy( + array('pid=?', 'ptable=?', 'isDefaultBilling=?'), + array($intCollection, 'tl_iso_product_collection', '1'), + $arrOptions + ); + } + + /** + * Find default shipping address for a product collection + * + * @param int $intCollection + * @param array $arrOptions + * + * @return static|null + */ + public static function findDefaultShippingForProductCollection($intCollection, array $arrOptions = array()) + { + return static::findOneBy( + array('pid=?', 'ptable=?', 'isDefaultShipping=?'), + array($intCollection, 'tl_iso_product_collection', '1'), + $arrOptions + ); + } + + /** + * Create a new address for a member and automatically set default properties + * + * @param int $intMember + * @param array|null $arrFill + * + * @return static + */ + public static function createForMember($intMember, $arrFill = null) + { + $objAddress = new static(); + + $arrData = array( + 'pid' => $intMember, + 'ptable' => 'tl_member', + 'tstamp' => time(), + 'store_id' => (int) Isotope::getCart()->store_id, + ); + + if (!empty($arrFill) && \is_array($arrFill) && ($objMember = MemberModel::findByPk($intMember)) !== null) { + $arrData = array_merge(static::getAddressDataForMember($objMember, $arrFill), $arrData); + } + + $objAddress->setRow($arrData); + + return $objAddress; + } + + /** + * Create a new address for a product collection + * + * @param IsotopeProductCollection $objCollection + * @param array|null $arrFill an array of member fields to inherit + * @param bool $blnDefaultBilling + * @param bool $blnDefaultShipping + * + * @return static + */ + public static function createForProductCollection( + IsotopeProductCollection $objCollection, + $arrFill = null, + $blnDefaultBilling = false, + $blnDefaultShipping = false + ) { + $objAddress = new static(); + + $arrData = array( + 'pid' => $objCollection->getId(), + 'ptable' => 'tl_iso_product_collection', + 'tstamp' => time(), + 'store_id' => $objCollection->getStoreId(), + 'isDefaultBilling' => $blnDefaultBilling ? '1' : '', + 'isDefaultShipping' => $blnDefaultShipping ? '1' : '', + ); + + if (!empty($arrFill) && \is_array($arrFill) && ($objMember = $objCollection->getMember()) !== null) { + $arrData = array_merge(static::getAddressDataForMember($objMember, $arrFill), $arrData); + } + + if (empty($arrData['country']) && null !== ($objConfig = $objCollection->getConfig())) { + if ($blnDefaultBilling) { + $arrData['country'] = $objConfig->billing_country ?: $objConfig->country; + } elseif ($blnDefaultShipping) { + $arrData['country'] = $objConfig->shipping_country ?: $objConfig->country; + } + } + + $objAddress->setRow($arrData); + + return $objAddress; + } + + /** + * Generate address data from tl_member, limit to fields enabled in the shop configuration + */ + public static function getAddressDataForMember(MemberModel $member, array $fields) + { + return array_intersect_key( + array_merge( + $member->row(), + array( + 'street_1' => $member->street, + + // Trying to guess subdivision by country and state + 'subdivision' => strtoupper($member->country . '-' . $member->state) + ) + ), + array_flip($fields) + ); + } +} diff --git a/api2/ApiExact.php b/api2/ApiExact.php new file mode 100644 index 0000000..92852cd --- /dev/null +++ b/api2/ApiExact.php @@ -0,0 +1,178 @@ +expiry_time < time()) { + $postData = array( + 'grant_type' => 'refresh_token', + 'refresh_token' => $tokenData->refresh_token, + 'client_id' => self::CLIENT_ID, + 'client_secret' => self::CLIENT_SECRET + ); + + $ch = curl_init(); + curl_setopt_array($ch, array( + CURLOPT_URL => self::API_URL . '/oauth2/token', + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_HTTPHEADER => array('Content-Type: application/x-www-form-urlencoded'), + CURLOPT_POSTFIELDS => http_build_query($postData) + )); + + $response = curl_exec($ch); + curl_close($ch); + $jsonResult = json_decode($response); + + if (property_exists($jsonResult, 'error')) { + throw new \Exception($jsonResult); + } + $jsonResult->expiry_time = time() + self::ACCESS_TOKEN_VALID_DURATION; + file_put_contents('./tokenData', json_encode($jsonResult)); + + $tokenData = json_decode(file_get_contents('./tokenData', true)); + } + return $tokenData->access_token; + } + + public function getProducts() + { + $baseUrl = self::API_URL_WNR . "/bulk/Logistics/Items?"; + $fields = [ + 'ID', + 'StandardSalesPrice', + 'SalesVatCode', + 'SalesVatCodeDescription', + 'Description', + 'ExtraDescription', + 'IsWebshopItem', + 'ItemGroup', + 'ItemGroupCode', + 'Stock', + 'NetWeight', + 'GrossWeight', + 'Code' + ]; + $filter = "\$filter=ItemGroup eq guid'".self::ITEM_GROUP_UUID."'"; + $select = "&\$select=".$this->getFieldString($fields); + $requestUrl = $baseUrl . $filter . $select; + $response = $this->getApiData($requestUrl); + $responseArr = json_decode($response, 1)['d']['results']; + + $res = []; + foreach ($responseArr as $productItem) { + if ( + array_key_exists('IsWebshopItem', $productItem) && + $productItem['IsWebshopItem'] === 1 && + array_key_exists('Code', $productItem) && + stripos(strtoupper($productItem['Code']), self::PRODUCT_CODE_PREFIX) === 0 + ) { + $res[] = Product::createProductFromApiItem($productItem); + } + } + return $res; + } + + public function createCustomer($customerData) + { + $url = self::API_URL_WNR . "/crm/Accounts"; + return json_decode($this->postApiData($url, $customerData)); + } + + 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)); + } + + private function getApiData($url) + { + $ch = curl_init(); + curl_setopt_array($ch, array( + CURLOPT_URL => $url, + CURLOPT_HTTPHEADER => array('Accept: application/json' , "Authorization: Bearer " . $this->getAccessToken() ), + CURLOPT_RETURNTRANSFER => TRUE, + )); + $res = curl_exec($ch); + curl_close($ch); + return $res; + } + + private function postApiData($url, $data) + { + $ch = curl_init(); + curl_setopt_array($ch, array( + CURLOPT_URL => $url, + CURLOPT_HTTPHEADER => array( + 'Content-Type: application/json', + 'Accept: application/json', + "Authorization: Bearer " . $this->getAccessToken() ), + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => json_encode($data) + )); + $res = curl_exec($ch); + curl_close($ch); + return $res; + } + + private function getFieldString($fields) + { + $res = ''; + $cnt = count($fields); + $i = 1; + foreach ($fields as $field) { + $res .= $cnt !== $i ? $field . ',' : $field; + $i++; + } + return $res; + } + + public function test() + { + return 'fuck off contao'; + } +} \ No newline at end of file diff --git a/api2/IsotopeDatabaseHandler.php b/api2/IsotopeDatabaseHandler.php new file mode 100644 index 0000000..559065c --- /dev/null +++ b/api2/IsotopeDatabaseHandler.php @@ -0,0 +1,197 @@ +dbHandle = new PDO('mysql:host='.self::HOST.';dbname='.self::DB_NAME.';charset=utf8',self::USER, self::PASSWORD); + $this->dbHandle->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + } catch (PDOException $e) { + print "Error!: " . $e->getMessage() . "
"; + die(); + } + } + + public function processProducts(array $products) + { + $this->dbHandle->beginTransaction(); + + $sql = "SELECT * FROM `tl_iso_product`"; + $stmt = $this->dbHandle->query($sql); + $dbProducts = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $sql = "SELECT * FROM `tl_iso_product_price`"; + $stmt = $this->dbHandle->query($sql); + $dbProductPrices = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $dbProductsBySku = []; + foreach ($dbProducts as $prod) { + $dbProductsBySku[$prod['sku']] = $prod; + } + + $dbProductsPricesByPid = []; + foreach ($dbProductPrices as $price) { + $dbProductsPricesByPid[$price['pid']] = $price; + } + + /** @var Product $product */ + foreach ($products as $product) { + + if (array_key_exists($product->getCode(), $dbProductsBySku)) { + $dbProduct = $dbProductsBySku[$product->getCode()]; + $dbPrice = $dbProductsPricesByPid[$dbProduct['id']]; + $this->updateProduct($product, $dbProduct['id']); + $this->updatePriceTier($product, $dbPrice['id']); + + } else { + $productId = $this->createProduct($product); + $productPriceId = $this->createPrice($product, $productId); + $this->createPriceTier($product, $productPriceId); + } + } + $this->dbHandle->commit(); + } + + private function updateProduct(Product $product, $productDbId) + { + $shippingWeight = $product->getGrossWeight() !== '' ? + ($product->getNetWeight() !== '' ? $product->getNetWeight() : '') : ''; + + $sql = + "UPDATE `tl_iso_product` SET + tstamp = ".time().", + shipping_weight = '$shippingWeight' + WHERE `id` = ".$productDbId; + $this->dbHandle->query($sql); + } + + private function updatePriceTier(Product $product, $priceDbId) + { + $price = $product->getStandardSalesPrice() * (1 + Product::VAT); + $sql = + "UPDATE `tl_iso_product_pricetier` SET + tstamp = ".time().", + price = $price + WHERE `pid` = ".$priceDbId; + $this->dbHandle->query($sql); + } + + private function createProduct(Product $product) + { + $alias = 'exact-product-'.$product->getCode(); + $shippingWeight = $product->getGrossWeight() !== '' ? + ($product->getNetWeight() !== '' ? $product->getNetWeight() : '') : ''; + $sql = + "INSERT into `tl_iso_product` ( + pid, + gid, + tstamp, + language, + dateAdded, + type, + fallback, + alias, + gtin, + sku, + name, + description, + meta_title, + baseprice, + shipping_weight, + shipping_exempt, + shipping_pickup, + shipping_price, + protected, + guests, + cssID, + published, + start, + stop, + exact_id + ) VALUES ( + 0, + 0, + ".time().", + '', + ".time().", + 2, + '', + '$alias', + '', + '".$product->getCode()."', + '".$product->getDescription()."', + '".$product->getExtraDescription()."', + '', + '', + '$shippingWeight', + '', + '', + 0.00, + '', + '', + '', + '0', + '', + '', + '".$product->getId()."' + )"; + $this->dbHandle->query($sql); + return $this->dbHandle->lastInsertId(); + } + + private function createPrice(Product $product, $productDbId) + { + $sql = + "INSERT INTO `tl_iso_product_price` ( + pid, + tstamp, + tax_class, + config_id, + member_group, + start, + stop + ) VALUES ( + $productDbId, + ".time().", + 1, + 0, + 0, + '', + '' + )"; + $this->dbHandle->query($sql); + return $this->dbHandle->lastInsertId(); + } + + private function createPriceTier(Product $product, $productPriceDbId) + { + $price = $product->getStandardSalesPrice() * (1 + Product::VAT); + $sql = + "INSERT INTO `tl_iso_product_pricetier` ( + pid, + tstamp, + min, + price + ) VALUES ( + $productPriceDbId, + ".time().", + 1, + $price + )"; + $this->dbHandle->query($sql); + return $this->dbHandle->lastInsertId(); + } +} \ No newline at end of file diff --git a/api2/Order.php b/api2/Order.php new file mode 100644 index 0000000..3ddd429 --- /dev/null +++ b/api2/Order.php @@ -0,0 +1,614 @@ +date_paid && $this->date_paid <= time()) { + return true; + } + + // Otherwise we check the orderstatus checkbox + try { + /** @var OrderStatus $objStatus */ + $objStatus = $this->getRelated('order_status'); + + return (null !== $objStatus && $objStatus->isPaid()); + } catch (\Exception $e) { + return false; + } + } + + /** + * @inheritdoc + */ + public function getDatePaid() + { + return $this->date_paid; + } + + /** + * @inheritdoc + */ + public function setDatePaid($timestamp = null) + { + $this->date_paid = $timestamp; + } + + /** + * @inheritdoc + */ + public function isShipped() + { + return null !== $this->date_shipped; + } + + /** + * @inheritdoc + */ + public function getDateShipped() + { + return $this->date_shipped; + } + + /** + * @inheritdoc + */ + public function setDateShipped($timestamp = null) + { + $this->date_shipped = $timestamp; + } + + /** + * Returns true if checkout has been completed + * + * @return bool + */ + public function isCheckoutComplete() + { + return (bool) $this->checkout_complete; + } + + /** + * Get label for current order status + * + * @return string + */ + public function getStatusLabel() + { + try { + /** @var OrderStatus $objStatus */ + $objStatus = $this->getRelated('order_status'); + + return (null === $objStatus) ? '' : $objStatus->getName(); + } catch (\Exception $e) { + return ''; + } + } + + /** + * Get the alias for current order status + * + * @return string + */ + public function getStatusAlias() + { + try { + /** @var OrderStatus $objStatus */ + $objStatus = $this->getRelated('order_status'); + + return null === $objStatus ? $this->order_status : $objStatus->getAlias(); + } catch (\Exception $e) { + return $this->order_status; + } + } + + /** + * Process the order checkout + * + * @return bool + */ + public function checkout() + { + if ($this->isCheckoutComplete()) { + return true; + } + + // Finish and lock the order + // (do this now, because otherwise surcharges etc. will not be loaded form the database) + $this->checkout_complete = true; + $this->generateDocumentNumber( + $this->getConfig()->orderPrefix, + (int) $this->getConfig()->orderDigits + ); + + if (!$this->isLocked()) { + $this->lock(); + } + + System::log('New order ID ' . $this->id . ' has been placed', __METHOD__, TL_ACCESS); + + // Delete cart after migrating to order + if (($objCart = Cart::findByPk($this->source_collection_id)) !== null) { + $objCart->delete(); + } + + // Delete all other orders that relate to the current cart + if (($objOrders = static::findSiblingsBy('source_collection_id', $this)) !== null) { + + /** @var static $objOrder */ + foreach ($objOrders as $objOrder) { + if (!$objOrder->isCheckoutComplete()) { + $objOrder->delete(true); + } + } + } + + $notificationIds = array_filter(explode(',', $this->nc_notification)); + + // Send the notifications + if (\count($notificationIds) > 0) { + foreach ($notificationIds as $notificationId) { + // Generate tokens + $arrTokens = $this->getNotificationTokens($notificationId); + + // Send notification + $blnNotificationError = true; + + /** @var Notification $objNotification */ + if (($objNotification = Notification::findByPk($notificationId)) !== null) { + $arrResult = $objNotification->send($arrTokens, $this->language); + + if (\count($arrResult) > 0 && !\in_array(false, $arrResult, true)) { + $blnNotificationError = false; + } + } + + if ($blnNotificationError === true) { + System::log('Error sending new order notification for order ID ' . $this->id, __METHOD__, TL_ERROR); + } + } + } + + // Set order status only if a payment module has not already set it + if ($this->order_status == 0) { + $this->updateOrderStatus($this->getRelated('config_id')->orderstatus_new); + } + + // !HOOK: post-process checkout + if (isset($GLOBALS['ISO_HOOKS']['postCheckout']) && \is_array($GLOBALS['ISO_HOOKS']['postCheckout'])) { + // Generate the default notification tokens if none set yet + if (!isset($arrTokens)) { + $arrTokens = $this->getNotificationTokens(0); + } + + foreach ($GLOBALS['ISO_HOOKS']['postCheckout'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($this, $arrTokens); + } + } + + return true; + } + + /** + * Complete order if the checkout has been made. This will cleanup session data + * + * @return bool + */ + public function complete() + { + if ($this->isCheckoutComplete()) { + unset($_SESSION['CHECKOUT_DATA'], $_SESSION['FILES']); + + // Retain custom config ID + if (($objCart = Isotope::getCart()) !== null && $objCart->config_id != $this->config_id) { + $objCart->config_id = $this->config_id; + } + + return true; + } + + return false; + } + + /** + * Update the status of this order and trigger actions (email & hook) + * + * @param int|array $updates + * @param int $flags Order::STATUS_UPDATE_SKIP_NOTIFICATION and/or Order::STATUS_UPDATE_SKIP_LOG + * + * @return bool + */ + public function updateOrderStatus($updates, $flags = 0) + { + // For BC reasons the parameter can be the new order status ID + if (!is_array($updates)) { + $updates = ['order_status' => $updates]; + } + + $previous = []; + $hasChanges = false; + foreach ($updates as $k => $v) { + $previous[$k] = $this->{$k}; + $hasChanges = $hasChanges ?: $v !== $previous[$k]; + } + + if (!isset($updates['order_status'])) { + throw new \LogicException('You must update the order status when calling Order::updateOrderStatus()'); + } + + if (!$hasChanges) { + return true; + } + + /** @var OrderStatus $objNewStatus */ + $objNewStatus = OrderStatus::findByPk($updates['order_status']); + + if (null === $objNewStatus) { + return false; + } + + // !HOOK: allow to cancel a status update + if (isset($GLOBALS['ISO_HOOKS']['preOrderStatusUpdate']) + && \is_array($GLOBALS['ISO_HOOKS']['preOrderStatusUpdate']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['preOrderStatusUpdate'] as $callback) { + $blnCancel = System::importStatic($callback[0])->{$callback[1]}($this, $objNewStatus, $updates); + + if ($blnCancel === true) { + return false; + } + } + } + + // Add the payment date if there is none + if ($objNewStatus->isPaid() && $this->date_paid == '' && !isset($updates['date_paid'])) { + $updates['date_paid'] = time(); + } + + // Store old status and set the new one + $oldStatusId = $this->order_status; + $oldStatusLabel = $this->getStatusLabel(); + foreach ($updates as $k => $v) { + $this->{$k} = $v; + } + $this->save(); + + if (!($flags & static::STATUS_UPDATE_SKIP_NOTIFICATION)) { + // Trigger notification + $blnNotificationError = null; + foreach (array_filter(explode(',', $objNewStatus->notification)) as $notificationId) { + $arrTokens = $this->getNotificationTokens($notificationId); + + // Override order status and save the old one to the tokens too + $arrTokens['order_status_id'] = $objNewStatus->id; + $arrTokens['order_status'] = $objNewStatus->getName(); + $arrTokens['order_status_old'] = $oldStatusLabel; + $arrTokens['order_status_id_old'] = $oldStatusId; + + $blnNotificationError = true; + + /** @var Notification $objNotification */ + if (($objNotification = Notification::findByPk($notificationId)) !== null) { + $arrResult = $objNotification->send($arrTokens, $this->language); + + if (\in_array(false, $arrResult, true)) { + $blnNotificationError = true; + System::log( + 'Error sending status update notification for order ID ' . $this->id, + __METHOD__, + TL_ERROR + ); + } elseif (\count($arrResult) > 0) { + $blnNotificationError = false; + } + } else { + System::log('Invalid notification for order status ID ' . $objNewStatus->id, __METHOD__, TL_ERROR); + } + } + + if ('BE' === TL_MODE) { + Message::addConfirmation($GLOBALS['TL_LANG']['tl_iso_product_collection']['orderStatusUpdate']); + + if ($blnNotificationError === true) { + Message::addError($GLOBALS['TL_LANG']['tl_iso_product_collection']['orderStatusNotificationError']); + } elseif ($blnNotificationError === false) { + Message::addConfirmation($GLOBALS['TL_LANG']['tl_iso_product_collection']['orderStatusNotificationSuccess']); + } + } + } + + // Add a log entry + if (!($flags & static::STATUS_UPDATE_SKIP_LOG)) { + $log = new ProductCollectionLog(); + $log->pid = $this->id; + $log->tstamp = time(); + $log->setData([ + 'order_status' => $this->order_status, + 'date_paid' => $this->date_paid, + 'date_shipped' => $this->date_shipped, + ]); + $log->save(); + } + + // !HOOK: order status has been updated + if (isset($GLOBALS['ISO_HOOKS']['postOrderStatusUpdate']) + && \is_array($GLOBALS['ISO_HOOKS']['postOrderStatusUpdate']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['postOrderStatusUpdate'] as $callback) { + System::importStatic($callback[0])->{$callback[1]}($this, $oldStatusId, $objNewStatus); + } + } + + // Trigger payment and shipping methods that implement the interface + if (($objPayment = $this->getPaymentMethod()) !== null && $objPayment instanceof IsotopeOrderStatusAware) { + $objPayment->onOrderStatusUpdate($this, $oldStatusId, $objNewStatus); + } + if (($objShipping = $this->getShippingMethod()) !== null && $objShipping instanceof IsotopeOrderStatusAware) { + $objShipping->onOrderStatusUpdate($this, $oldStatusId, $objNewStatus); + } + + return true; + } + + /** + * Retrieve the array of notification data for parsing simple tokens + * + * @param int $intNotification + * + * @return array + */ + public function getNotificationTokens($intNotification) + { + $objConfig = $this->getRelated('config_id') ?: Isotope::getConfig(); + Isotope::setConfig($objConfig); + + $arrTokens = StringUtil::deserialize($this->email_data, true); + $arrTokens['uniqid'] = $this->uniqid; + $arrTokens['order_status_id'] = $this->order_status; + $arrTokens['order_status'] = $this->getStatusLabel(); + $arrTokens['recipient_email'] = $this->getEmailRecipient(); + $arrTokens['order_id'] = $this->id; + $arrTokens['order_items'] = $this->sumItemsQuantity(); + $arrTokens['order_products'] = $this->countItems(); + $arrTokens['order_subtotal'] = Isotope::formatPriceWithCurrency($this->getSubtotal(), false, $objConfig->currency); + $arrTokens['order_total'] = Isotope::formatPriceWithCurrency($this->getTotal(), false, $objConfig->currency); + $arrTokens['document_number'] = $this->document_number; + $arrTokens['bank_name'] = $objConfig->bankName; + $arrTokens['bank_account'] = $objConfig->bankAccount; + $arrTokens['bank_code'] = $objConfig->bankCode; + $arrTokens['tax_number'] = $objConfig->taxNumber; + $arrTokens['cart_html'] = ''; + $arrTokens['cart_text'] = ''; + $arrTokens['document'] = ''; + + // Add all the collection fields + foreach ($this->row() as $k => $v) { + $arrTokens['collection_' . $k] = $v; + } + + // Add billing/customer address fields + if (($objAddress = $this->getBillingAddress()) !== null) { + foreach ($objAddress->row() as $k => $v) { + $arrTokens['billing_address_' . $k] = Format::dcaValue(Address::getTable(), $k, $v); + + // @deprecated (use ##billing_address_*##) + $arrTokens['billing_' . $k] = $arrTokens['billing_address_' . $k]; + } + + $arrTokens['billing_address'] = $objAddress->generate($objConfig->getBillingFieldsConfig()); + + // @deprecated (use ##billing_address##) + $arrTokens['billing_address_text'] = $arrTokens['billing_address']; + } + + // Add shipping address fields + if (($objAddress = $this->getShippingAddress()) !== null) { + foreach ($objAddress->row() as $k => $v) { + $arrTokens['shipping_address_' . $k] = Format::dcaValue(Address::getTable(), $k, $v); + + // @deprecated (use ##billing_address_*##) + $arrTokens['shipping_' . $k] = $arrTokens['shipping_address_' . $k]; + } + + $arrTokens['shipping_address'] = $objAddress->generate($objConfig->getShippingFieldsConfig()); + + // Shipping address equals billing address + // @deprecated (use ##shipping_address##) + if ($objAddress->id == $this->getBillingAddress()->id) { + $arrTokens['shipping_address_text'] = ($this->requiresPayment() ? $GLOBALS['TL_LANG']['MSC']['useBillingAddress'] : $GLOBALS['TL_LANG']['MSC']['useCustomerAddress']); + } else { + $arrTokens['shipping_address_text'] = $arrTokens['shipping_address']; + } + } + + // Add payment method info + if ($this->hasPayment() && ($objPayment = $this->getPaymentMethod()) !== null) { + $arrTokens['payment_id'] = $objPayment->getId(); + $arrTokens['payment_label'] = $objPayment->getLabel(); + $arrTokens['payment_note'] = $objPayment->getNote(); + + if ($objPayment instanceof IsotopeNotificationTokens) { + $arrTokens = array_merge($arrTokens, $objPayment->getNotificationTokens($this)); + } + } + + // Add shipping method info + if ($this->hasShipping() && ($objShipping = $this->getShippingMethod()) !== null) { + $arrTokens['shipping_id'] = $objShipping->getId(); + $arrTokens['shipping_label'] = $objShipping->getLabel(); + $arrTokens['shipping_note'] = $objShipping->getNote(); + + if ($objShipping instanceof IsotopeNotificationTokens) { + $arrTokens = array_merge($arrTokens, $objShipping->getNotificationTokens($this)); + } + } + + // Add config fields + if ($this->getRelated('config_id') !== null) { + foreach ($this->getRelated('config_id')->row() as $k => $v) { + $arrTokens['config_' . $k] = Format::dcaValue($this->getRelated('config_id')->getTable(), $k, $v); + } + } + + // Add member fields + if ($this->member > 0 && $this->getRelated('member') !== null) { + foreach ($this->getRelated('member')->row() as $k => $v) { + $arrTokens['member_' . $k] = Format::dcaValue($this->getRelated('member')->getTable(), $k, $v); + } + } + + /** @var Notification|object $objNotification */ + if ($intNotification > 0 && ($objNotification = Notification::findByPk($intNotification)) !== null) { + /** @var \Isotope\Template|object $objTemplate */ + $objTemplate = new \Isotope\Template($objNotification->iso_collectionTpl); + $objTemplate->isNotification = true; + + $this->addToTemplate( + $objTemplate, + array( + 'gallery' => $objNotification->iso_gallery, + 'sorting' => static::getItemsSortingCallable($objNotification->iso_orderCollectionBy), + ) + ); + + $arrTokens['cart_html'] = Controller::replaceInsertTags($objTemplate->parse(), false); + $objTemplate->textOnly = true; + $arrTokens['cart_text'] = strip_tags(Controller::replaceInsertTags($objTemplate->parse(), true)); + + // Generate and "attach" document + /** @var \Isotope\Interfaces\IsotopeDocument $objDocument */ + if ($objNotification->iso_document > 0 + && (($objDocument = Document::findByPk($objNotification->iso_document)) !== null) + ) { + $strFilePath = $objDocument->outputToFile($this, TL_ROOT . '/system/tmp'); + $arrTokens['document'] = str_replace(TL_ROOT . '/', '', $strFilePath); + } + } + + // !HOOK: add custom email tokens + if (isset($GLOBALS['ISO_HOOKS']['getOrderNotificationTokens']) + && \is_array($GLOBALS['ISO_HOOKS']['getOrderNotificationTokens']) + ) { + foreach ($GLOBALS['ISO_HOOKS']['getOrderNotificationTokens'] as $callback) { + $arrTokens = System::importStatic($callback[0])->{$callback[1]}($this, $arrTokens); + } + } + + return $arrTokens; + } + + /** + * Include downloads when adding items to template + * + * @return array + */ + protected function addItemsToTemplate(Template $objTemplate, $varCallable = null) + { + $taxIds = []; + $arrItems = []; + $arrAllDownloads = []; + + foreach ($this->getItems($varCallable) as $objItem) { + $arrDownloads = []; + $arrItem = $this->generateItem($objItem); + + foreach ($objItem->getDownloads() as $objDownload) { + $arrDownloads = array_merge($arrDownloads, $objDownload->getForTemplate($this->isPaid(), $this->orderdetails_page)); + } + + $arrItem['downloads'] = $arrDownloads; + $arrAllDownloads = array_merge($arrAllDownloads, $arrDownloads); + + $taxIds[] = $arrItem['tax_id']; + $arrItems[] = $arrItem; + } + + RowClass::withKey('rowClass')->addCount('row_')->addFirstLast('row_')->addEvenOdd('row_')->applyTo($arrItems); + + $objTemplate->items = $arrItems; + $objTemplate->downloads = $arrAllDownloads; + $objTemplate->total_tax_ids = \count(array_count_values($taxIds)); + + return $arrItems; + } + + /** + * Generate unique order ID including the order prefix + * + * @return string + */ + protected function generateUniqueId() + { + if (!empty($this->arrData['uniqid'])) { + return $this->arrData['uniqid']; + } + + $objConfig = $this->getConfig(); + + if (null === $objConfig) { + $objConfig = Isotope::getConfig(); + } + + return uniqid( + Controller::replaceInsertTags((string) $objConfig->orderPrefix, false), + true + ); + } +} diff --git a/api2/PostCheckoutListener.php b/api2/PostCheckoutListener.php new file mode 100644 index 0000000..bfdfee0 --- /dev/null +++ b/api2/PostCheckoutListener.php @@ -0,0 +1,147 @@ +getBillingAddress(); + +// ob_start(); +// var_dump($order); +// file_put_contents('flo.txt', ob_get_contents()); +// ob_end_clean(); +// +// ob_start(); +// var_dump($item); +// file_put_contents('dan.txt', ob_get_contents()); +// ob_end_clean(); + + // throw new \Exception('ARRRRRRRGGGGGGGGGG'); + $apiExact = new ApiExact(); + + ob_start(); + /** @var ProductCollectionItem $orderItem */ + foreach($order->getItems() as $orderItem) { + /** @var Product $orderItem */ + $product = $orderItem->getProduct(); + var_dump($orderItem->id); + var_dump($orderItem->sku); + var_dump($product->id); + var_dump($product->name); + var_dump($product->is_set); + } + var_dump($apiExact->test()); +// var_dump($order->getItems()); + //var_dump($order); + 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()) +// ); +// +// $this->createSalesOrderData($order, $billingCustomer, $shippingCustomer); + +// $billingFirstName = $order->getBillingAddress()->firstname; +// $billingLastName = $order->getBillingAddress()->lastname; +// $billingStreet = $order->getBillingAddress()->street_1; +// $billingPostcode = $order->getBillingAddress()->postal; +// $billingCity = $order->getBillingAddress()->city; +// $billingCompany = $order->getBillingAddress()->company; +// $billingPhone = $order->getBillingAddress()->phone; +// $billingEmail = $order->getBillingAddress()->email; +// +// $shippingFirstName = $order->getShippingAddress()->firstname; +// $shippingLastName = $order->getShippingAddress()->lastname; +// $shippingStreet = $order->getShippingAddress()->street_1; +// $shippingPostcode = $order->getShippingAddress()->postal; +// $shippingCity = $order->getShippingAddress()->city; +// $shippingCompany = $order->getShippingAddress()->company; +// $shippingPhone = $order->getShippingAddress()->phone; +// $shippingEmail = $order->getShippingAddress()->email; + + } + + private function compareAddresses(Order $order) + { + return + $order->getBillingAddress()->firstname === $order->getShippingAddress()->firstname && + $order->getBillingAddress()->lastname === $order->getShippingAddress()->lastname && + $order->getBillingAddress()->street_1 === $order->getShippingAddress()->street_1 && + $order->getBillingAddress()->postal === $order->getShippingAddress()->postal && + $order->getBillingAddress()->city === $order->getShippingAddress()->city && + $order->getBillingAddress()->company === $order->getShippingAddress()->company && + $order->getBillingAddress()->phone === $order->getShippingAddress()->phone && + $order->getBillingAddress()->email === $order->getShippingAddress()->email; + } + + private function createCustomerApiData(Address $address) + { + return [ + 'Name' => $address->firstname . ' ' . $address->lastname, + 'AddressLine1' => $address->street_1, + 'AddressLine2' => $address->company, + 'City' => $address->city, + 'Postcode' => $address->postal, + 'Email' => $address->email, + 'Country' => 'DE', + 'Phone' => $address->phone, + 'Status' => 'C', + ]; + } + + private function createSalesOrderData(Order $order, $billingCustomer, $shippingCustomer) + { + $date = new \DateTime('now'); + + $orderItems = $order->getItems(); + + return [ + 'Description' => $billingCustomer['Name'], + 'OrderDate' => $date->format('m/d/Y H:i:s'), + 'OrderedBy' => $billingCustomer['ID'], + 'WarehouseID' => self::WAREHOUSE_ID, + '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' + ], + ] + ]; + } +} \ No newline at end of file diff --git a/api2/Product.php b/api2/Product.php new file mode 100644 index 0000000..f507f25 --- /dev/null +++ b/api2/Product.php @@ -0,0 +1,809 @@ +'" . ($time + 60) . "')" + ); + } + + return static::findBy($arrColumns, $arrValues, $arrOptions); + } + + /** + * Find a single product by primary key + * + * @param int $intId + * @param array $arrOptions + * + * @return static|null + */ + public static function findPublishedByPk($intId, array $arrOptions = array()) + { + $arrOptions = array_merge( + array( + 'return' => 'Model' + ), + $arrOptions + ); + + return static::findPublishedBy(static::$strPk, (int) $intId, $arrOptions); + } + + /** + * Find a single product by its ID or alias + * + * @param mixed $varId The ID or alias + * @param array $arrOptions An optional options array + * + * @return static|null The model or null if the result is empty + */ + public static function findPublishedByIdOrAlias($varId, array $arrOptions = array()) + { + $t = static::$strTable; + + $arrColumns = array("($t.id=? OR $t.alias=?)"); + $arrValues = array(is_numeric($varId) ? $varId : 0, $varId); + + $arrOptions = array_merge( + array( + 'limit' => 1, + 'return' => 'Model' + ), + $arrOptions + ); + + return static::findPublishedBy($arrColumns, $arrValues, $arrOptions); + } + + /** + * Find products by IDs + * + * @param array $arrIds + * @param array $arrOptions + * + * @return Product[]|Collection + */ + public static function findPublishedByIds(array $arrIds, array $arrOptions = array()) + { + if (0 === \count($arrIds)) { + return null; + } + + return static::findPublishedBy( + array(static::$strTable . '.id IN (' . implode(',', array_map('intval', $arrIds)) . ')'), + null, + $arrOptions + ); + } + + /** + * Return collection of published product variants by product PID + * + * @param int $intPid + * @param array $arrOptions + * + * @return Collection|Product[]|null + */ + public static function findPublishedByPid($intPid, array $arrOptions = array()) + { + return static::findPublishedBy('pid', (int) $intPid, $arrOptions); + } + + /** + * Return collection of published products by categories + * + * @param array $arrCategories + * @param array $arrOptions + * + * @return Collection|Product[]|null + */ + public static function findPublishedByCategories(array $arrCategories, array $arrOptions = array()) + { + return static::findPublishedBy( + array('c.page_id IN (' . implode(',', array_map('intval', $arrCategories)) . ')'), + null, + $arrOptions + ); + } + + /** + * Find a single frontend-available product by primary key + * + * @param int $intId + * @param array $arrOptions + * + * @return static|null + */ + public static function findAvailableByPk($intId, array $arrOptions = array()) + { + $objProduct = static::findPublishedByPk($intId, $arrOptions); + + if (null === $objProduct || !$objProduct->isAvailableInFrontend()) { + return null; + } + + return $objProduct; + } + + /** + * Find a single frontend-available product by its ID or alias + * + * @param mixed $varId The ID or alias + * @param array $arrOptions An optional options array + * + * @return Product|null The model or null if the result is empty + */ + public static function findAvailableByIdOrAlias($varId, array $arrOptions = array()) + { + $objProduct = static::findPublishedByIdOrAlias($varId, $arrOptions); + + if (null === $objProduct || !$objProduct->isAvailableInFrontend()) { + return null; + } + + return $objProduct; + } + + /** + * Find frontend-available products by IDs + * + * @param array $arrIds + * @param array $arrOptions + * + * @return Collection|Product[]|null + */ + public static function findAvailableByIds(array $arrIds, array $arrOptions = array()) + { + $objProducts = static::findPublishedByIds($arrIds, $arrOptions); + + if (null === $objProducts) { + return null; + } + + $arrProducts = []; + foreach ($objProducts as $objProduct) { + if ($objProduct->isAvailableInFrontend()) { + $arrProducts[] = $objProduct; + } + } + + if (0 === \count($arrProducts)) { + return null; + } + + return new Collection($arrProducts, static::$strTable); + } + + /** + * Find frontend-available products by condition + * + * @param mixed $arrColumns + * @param mixed $arrValues + * @param array $arrOptions + * + * @return Collection + */ + public static function findAvailableBy($arrColumns, $arrValues, array $arrOptions = array()) + { + $objProducts = static::findPublishedBy($arrColumns, $arrValues, $arrOptions); + + if (null === $objProducts) { + return null; + } + + $arrProducts = []; + foreach ($objProducts as $objProduct) { + if ($objProduct->isAvailableInFrontend()) { + $arrProducts[] = $objProduct; + } + } + + if (0 === \count($arrProducts)) { + return null; + } + + return new Collection($arrProducts, static::$strTable); + } + + /** + * Find variant of a product + * + * @param IsotopeProduct $objProduct + * @param array $arrVariant + * @param array $arrOptions + * + * @return Model|null + */ + public static function findVariantOfProduct( + IsotopeProduct $objProduct, + array $arrVariant, + array $arrOptions = array() + ) { + $t = static::$strTable; + + $arrColumns = array( + "$t.id IN (" . implode(',', $objProduct->getVariantIds()) . ')', + "$t." . implode("=? AND $t.", array_keys($arrVariant)) . '=?' + ); + + $arrOptions = array_merge( + array( + 'limit' => 1, + 'column' => $arrColumns, + 'value' => $arrVariant, + 'return' => 'Model' + ), + $arrOptions + ); + + return static::find($arrOptions); + } + + /** + * Finds the default variant of a product. + * + * @param IsotopeProduct $objProduct + * @param array $arrOptions + * + * @return static|null + */ + public static function findDefaultVariantOfProduct(IsotopeProduct $objProduct, array $arrOptions = array()) + { + static $cache; + + if (null === $cache) { + $cache = []; + $data = Database::getInstance()->execute( + "SELECT id, pid FROM tl_iso_product WHERE pid>0 AND language='' AND fallback='1'" + ); + + while ($data->next()) { + $cache[$data->pid] = $data->id; + } + } + + $defaultId = $cache[$objProduct->getProductId()]; + + if ($defaultId < 1 || !\in_array($defaultId, $objProduct->getVariantIds())) { + return null; + } + + return static::findByPk($defaultId, $arrOptions); + } + + /** + * Returns the number of published products. + * + * @param array $arrOptions + * + * @return int + */ + public static function countPublished(array $arrOptions = array()) + { + return static::countPublishedBy(array(), array(), $arrOptions); + } + + /** + * Return the number of products matching certain criteria + * + * @param mixed $arrColumns + * @param mixed $arrValues + * @param array $arrOptions + * + * @return int + */ + public static function countPublishedBy($arrColumns, $arrValues, array $arrOptions = array()) + { + $t = static::$strTable; + + $arrValues = (array) $arrValues; + + if (!\is_array($arrColumns)) { + $arrColumns = array(static::$strTable . '.' . $arrColumns . '=?'); + } + + // Add publish check to $arrColumns as the first item to enable SQL keys + if (BE_USER_LOGGED_IN !== true) { + $time = Date::floorToMinute(); + array_unshift( + $arrColumns, + " + $t.published='1' + AND ($t.start='' OR $t.start<'$time') + AND ($t.stop='' OR $t.stop>'" . ($time + 60) . "') + " + ); + } + + return static::countBy($arrColumns, $arrValues, $arrOptions); + } + + /** + * Gets the number of translation records in the product table. + * Mostly useful to see if there are any translations at all to optimize queries. + * + * @return int + */ + public static function countTranslatedProducts() + { + static $result; + + if (null === $result) { + $result = Database::getInstance()->query( + "SELECT COUNT(*) AS total FROM tl_iso_product WHERE language!=''" + )->total; + } + + return $result; + } + + /** + * Return a model or collection based on the database result type + * + * @param array $arrOptions + * + * @return Product|Product[]|Collection|null + */ + protected static function find(array $arrOptions) + { + $arrOptions['group'] = static::getTable() . '.id' . (null === ($arrOptions['group'] ?? null) ? '' : ', '.$arrOptions['group']); + + $objProducts = parent::find($arrOptions); + + if (null === $objProducts) { + return null; + } + + /** @var Filter[] $arrFilters */ + $arrFilters = $arrOptions['filters'] ?? null; + $arrSorting = $arrOptions['sorting'] ?? null; + + $hasFilters = \is_array($arrFilters) && 0 !== \count($arrFilters); + $hasSorting = \is_array($arrSorting) && 0 !== \count($arrSorting); + + if ($hasFilters || $hasSorting) { + + /** @var static[] $arrProducts */ + $arrProducts = $objProducts->getModels(); + + if ($hasFilters) { + $arrProducts = array_filter($arrProducts, function ($objProduct) use ($arrFilters) { + $arrGroups = []; + + foreach ($arrFilters as $objFilter) { + $blnMatch = $objFilter->matches($objProduct); + + if ($objFilter->hasGroup()) { + $arrGroups[$objFilter->getGroup()] = $arrGroups[$objFilter->getGroup()] ? : $blnMatch; + } elseif (!$blnMatch) { + return false; + } + } + + return !\in_array(false, $arrGroups, true); + }); + } + + // $arrProducts can be empty if the filter removed all records + if ($hasSorting && 0 !== \count($arrProducts)) { + $arrParam = array(); + $arrData = array(); + + foreach ($arrSorting as $strField => $arrConfig) { + foreach ($arrProducts as $objProduct) { + + // Both SORT_STRING and SORT_REGULAR are case sensitive, strings starting with a capital letter + // will come before strings starting with a lowercase letter. To perform a case insensitive + // search, force the sorting order to be determined by a lowercase copy of the original value. + + // Temporary fix for price attribute (see #945) + if ('price' === $strField) { + if (null !== $objProduct->getPrice()) { + $arrData[$strField][$objProduct->id] = $objProduct->getPrice()->getAmount(); + } else { + $arrData[$strField][$objProduct->id] = 0; + } + } else { + $arrData[$strField][$objProduct->id] = strtolower( + str_replace('"', '', $objProduct->$strField) + ); + } + } + + $arrParam[] = &$arrData[$strField]; + $arrParam[] = $arrConfig[0]; + $arrParam[] = $arrConfig[1]; + } + + // Add product array as the last item. + // This will sort the products array based on the sorting of the passed in arguments. + $arrParam[] = &$arrProducts; + \call_user_func_array('array_multisort', $arrParam); + } + + $objProducts = new Collection($arrProducts, static::$strTable); + } + + return $objProducts; + } + + /** + * Return select statement to load product data including multilingual fields + * + * @param array $arrOptions an array of columns + * + * @return string + */ + protected static function buildFindQuery(array $arrOptions) + { + $objBase = DcaExtractor::getInstance($arrOptions['table']); + $hasTranslations = (static::countTranslatedProducts() > 0); + $hasVariants = (ProductType::countByVariants() > 0); + + $arrJoins = array(); + $arrFields = array( + $arrOptions['table'] . '.*', + "'" . str_replace('-', '_', $GLOBALS['TL_LANGUAGE']) . "' AS language", + ); + + if ($hasVariants) { + $arrFields[] = sprintf( + 'IF(%s.pid>0, parent.type, %s.type) AS type', + $arrOptions['table'], + $arrOptions['table'] + ); + } + + if ($hasTranslations) { + foreach (Attribute::getMultilingualFields() as $attribute) { + $arrFields[] = "IFNULL(translation.$attribute, " . $arrOptions['table'] . ".$attribute) AS $attribute"; + } + } + + foreach (Attribute::getFetchFallbackFields() as $attribute) { + $arrFields[] = "{$arrOptions['table']}.$attribute AS {$attribute}_fallback"; + } + + if ($hasTranslations) { + $arrJoins[] = sprintf( + " LEFT OUTER JOIN %s translation ON %s.id=translation.pid AND translation.language='%s'", + $arrOptions['table'], + $arrOptions['table'], + str_replace('-', '_', $GLOBALS['TL_LANGUAGE']) + ); + + $arrOptions['group'] = (null === $arrOptions['group'] ? '' : $arrOptions['group'].', ') . 'translation.id'; + } + + if ($hasVariants) { + $arrJoins[] = sprintf( + ' LEFT OUTER JOIN %s parent ON %s.pid=parent.id', + $arrOptions['table'], + $arrOptions['table'] + ); + } + + $arrFields[] = 'GROUP_CONCAT(c.page_id) AS product_categories'; + $arrJoins[] = sprintf( + ' LEFT OUTER JOIN %s c ON %s=c.pid', + ProductCategory::getTable(), + ($hasVariants ? "IFNULL(parent.id, {$arrOptions['table']}.id)" : "{$arrOptions['table']}.id") + ); + + if ('c.sorting' === ($arrOptions['order'] ?? '')) { + $arrFields[] = 'c.sorting'; + + $arrOptions['group'] = (null === $arrOptions['group'] ? '' : $arrOptions['group'].', ') . 'c.id'; + } + + if ($objBase->hasRelations()) { + $intCount = 0; + + foreach ($objBase->getRelations() as $strKey => $arrConfig) { + // Automatically join the single-relation records + if (('eager' === $arrConfig['load'] || ($arrOptions['eager'] ?? false)) + && ('hasOne' === $arrConfig['type'] || 'belongsTo' === $arrConfig['type']) + ) { + if (\is_array($arrOptions['joinAliases'] ?? null) + && ($key = array_search($arrConfig['table'], $arrOptions['joinAliases'], true)) !== false + ) { + $strJoinAlias = $key; + unset($arrOptions['joinAliases'][$key]); + } else { + ++$intCount; + $strJoinAlias = 'j' . $intCount; + } + + $objRelated = DcaExtractor::getInstance($arrConfig['table']); + + foreach ($objRelated->getFields() as $strField => $config) { + $arrFields[] = $strJoinAlias . '.' . $strField . ' AS ' . $strKey . '__' . $strField; + } + + $arrJoins[] = sprintf( + ' LEFT JOIN %s %s ON %s.%s=%s.id', + $arrConfig['table'], + $strJoinAlias, + $arrOptions['table'], + $strKey, + $strJoinAlias + ); + } + } + } + + // Generate the query + $strQuery = 'SELECT ' . implode(', ', $arrFields) . ' FROM ' . $arrOptions['table'] . implode('', $arrJoins); + + // Where condition + if (!\is_array($arrOptions['column'] ?? null)) { + $arrOptions['column'] = array($arrOptions['table'] . '.' . $arrOptions['column'] . '=?'); + } + + // The model must never find a language record + $strQuery .= " WHERE {$arrOptions['table']}.language='' AND " . implode(' AND ', $arrOptions['column']); + + // Group by + if (($arrOptions['group'] ?? null) !== null) { + $strQuery .= ' GROUP BY ' . $arrOptions['group']; + } + + // Order by + if (($arrOptions['order'] ?? null) !== null) { + $strQuery .= ' ORDER BY ' . $arrOptions['order']; + } + + return $strQuery; + } + + /** + * Build a query based on the given options to count the number of products. + * + * @param array $arrOptions The options array + * + * @return string + */ + protected static function buildCountQuery(array $arrOptions) + { + $hasTranslations = (static::countTranslatedProducts() > 0); + $hasVariants = (ProductType::countByVariants() > 0); + + $arrJoins = array(); + $arrFields = array( + $arrOptions['table'] . '.id', + "'" . str_replace('-', '_', $GLOBALS['TL_LANGUAGE']) . "' AS language", + ); + + if ($hasVariants) { + $arrFields[] = sprintf( + 'IF(%s.pid>0, parent.type, %s.type) AS type', + $arrOptions['table'], + $arrOptions['table'] + ); + } + + if ($hasTranslations) { + foreach (Attribute::getMultilingualFields() as $attribute) { + $arrFields[] = "IFNULL(translation.$attribute, " . $arrOptions['table'] . ".$attribute) AS $attribute"; + } + } + + if ($hasTranslations) { + $arrJoins[] = sprintf( + " LEFT OUTER JOIN %s translation ON %s.id=translation.pid AND translation.language='%s'", + $arrOptions['table'], + $arrOptions['table'], + str_replace('-', '_', $GLOBALS['TL_LANGUAGE']) + ); + + $arrOptions['group'] = (null === $arrOptions['group'] ? '' : $arrOptions['group'].', ') . 'translation.id, tl_iso_product.id'; + } + + if ($hasVariants) { + $arrJoins[] = sprintf( + ' LEFT OUTER JOIN %s parent ON %s.pid=parent.id', + $arrOptions['table'], + $arrOptions['table'] + ); + } + + $arrJoins[] = sprintf( + ' LEFT OUTER JOIN %s c ON %s=c.pid', + ProductCategory::getTable(), + ($hasVariants ? "IFNULL(parent.id, {$arrOptions['table']}.id)" : "{$arrOptions['table']}.id") + ); + + // Generate the query + $strWhere = ''; + $strQuery = ' + SELECT + ' . implode(', ', $arrFields) . ' + FROM ' . $arrOptions['table'] . implode('', $arrJoins); + + // Where condition + if (!empty($arrOptions['column'])) { + if (!\is_array($arrOptions['column'])) { + $arrOptions['column'] = array($arrOptions['table'] . '.' . $arrOptions['column'] . '=?'); + } + + $strWhere = ' AND ' . implode(' AND ', $arrOptions['column']); + } + + // The model must never find a language record + $strQuery .= " WHERE {$arrOptions['table']}.language=''" . $strWhere; + + // Group by + if ($arrOptions['group'] !== null) { + $strQuery .= ' GROUP BY ' . $arrOptions['group']; + } + + return 'SELECT COUNT(*) AS count FROM ('.$strQuery.') c1'; + } + + /** + * Return select statement to load product data including multilingual fields + * + * @param array $arrOptions an array of columns + * @param array $arrJoinAliases an array of table join aliases + * + * @return string + * + * @deprecated use buildFindQuery introduced in Contao 3.3 + */ + protected static function buildQueryString($arrOptions, $arrJoinAliases = array('t' => 'tl_iso_producttype')) + { + $arrOptions['joinAliases'] = $arrJoinAliases; + + return static::buildFindQuery((array) $arrOptions); + } +} 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/ProductCollectionItem.php b/api2/ProductCollectionItem.php new file mode 100644 index 0000000..261a52a --- /dev/null +++ b/api2/ProductCollectionItem.php @@ -0,0 +1,489 @@ +isLocked()) { + return true; + } + + if (isset($GLOBALS['ISO_HOOKS']['itemIsAvailable']) && \is_array($GLOBALS['ISO_HOOKS']['itemIsAvailable'])) { + foreach ($GLOBALS['ISO_HOOKS']['itemIsAvailable'] as $callback) { + $available = System::importStatic($callback[0])->{$callback[1]}($this); + + // If return value is boolean then we accept it as result + if (true === $available || false === $available) { + return $available; + } + } + } + + if (!$this->hasProduct() || !$this->getProduct()->isAvailableForCollection($this->getRelated('pid'))) { + return false; + } + + $arrConfig = $this->getOptions(); + foreach ($this->getProduct()->getOptions() as $k => $v) { + if ($arrConfig[$k] !== $v) { + return false; + } + } + + return true; + } + + /** + * Return true if product collection item is locked + */ + public function isLocked() + { + return $this->blnLocked; + } + + /** + * Lock item, necessary if product collection is locked + */ + public function lock() + { + $this->blnLocked = true; + } + + /** + * Delete downloads when deleting product collection item + * + * @return int + */ + public function delete() + { + $intId = $this->id; + $intAffected = parent::delete(); + + if ($intAffected) { + Database::getInstance()->query("DELETE FROM tl_iso_product_collection_download WHERE pid=$intId"); + } + + return $intAffected; + } + + /** + * Get the product related to this item + * + * @param bool $blnNoCache + * + * @return IsotopeProduct|null + */ + public function getProduct($blnNoCache = false) + { + if (false === $this->objProduct || true === $blnNoCache) { + + $this->objProduct = null; + + /** @var string|\Isotope\Model\Product $strClass */ + $strClass = Product::getClassForModelType($this->type); + + if ($strClass == '' || !class_exists($strClass)) { + System::log('Error creating product object of type "' . $this->type . '"', __METHOD__, TL_ERROR); + + return null; + } + + try { + $this->objProduct = $strClass::findByPk($this->product_id); + } catch (\Exception $e) { + $this->objProduct = null; + $this->addError($e->getMessage()); + } + + if (null !== $this->objProduct && $this->objProduct instanceof IsotopeProductWithOptions) { + try { + if ($this->objProduct instanceof Model) { + $this->objProduct = clone $this->objProduct; + $this->objProduct->preventSaving(false); + $this->objProduct->id = $this->product_id; + } + + $this->objProduct->setOptions($this->getOptions()); + } catch (\RuntimeException $e) { + $this->addError($GLOBALS['TL_LANG']['ERR']['collectionItemNotAvailable']); + } + } + } + + return $this->objProduct; + } + + /** + * Return boolean flag if product could be loaded + * + * @return bool + */ + public function hasProduct() + { + return (null !== $this->getProduct()); + } + + /** + * Get product SKU. Automatically falls back to the collection item table if product is not found. + * + * @return string + */ + public function getSku() + { + return (string) ($this->isLocked() || !$this->hasProduct()) ? $this->sku : $this->getProduct()->getSku(); + } + + /** + * Get product name. Automatically falls back to the collection item table if product is not found. + * + * @return string + */ + public function getName() + { + return (string) ($this->isLocked() || !$this->hasProduct()) ? $this->name : $this->getProduct()->getName(); + } + + /** + * Returns key-value array for variant-enabled and customer editable attributes. + * + * @return array + * + * @deprecated Use getOptions() + */ + public function getAttributes() + { + return $this->getOptions(); + } + + /** + * Returns key-value array for variant-enabled and customer editable attributes. + * + * @return array + */ + public function getOptions() + { + $arrConfig = StringUtil::deserialize($this->configuration); + + return \is_array($arrConfig) ? $arrConfig : []; + } + + /** + * Get product configuration + * + * @return array + * + * @deprecated Deprecated since Isotope 2.4, to be removed in Isotope 3.0. Use getOptions() instead. + */ + public function getConfiguration() + { + $arrConfig = StringUtil::deserialize($this->configuration); + + if (empty($arrConfig) || !\is_array($arrConfig)) { + return array(); + } + + if ($this->hasProduct()) { + return Isotope::formatProductConfiguration($arrConfig, $this->getProduct()); + + } else { + foreach ($arrConfig as $k => $v) { + $arrConfig[$k] = new Plain($v, $k); + } + + return $arrConfig; + } + } + + /** + * Get product price. Automatically falls back to the collection item table if product is not found. + * + * @return string + */ + public function getPrice() + { + if ($this->isLocked() || !$this->hasProduct()) { + return $this->price; + } + + $objPrice = $this->getProduct()->getPrice($this->getRelated('pid')); + + if (null === $objPrice) { + return ''; + } + + return $objPrice->getAmount((int) $this->quantity, $this->getOptions()); + } + + /** + * Get tax free product price. Automatically falls back to the collection item table if product is not found. + * + * @return string + */ + public function getTaxFreePrice() + { + if ($this->isLocked() || !$this->hasProduct()) { + return $this->tax_free_price; + } + + $objPrice = $this->getProduct()->getPrice($this->getRelated('pid')); + + if (null === $objPrice) { + return ''; + } + + return $objPrice->getNetAmount((int) $this->quantity, $this->getOptions()); + } + + /** + * Get original product price. Automatically falls back to the collection item table if product is not found. + * + * @return string + */ + public function getOriginalPrice() + { + if ($this->isLocked() || !$this->hasProduct()) { + return $this->price; + } + + $objPrice = $this->getProduct()->getPrice($this->getRelated('pid')); + + if (null === $objPrice) { + return ''; + } + + return $objPrice->getOriginalAmount((int) $this->quantity, $this->getOptions()); + } + + /** + * Get product price multiplied by the requested product quantity + * + * @return string + */ + public function getTotalPrice() + { + return (string) ($this->getPrice() * (int) $this->quantity); + } + + /** + * Get original product price multiplied by the requested product quantity + * + * @return string + */ + public function getTotalOriginalPrice() + { + return (string) ($this->getOriginalPrice() * (int) $this->quantity); + } + + /** + * Get tax free product price multiplied by the requested product quantity + * + * @return string + */ + public function getTaxFreeTotalPrice() + { + return (string) ($this->getTaxFreePrice() * (int) $this->quantity); + } + + /** + * Return downloads associated with this product collection item + * + * @return ProductCollectionDownload[] + */ + public function getDownloads() + { + if (null === $this->arrDownloads) { + $this->arrDownloads = array(); + + $objDownloads = ProductCollectionDownload::findBy('pid', $this->id); + + if (null !== $objDownloads) { + while ($objDownloads->next()) { + $this->arrDownloads[] = $objDownloads->current(); + } + } + } + + return $this->arrDownloads; + } + + /** + * Increase quantity of product collection item + * + * @param int $intQuantity + * + * @return self + */ + public function increaseQuantityBy($intQuantity) + { + $time = time(); + + Database::getInstance()->query(" + UPDATE tl_iso_product_collection_item + SET tstamp=$time, quantity=(quantity+" . (int) $intQuantity . ') + WHERE id=' . $this->id + ); + + $this->tstamp = $time; + $this->quantity = Database::getInstance() + ->query("SELECT quantity FROM tl_iso_product_collection_item WHERE id=" . $this->id) + ->quantity + ; + + return $this; + } + + /** + * Decrease quantity of product collection item + * + * @param int $intQuantity + * + * @return self + */ + public function decreaseQuantityBy($intQuantity) + { + if (($this->quantity - $intQuantity) < 1) { + throw new \UnderflowException('Quantity of product collection item cannot be less than 1.'); + } + + $time = time(); + + Database::getInstance()->query(" + UPDATE tl_iso_product_collection_item + SET tstamp=$time, quantity=(quantity-" . (int) $intQuantity . ') + WHERE id=' . $this->id + ); + + $this->tstamp = $time; + $this->quantity = Database::getInstance() + ->query('SELECT quantity FROM tl_iso_product_collection_item WHERE id=' . $this->id) + ->quantity + ; + + return $this; + } + + /** + * Calculate the sum of a database column + * + * @param string $strField + * @param mixed $strColumn + * @param mixed $varValue + * + * @return int + */ + public static function sumBy($strField, $strColumn = null, $varValue = null) + { + $strQuery = "SELECT SUM($strField) AS sum FROM tl_iso_product_collection_item"; + + if ($strColumn !== null) { + $strQuery .= ' WHERE ' . (\is_array($strColumn) ? implode(' AND ', $strColumn) : static::$strTable . '.' . $strColumn . "=?"); + } + + return (int) Database::getInstance()->prepare($strQuery)->execute($varValue)->sum; + } + + /** + * Add an error message + * + * @param string $strError + */ + public function addError($strError) + { + $this->arrErrors[] = $strError; + } + + /** + * Return true if the collection item has errors + * + * @return bool + */ + public function hasErrors() + { + return 0 !== \count($this->arrErrors); + } + + /** + * Return the errors array + * + * @return array + */ + public function getErrors() + { + return $this->arrErrors; + } +} diff --git a/api2/apiTestCreateCustomer.php b/api2/apiTestCreateCustomer.php new file mode 100644 index 0000000..7236c47 --- /dev/null +++ b/api2/apiTestCreateCustomer.php @@ -0,0 +1,7 @@ +createCustomer()); diff --git a/api2/apiTestCreateSalesOrder.php b/api2/apiTestCreateSalesOrder.php new file mode 100644 index 0000000..6749043 --- /dev/null +++ b/api2/apiTestCreateSalesOrder.php @@ -0,0 +1,7 @@ +createSalesOrder()); diff --git a/api2/apiTestGetAccessToken.php b/api2/apiTestGetAccessToken.php new file mode 100644 index 0000000..de9dc09 --- /dev/null +++ b/api2/apiTestGetAccessToken.php @@ -0,0 +1,9 @@ +getAccessToken() . "\n"; diff --git a/api2/apiUpdateProducts.php b/api2/apiUpdateProducts.php new file mode 100644 index 0000000..327b88e --- /dev/null +++ b/api2/apiUpdateProducts.php @@ -0,0 +1,10 @@ +getProducts(); +$databaseHandler->processProducts($products); + diff --git a/api2/dan.txt b/api2/dan.txt new file mode 100644 index 0000000..f31e9f6 --- /dev/null +++ b/api2/dan.txt @@ -0,0 +1,11 @@ +string(3) "586" +string(8) "WR000400" +string(2) "41" +string(24) "Schaum-Sprüher 1,5Liter" +NULL +string(3) "587" +string(8) "WR000250" +string(2) "42" +string(11) "Tragebeutel" +NULL +string(15) "fuck off contao" diff --git a/api2/tokenData b/api2/tokenData new file mode 100644 index 0000000..67a3864 --- /dev/null +++ b/api2/tokenData @@ -0,0 +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":1657726518} \ No newline at end of file