From 12c5ced3d5069731ba7518456faed2fff970e5e2 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 8 Nov 2019 13:18:23 +0100 Subject: [PATCH 01/62] [TASK] Created basic structure for coupons implementation --- Classes/Controller/ProductController.php | 11 + Classes/Domain/Model/Coupon.php | 210 ++++++++++++++++++ Classes/Domain/Model/Order.php | 77 +++++++ .../Domain/Repository/CouponRepository.php | 52 +++++ Classes/Service/PriceService.php | 181 +++++++++++++++ ..._pxaproductmanager_domain_model_coupon.php | 152 +++++++++++++ ...x_pxaproductmanager_domain_model_order.php | 44 +++- Resources/Private/Language/locallang.xlf | 10 + Resources/Private/Language/locallang_db.xlf | 45 +++- .../Partials/WishList/RowsWithOrder.html | 18 +- .../Private/Templates/Product/WishList.html | 2 +- .../Templates/Product/WishListCart.html | 2 +- ext_tables.sql | 51 ++++- 13 files changed, 846 insertions(+), 9 deletions(-) create mode 100644 Classes/Domain/Model/Coupon.php create mode 100644 Classes/Domain/Repository/CouponRepository.php create mode 100644 Classes/Service/PriceService.php create mode 100644 Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php diff --git a/Classes/Controller/ProductController.php b/Classes/Controller/ProductController.php index b564de08..9d27d125 100644 --- a/Classes/Controller/ProductController.php +++ b/Classes/Controller/ProductController.php @@ -281,6 +281,17 @@ public function wishListAction(bool $sendOrder = false) ]); } + /** + * @param string $couponCode submitted by the user + * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException + * @throws \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException + */ + public function addCouponCodeToOrderAction($couponCode = '') { + + + $this->redirect('wishList'); + } + /** * Finish order text * Show some successful texts diff --git a/Classes/Domain/Model/Coupon.php b/Classes/Domain/Model/Coupon.php new file mode 100644 index 00000000..17edc9de --- /dev/null +++ b/Classes/Domain/Model/Coupon.php @@ -0,0 +1,210 @@ +title; + } + + /** + * @param string $title + */ + public function setName(string $name) + { + $this->title = $title; + } + + /** + * @return string + */ + public function getCode(): string + { + return $this->code; + } + + /** + * @param string $code + */ + public function setCode(string $code) + { + $this->code = $code; + } + + /** + * @return int + */ + public function getUsageLimit(): int + { + return $this->maxUses; + } + + /** + * @param int $maxUses + */ + public function setUsageLimit(int $usageLimit) + { + $this->maxUses = $maxUses; + } + + /** + * @return float + */ + public function getCostLimit(): float + { + return $this->maxCost; + } + + /** + * @param float $maxCost + */ + public function setCostLimit(float $costLimit) + { + $this->maxCost = $maxCost; + } + + /** + * @return int + */ + public function getUsageCount(): int + { + return $this->usageCount; + } + + /** + * @param int $usageCount + */ + public function setUsageCount(int $usageCount) + { + $this->usageCount = $usageCount; + } + + /** + * @return float + */ + public function getTotalCost(): float + { + return $this->totalCost; + } + + /** + * @param float $totalCost + */ + public function setTotalCost(float $totalCost) + { + $this->totalCost = $totalCost; + } + + /** + * @return float + */ + public function getValue(): float + { + return $this->value; + } + + /** + * @param float $value + */ + public function setValue(float $value) + { + $this->value = $value; + } + + /** + * @return int + */ + public function getType(): int + { + return $this->type; + } + + /** + * @param int $type + */ + public function setType(int $type) + { + $this->type = $type; + } + +} diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 2de11ffa..efdb07ca 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -90,6 +90,27 @@ class Order extends AbstractEntity */ protected $checkoutType = 'default'; + /** + * Coupons used for this order + * + * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelant\PxaProductManager\Domain\Model\Coupon> + */ + protected $coupons = null; + + /** + * The price of the order all inclusive at checkout time, independent on changing product prices, taxes and coupons + * + * @var float + */ + protected $priceAtCheckout = 0.0; + + /** + * The tax for the order at checkout time, independent on changing product prices, taxes and coupons + * + * @var float + */ + protected $taxAtCheckout = 0.0; + /** * __construct */ @@ -377,4 +398,60 @@ public function setCheckoutType(string $checkoutType) { $this->checkoutType = $checkoutType; } + + /** + * @return ObjectStorage + */ + public function getCoupons(): ObjectStorage + { + return $this->coupons; + } + + /** + * @param ObjectStorage $coupons + */ + public function setCoupons(ObjectStorage $coupons) + { + $this->coupons = $coupons; + } + + /** + * The all-inclusive price at checkout time + * + * @return float + */ + public function getPriceAtCheckout(): float + { + return $this->priceAtCheckout; + } + + /** + * The all-inclusive price at checkout time + * + * @param float $priceAtCheckout + */ + public function setPriceAtCheckout(float $priceAtCheckout) + { + $this->priceAtCheckout = $priceAtCheckout; + } + + /** + * The tax sum at checkout time + * + * @return float + */ + public function getTaxAtCheckout(): float + { + return $this->taxAtCheckout; + } + + /** + * The tax sum at checkout time + * + * @param float $taxAtCheckout + */ + public function setTaxAtCheckout(float $taxAtCheckout) + { + $this->taxAtCheckout = $taxAtCheckout; + } } diff --git a/Classes/Domain/Repository/CouponRepository.php b/Classes/Domain/Repository/CouponRepository.php new file mode 100644 index 00000000..a5fdc694 --- /dev/null +++ b/Classes/Domain/Repository/CouponRepository.php @@ -0,0 +1,52 @@ + + */ + protected $coupons = []; + + /** + * @return Order + */ + public function getOrder(): Order + { + return $this->order; + } + + /** + * @param Order $order + */ + public function setOrder(Order $order): self + { + $this->order = $order; + return $this; + } + + /** + * @return Product + */ + public function getProduct(): Product + { + return $this->product; + } + + /** + * @param Product $product + */ + public function setProduct(Product $product): self + { + $this->product = $product; + return $this; + } + + /** + * @return array + */ + public function getCoupons(): array + { + return $this->coupons; + } + + /** + * @param array $coupons + */ + public function setCoupons(array $coupons): self + { + $this->coupons = $coupons; + return $this; + } + + /** + * Reset the service properties + * + * @return $this + */ + public function reset() + { + $this->order = null; + $this->product = null; + $this->coupons = []; + + return $this; + } + + /** + * Returns the total net price with all modifiers included + * + * @return float + */ + public function calculateTotalPrice(): float + { + + } + + /** + * Returns the total tax + * + * @return float + */ + public function caluclateTotalTax(): float + { + + } + + /** + * Returns the change in value resulting from the applied coupons + * + * @return float + */ + public function calculateTotalCouponValue(): float + { + + } + + /** + * Returns the price before tax, but including coupons + * + * @return float + */ + public function calculatePriceBeforeTax(): float + { + + } + + /** + * Returns the price before tax and coupons + * + * @return float + */ + public function calculatePriceBeforeTaxAndCoupons(): float + { + + } + + /** + * Returns the price including tax, but excluding coupons + * + * @return float + */ + public function calculatePriceBeforeCoupons(): float + { + + } +} diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php new file mode 100644 index 00000000..a2ecce8d --- /dev/null +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php @@ -0,0 +1,152 @@ + [ + 'title' => $ll, + 'label' => 'name', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'cruser_id' => 'cruser_id', + 'dividers2tabs' => true, + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + ], + 'searchFields' => 'name,code', + #'hideTable' => true, + 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/coupon_tca.svg' + ], + 'interface' => [ + 'showRecordFieldList' => 'hidden, name, code, type, value, usage_limit, cost_limit, usage_count, total_cost' + ], + 'types' => [ + '1' => [ + 'showitem' => 'hidden, name, code, type, value, usage_limit, cost_limit, usage_count, total_cost, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime' + ], + ], + 'columns' => [ + 'hidden' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.hidden', + 'config' => [ + 'type' => 'check', + 'items' => [ + '1' => [ + '0' => $llCore . 'locallang_core.xlf:labels.enabled' + ] + ], + ], + ], + 'starttime' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.starttime', + 'config' => [ + 'type' => 'input', + 'size' => 13, + 'eval' => 'datetime,int', + 'renderType' => 'inputDateTime', + 'default' => 0, + ] + ], + 'endtime' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.endtime', + 'config' => [ + 'type' => 'input', + 'size' => 13, + 'eval' => 'datetime,int', + 'default' => 0, + 'renderType' => 'inputDateTime', + 'range' => [ + 'upper' => mktime(0, 0, 0, 1, 1, 2038) + ] + ], + ], + 'name' => [ + 'exclude' => 0, + 'label' => $ll . '.name', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'eval' => 'trim,required' + ], + ], + 'code' => [ + 'exclude' => 0, + 'label' => $ll . '.code', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'eval' => 'upper,alphanum_x,nospace,unique,trim,required' + ], + ], + 'type' => [ + 'exclude' => 0, + 'label' => $ll . '.type', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + [$ll . '.type.' . \Pixelant\PxaProductManager\Domain\Model\Coupon::TYPE_CASH_REBATE, \Pixelant\PxaProductManager\Domain\Model\Coupon::TYPE_CASH_REBATE], + [$ll . '.type.' . \Pixelant\PxaProductManager\Domain\Model\Coupon::TYPE_PERCENTAGE_REBATE, \Pixelant\PxaProductManager\Domain\Model\Coupon::TYPE_PERCENTAGE_REBATE] + ], + 'eval' => 'required' + ], + ], + 'value' => [ + 'exclude' => 0, + 'label' => $ll . '.value', + 'config' => [ + 'type' => 'input', + 'size' => 5, + 'eval' => 'double2,required' + ], + ], + 'usage_limit' => [ + 'exclude' => 1, + 'label' => $ll . '.usage_limit', + 'config' => [ + 'type' => 'input', + 'default' => 0, + 'size' => 5, + 'eval' => 'int' + ], + ], + 'cost_limit' => [ + 'exclude' => 0, + 'label' => $ll . '.cost_limit', + 'config' => [ + 'type' => 'input', + 'default' => 0.0, + 'size' => 5, + 'eval' => 'double2' + ], + ], + 'usage_count' => [ + 'exclude' => 1, + 'label' => $ll . '.usage_limit', + 'config' => [ + 'type' => 'input', + 'default' => 0, + 'size' => 10, + 'eval' => 'int' + ], + ], + 'total_cost' => [ + 'exclude' => 1, + 'label' => $ll . '.total_cost', + 'config' => [ + 'type' => 'input', + 'default' => 0, + 'size' => 10, + 'eval' => 'int' + ], + ], + ] +]; diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php index 15791740..a35e0058 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php @@ -203,6 +203,48 @@ 'readOnly' => true, 'default' => 'default' ], - ] + ], + 'coupons' => [ + 'exclude' => 0, + 'label' => $ll . '.coupons', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'foreign_table' => 'tx_pxaproductmanager_domain_model_coupon', + 'MM' => 'tx_pxaproductmanager_order_coupon_mm', + 'size' => 10, + 'autoSizeMax' => 30, + 'maxitems' => 9999, + 'multiple' => 0, + 'fieldControl' => [ + 'editPopup' => [ + 'disabled' => false + ], + 'addRecord' => [ + 'disabled' => false, + ] + ] + ], + ], + 'price_at_checkout' => [ + 'exclude' => 0, + 'label' => $ll . '.price_at_checkout', + 'config' => [ + 'type' => 'input', + 'default' => 0.0, + 'size' => 5, + 'eval' => 'double2' + ], + ], + 'tax_at_checkout' => [ + 'exclude' => 0, + 'label' => $ll . '.tax_at_checkout', + 'config' => [ + 'type' => 'input', + 'default' => 0.0, + 'size' => 5, + 'eval' => 'double2' + ], + ], ] ]; diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 5622fbfb..8c42560a 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -205,6 +205,16 @@ Products + + Add discount code + + + Add discount code + + + Enter discount code + + No results diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 87b19959..f174b483 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -264,6 +264,49 @@ Checkout type + + Coupons + + + Price at checkout + + + Tax at checkout + + + + Price coupon + + + Name + + + Coupon code (uppercase, dash, and underscore allowed) + + + Type + + + Cash rebate + + + Percentage rebate + + + Value + + + Max uses (0 = no limit) + + + Cost limit (0.00 = no limit) + + + Usage count + + + Total cost + @@ -400,4 +443,4 @@ - \ No newline at end of file + diff --git a/Resources/Private/Partials/WishList/RowsWithOrder.html b/Resources/Private/Partials/WishList/RowsWithOrder.html index 2c7fa932..0a609029 100644 --- a/Resources/Private/Partials/WishList/RowsWithOrder.html +++ b/Resources/Private/Partials/WishList/RowsWithOrder.html @@ -90,6 +90,8 @@ + +
0 - @@ -205,8 +207,22 @@
+ +
+
+ +
+ + + +
+
+
+
+
+ - \ No newline at end of file + diff --git a/Resources/Private/Templates/Product/WishList.html b/Resources/Private/Templates/Product/WishList.html index b0fccc98..46457e74 100644 --- a/Resources/Private/Templates/Product/WishList.html +++ b/Resources/Private/Templates/Product/WishList.html @@ -14,4 +14,4 @@ - \ No newline at end of file + diff --git a/Resources/Private/Templates/Product/WishListCart.html b/Resources/Private/Templates/Product/WishListCart.html index 31650f9f..4cfdb80c 100644 --- a/Resources/Private/Templates/Product/WishListCart.html +++ b/Resources/Private/Templates/Product/WishListCart.html @@ -17,4 +17,4 @@ Wish list page is not set {$plugin.tx_pxaproductmanager.settings.wishList.pagePid} - \ No newline at end of file + diff --git a/ext_tables.sql b/ext_tables.sql index c330305d..6bf20b66 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -429,10 +429,11 @@ CREATE TABLE tx_pxaproductmanager_domain_model_order ( products int(11) unsigned DEFAULT '0' NOT NULL, fe_user int(11) unsigned DEFAULT '0' NOT NULL, complete tinyint(4) unsigned DEFAULT '0' NOT NULL, - serialized_order_fields blob, - serialized_products_quantity blob, - external_id varchar(255) DEFAULT '' NOT NULL, - checkout_type varchar(255) DEFAULT 'default' NOT NULL, + serialized_order_fields blob, + serialized_products_quantity blob, + external_id varchar(255) DEFAULT '' NOT NULL, + checkout_type varchar(255) DEFAULT 'default' NOT NULL, + coupons int(11) unsigned DEFAULT '0' NOT NULL, tstamp int(11) unsigned DEFAULT '0' NOT NULL, crdate int(11) unsigned DEFAULT '0' NOT NULL, @@ -464,6 +465,19 @@ CREATE TABLE tx_pxaproductmanager_order_product_mm ( KEY uid_foreign (uid_foreign) ); +# +# Table structure for table 'tx_pxaproductmanager_order_coupons_mm' +# +CREATE TABLE tx_pxaproductmanager_order_product_mm ( + uid_local int(11) unsigned DEFAULT '0' NOT NULL, + uid_foreign int(11) unsigned DEFAULT '0' NOT NULL, + sorting int(11) unsigned DEFAULT '0' NOT NULL, + sorting_foreign int(11) unsigned DEFAULT '0' NOT NULL, + + KEY uid_local (uid_local), + KEY uid_foreign (uid_foreign) +); + # # Table structure for table 'tx_pxaproductmanager_domain_model_orderconfiguration' # @@ -530,3 +544,32 @@ CREATE TABLE tx_pxaproductmanager_domain_model_orderformfield ( KEY parent (pid), KEY language (l10n_parent,sys_language_uid) ); + +# +# Table structure for table 'tx_pxaproductmanager_domain_model_cupon' +# +CREATE TABLE tx_pxaproductmanager_domain_model_coupon ( + uid int(11) NOT NULL auto_increment, + pid int(11) DEFAULT '0' NOT NULL, + + name varchar(255) DEFAULT '' NOT NULL, + code varchar(255) DEFAULT '' NOT NULL, + type tinyint(4) unsigned DEFAULT '0' NOT NULL, + value double(11,2) DEFAULT '0.00' NOT NULL, + usage_limit int(11) unsigned DEFAULT '0' NOT NULL, + cost_limit double(11,2) DEFAULT '0.00' NOT NULL, + usage_count int(11) unsigned DEFAULT '0' NOT NULL, + total_cost double(11,2) DEFAULT '0.00' NOT NULL, + + tstamp int(11) unsigned DEFAULT '0' NOT NULL, + crdate int(11) unsigned DEFAULT '0' NOT NULL, + cruser_id int(11) unsigned DEFAULT '0' NOT NULL, + deleted tinyint(4) unsigned DEFAULT '0' NOT NULL, + hidden tinyint(4) unsigned DEFAULT '0' NOT NULL, + starttime int(11) unsigned DEFAULT '0' NOT NULL, + endtime int(11) unsigned DEFAULT '0' NOT NULL, + + PRIMARY KEY (uid), + KEY couponcode (code), + KEY parent (pid) +); From 9fd155015b64b1d5641ddb84bdf3c418cea6d511 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 10:48:32 +0100 Subject: [PATCH 02/62] [TASK] Wish list is now stored in TYPO3 session as an Order object --- Classes/Utility/MainUtility.php | 5 + Classes/Utility/OrderUtility.php | 175 +++++++++++++++++++++++++++++ Classes/Utility/ProductUtility.php | 32 +++++- 3 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 Classes/Utility/OrderUtility.php diff --git a/Classes/Utility/MainUtility.php b/Classes/Utility/MainUtility.php index 6efb9081..d39f393a 100644 --- a/Classes/Utility/MainUtility.php +++ b/Classes/Utility/MainUtility.php @@ -150,6 +150,11 @@ public static function removeValueFromListCookie(string $name, int $value, int $ */ public static function addValueToListCookie(string $name, int $value, int $maxValues = 20) { + if ($name === ProductUtility::WISH_LIST_COOKIE_NAME) { + OrderUtility::addProductUidToSessionOrder($value); + return; + } + // Can't be 0 $maxValues = $maxValues === 0 ? 20 : $maxValues; diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php new file mode 100644 index 00000000..d50660fa --- /dev/null +++ b/Classes/Utility/OrderUtility.php @@ -0,0 +1,175 @@ +, Pixelant + * + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + ***************************************************************/ + +/** + * Class for handling order-related actions + * + * Currently related to session order persistence + * + * Class OrderUtility + * @package Pixelant\PxaProductManager\Utility + */ +class OrderUtility +{ + const SESSION_KEY = 'tx_pxa_product_manager_sessionOrder'; + + const MAX_PRODUCTS = 20; + + /** + * Fetches the current session's order + * + * @return Order + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException + */ + public static function getSessionOrder() + { + /** @var OrderRepository $orderRepository */ + $orderRepository = GeneralUtility::makeInstance(OrderRepository::class); + + $orderUid = (int) $GLOBALS['TSFE']->fe_user->getKey('ses', self::SESSION_KEY); + + if ($orderUid > 0) { + /** @var Order $order */ + $order = $orderRepository->findByUid($orderUid); + + if ($order !== null && !$order->isComplete()) { + return $order; + } + } + + $order = MainUtility::getObjectManager()->get(Order::class); + + $orderRepository->add($order); + + /** @var PersistenceManagerInterface $persistanceManager */ + $persistanceManager = GeneralUtility::makeInstance(PersistenceManagerInterface::class); + + //Make sure we get a UID + $persistanceManager->persistAll(); + + $GLOBALS['TSFE']->fe_user->setKey('ses', self::SESSION_KEY, $order->getUid()); + + return $order; + } + /** + * Adds a product to the session order using the product's UID + * + * @param int $uid + * @throws UnknownProductException + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException + */ + public static function addProductUidToSessionOrder(int $uid) + { + self::addProductToSessionOrder(ProductUtility::getProductByUid($uid)); + } + + /** + * Removed a product UID from the order + * + * @param int $uid + * @throws UnknownProductException + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException + */ + public static function removeProductUidFromSessionOrder(int $uid) + { + self::removeProductFromSessionOrder(ProductUtility::getProductByUid($uid)); + } + + /** + * Adds a product to the session order + * + * @param Product $product + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException + */ + public static function addProductToSessionOrder(Product $product) + { + $settings = ConfigurationUtility::getSettingsByPath('pagePid'); + + $order = self::getSessionOrder(); + + //If there are too many products, remove one. + if ( + ((int) $settings['wishList']['limit'] > 0 && $order->getProducts()->count() + 1 > (int) $settings['wishList']['limit']) + || + ((int) $settings['wishList']['limit'] === 0 && $order->getProducts()->count() + 1 > self::MAX_PRODUCTS) + ) { + $order->getProducts()->rewind(); + $order->removeProduct($order->getProducts()->current()); + } + + $order->addProduct($product); + + self::updateOrder($order); + } + + /** + * Remove a product from the session order + * + * @param Product $product + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException + */ + public static function removeProductFromSessionOrder(Product $product) + { + $order = self::getSessionOrder(); + + $order->removeProduct($product); + + self::updateOrder($order); + } + + /** + * Unsets the Order uid stored in session + * + * Call if the order has been completed or otherwise shouldn't be related to the session any longer. + */ + public static function removeOrderFromSession() + { + $GLOBALS['TSFE']->fe_user->setKey('ses', self::SESSION_KEY, null); + } + + /** + * Convenience function for running OrderRepository->update() + * + * @param Order $order + */ + public static function updateOrder(Order $order) + { + GeneralUtility::makeInstance(OrderRepository::class)->update($order); + } +} diff --git a/Classes/Utility/ProductUtility.php b/Classes/Utility/ProductUtility.php index f8d84920..5b6ee7f3 100644 --- a/Classes/Utility/ProductUtility.php +++ b/Classes/Utility/ProductUtility.php @@ -33,6 +33,7 @@ use Pixelant\PxaProductManager\Domain\Model\Product; use Pixelant\PxaProductManager\Domain\Repository\CategoryRepository; use Pixelant\PxaProductManager\Domain\Repository\ProductRepository; +use Pixelant\PxaProductManager\Exception\UnknownProductException; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; @@ -254,9 +255,7 @@ public static function formatPrice(float $price): string */ public static function getWishList(): array { - $list = $_COOKIE[self::WISH_LIST_COOKIE_NAME] ?: ''; - - return GeneralUtility::intExplode(',', $list, true); + return OrderUtility::getSessionOrder()->getProducts()->getArray(); } /** @@ -267,9 +266,32 @@ public static function getWishList(): array */ public static function isProductInWishList($product): bool { - $list = $_COOKIE[self::WISH_LIST_COOKIE_NAME] ?: ''; + if (!is_object($product)) { + $product = self::getProductByUid($product); + } + return OrderUtility::getSessionOrder()->getProducts()->contains($product); + } + + /** + * Returns the product + * + * @param int $uid + * @return Product + * @throws UnknownProductException + */ + public static function getProductByUid(int $uid) + { + /** @var ProductRepository $productRepository */ + $productRepository = GeneralUtility::makeInstance(ProductRepository::class); + + /** @var Product $product */ + $product = $productRepository->findByUid($uid); + + if ($product === null) { + throw new UnknownProductException('Product with UID ' . $uid . ' does not exist.'); + } - return GeneralUtility::inList($list, is_object($product) ? $product->getUid() : (int)$product); + return $product; } /** From 5b9670128aae66078b00e5304d8974f6298eed63 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 11:07:42 +0100 Subject: [PATCH 03/62] [TASK] Find coupons by case insensitive code search --- Classes/Domain/Repository/CouponRepository.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Classes/Domain/Repository/CouponRepository.php b/Classes/Domain/Repository/CouponRepository.php index a5fdc694..b94fe2a0 100644 --- a/Classes/Domain/Repository/CouponRepository.php +++ b/Classes/Domain/Repository/CouponRepository.php @@ -45,8 +45,16 @@ class CouponRepository extends Repository * * @return Coupon|null */ - public function findByCodeCaseInsensitive(string $code): Coupon + public function findByCaseInsensitiveCode(string $code): Coupon { + $query = $this->createQuery(); + /** @var Coupon|null $coupon */ + $coupon = $query + ->matching($query->like('code', $code, false)) + ->execute() + ->getFirst(); + + return $coupon; } } From 80cb5f4913530da2988bc65db78c80f696645141 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 11:45:26 +0100 Subject: [PATCH 04/62] [TASK] Created OrderUtility --- Classes/Utility/OrderUtility.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index d50660fa..af63a4f9 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -10,6 +10,7 @@ use Pixelant\PxaProductManager\Exception\UnknownProductException; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException; +use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface; /*************************************************************** @@ -78,7 +79,7 @@ public static function getSessionOrder() $orderRepository->add($order); /** @var PersistenceManagerInterface $persistanceManager */ - $persistanceManager = GeneralUtility::makeInstance(PersistenceManagerInterface::class); + $persistanceManager = GeneralUtility::makeInstance(PersistenceManager::class); //Make sure we get a UID $persistanceManager->persistAll(); From 386c181fb3a3f4593245cc22149e99a57ea4ceed Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 11:45:43 +0100 Subject: [PATCH 05/62] [TASK] Created UnknownProductException --- Classes/Exception/UnknownProductException.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Classes/Exception/UnknownProductException.php diff --git a/Classes/Exception/UnknownProductException.php b/Classes/Exception/UnknownProductException.php new file mode 100644 index 00000000..dfbc4f34 --- /dev/null +++ b/Classes/Exception/UnknownProductException.php @@ -0,0 +1,12 @@ + Date: Fri, 15 Nov 2019 11:46:04 +0100 Subject: [PATCH 06/62] [TASK] Added new translation labels --- Resources/Private/Language/locallang.xlf | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 8c42560a..69d77f53 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -214,6 +214,18 @@ Enter discount code + + No discount code was supplied. Please enter the code in the text field before clicking the Add discount code button. + + + The coupon code you supplied could not be found. Please check that you entered it correctly. + + + You cannot add a coupon code twice. The coupon code you supplied was already in the list. + + + The coupon code was successfully applied. + From c5e84043a2e54fc9554843ecf33e7924d5cad758 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 11:46:44 +0100 Subject: [PATCH 07/62] [TASK] addCoupon and removeCoupon convenience functions in Order class --- Classes/Domain/Model/Order.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index efdb07ca..344760e7 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -415,6 +415,26 @@ public function setCoupons(ObjectStorage $coupons) $this->coupons = $coupons; } + /** + * Add a coupon + * + * @param Coupon $coupon + */ + public function addCoupon(Coupon $coupon) + { + $this->coupons->attach($coupon); + } + + /** + * Remove a coupon + * + * @param Coupon $coupon + */ + public function removeCoupon(Coupon $coupon) + { + $this->coupons->detach($coupon); + } + /** * The all-inclusive price at checkout time * From 05810a2b00cf2b649a2c11e1097cc3f2004fe1f1 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 11:47:13 +0100 Subject: [PATCH 08/62] [TASK] First full implementation of addCouponCodeToOrderAction in ProductController --- Classes/Controller/ProductController.php | 62 ++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/Classes/Controller/ProductController.php b/Classes/Controller/ProductController.php index 9d27d125..85f0ba05 100644 --- a/Classes/Controller/ProductController.php +++ b/Classes/Controller/ProductController.php @@ -10,13 +10,16 @@ use Pixelant\PxaProductManager\Domain\Model\OrderConfiguration; use Pixelant\PxaProductManager\Domain\Model\OrderFormField; use Pixelant\PxaProductManager\Domain\Model\Product; +use Pixelant\PxaProductManager\Domain\Repository\CouponRepository; use Pixelant\PxaProductManager\Service\OrderMailService; use Pixelant\PxaProductManager\Utility\ConfigurationUtility; use Pixelant\PxaProductManager\Utility\MainUtility; +use Pixelant\PxaProductManager\Utility\OrderUtility; use Pixelant\PxaProductManager\Utility\ProductUtility; use Pixelant\PxaProductManager\Validation\ValidatorResolver; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\VisibilityAspect; +use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\StringUtility; @@ -287,7 +290,52 @@ public function wishListAction(bool $sendOrder = false) * @throws \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException */ public function addCouponCodeToOrderAction($couponCode = '') { - + if($couponCode === '') { + $this->addFlashMessage( + $this->translate('fe.couponCode.noCodeSuppliedWarning'), + '', + FlashMessage::WARNING + ); + + $this->redirect('wishList'); + } + + /** @var CouponRepository $couponRepository */ + $couponRepository = GeneralUtility::makeInstance(CouponRepository::class); + + $coupon = $couponRepository->findByCaseInsensitiveCode($couponCode); + + if ($coupon === null) { + $this->addFlashMessage( + $this->translate('fe.couponCode.codeNotFoundError'), + '', + FlashMessage::ERROR + ); + + $this->redirect('wishList'); + } + + $order = OrderUtility::getSessionOrder(); + + if ($order->getCoupons()->contains($coupon)) { + $this->addFlashMessage( + $this->translate('fe.couponCode.codeAlreadyAddedWarning'), + '', + FlashMessage::WARNING + ); + + $this->redirect('wishList'); + } + + $order->addCoupon($coupon); + + $this->orderRepository->update($order); + + $this->addFlashMessage( + $this->translate('fe.couponCode.newCodeAdded'), + '', + FlashMessage::OK + ); $this->redirect('wishList'); } @@ -930,11 +978,15 @@ protected function buildProductCanonicalUrl(Product $product) */ protected function getProductsFromCookieList($cookieName, int $excludeProduct = 0, int $limit = 0) { - $productUids = array_key_exists($cookieName, $_COOKIE) - ? GeneralUtility::intExplode(',', $_COOKIE[$cookieName], true) - : []; + if($cookieName === ProductUtility::WISH_LIST_COOKIE_NAME) { + $products = OrderUtility::getSessionOrder()->getProducts()->getArray(); + } else { + $productUids = array_key_exists($cookieName, $_COOKIE) + ? GeneralUtility::intExplode(',', $_COOKIE[$cookieName], true) + : []; - $products = $this->getProductByUidsList($productUids, $excludeProduct); + $products = $this->getProductByUidsList($productUids, $excludeProduct); + } if ($limit && count($products) > $limit) { $products = array_slice($products, 0, $limit); From 7e3f9a32b193bfb92eae5389fb9a3f4ae9fce01f Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 11:53:21 +0100 Subject: [PATCH 09/62] [TASK] DB schema fixes --- .../TCA/tx_pxaproductmanager_domain_model_order.php | 4 ++-- ext_tables.sql | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php index a35e0058..319a2484 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php @@ -25,11 +25,11 @@ 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/cart_tca.svg' ], 'interface' => [ - 'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, serialized_products_quantity, serialized_order_fields, external_id, fe_user, checkout_type', + 'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, serialized_products_quantity, serialized_order_fields, external_id, fe_user, checkout_type, price_at_checkout, tax_at_checkout', ], 'types' => [ '1' => [ - 'showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, fe_user, checkout_type, + 'showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, fe_user, checkout_type, price_at_checkout, tax_at_checkout --div--;' . $ll . '.order_fields,|order_fields|, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime' ], diff --git a/ext_tables.sql b/ext_tables.sql index 6bf20b66..5333c874 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -434,6 +434,8 @@ CREATE TABLE tx_pxaproductmanager_domain_model_order ( external_id varchar(255) DEFAULT '' NOT NULL, checkout_type varchar(255) DEFAULT 'default' NOT NULL, coupons int(11) unsigned DEFAULT '0' NOT NULL, + price_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, + tax_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, tstamp int(11) unsigned DEFAULT '0' NOT NULL, crdate int(11) unsigned DEFAULT '0' NOT NULL, @@ -466,9 +468,9 @@ CREATE TABLE tx_pxaproductmanager_order_product_mm ( ); # -# Table structure for table 'tx_pxaproductmanager_order_coupons_mm' +# Table structure for table 'tx_pxaproductmanager_order_coupon_mm' # -CREATE TABLE tx_pxaproductmanager_order_product_mm ( +CREATE TABLE tx_pxaproductmanager_order_coupon_mm ( uid_local int(11) unsigned DEFAULT '0' NOT NULL, uid_foreign int(11) unsigned DEFAULT '0' NOT NULL, sorting int(11) unsigned DEFAULT '0' NOT NULL, From a5c4a49f4cd9ae62494fc370b20672403ba879e3 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 12:30:44 +0100 Subject: [PATCH 10/62] [TASK] Misc. parsing and execution errors fixed --- Classes/Controller/ProductController.php | 2 +- Classes/Domain/Repository/CouponRepository.php | 2 +- Classes/Utility/OrderUtility.php | 3 ++- Configuration/FlexForms/flexform_pi1.xml | 4 ++-- ext_localconf.php | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Classes/Controller/ProductController.php b/Classes/Controller/ProductController.php index 85f0ba05..ea64b6aa 100644 --- a/Classes/Controller/ProductController.php +++ b/Classes/Controller/ProductController.php @@ -301,7 +301,7 @@ public function addCouponCodeToOrderAction($couponCode = '') { } /** @var CouponRepository $couponRepository */ - $couponRepository = GeneralUtility::makeInstance(CouponRepository::class); + $couponRepository = $this->objectManager->get(CouponRepository::class); $coupon = $couponRepository->findByCaseInsensitiveCode($couponCode); diff --git a/Classes/Domain/Repository/CouponRepository.php b/Classes/Domain/Repository/CouponRepository.php index b94fe2a0..28058896 100644 --- a/Classes/Domain/Repository/CouponRepository.php +++ b/Classes/Domain/Repository/CouponRepository.php @@ -45,7 +45,7 @@ class CouponRepository extends Repository * * @return Coupon|null */ - public function findByCaseInsensitiveCode(string $code): Coupon + public function findByCaseInsensitiveCode(string $code): ?Coupon { $query = $this->createQuery(); diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index af63a4f9..be7fa41f 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -9,6 +9,7 @@ use Pixelant\PxaProductManager\Domain\Repository\ProductRepository; use Pixelant\PxaProductManager\Exception\UnknownProductException; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException; use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface; @@ -171,6 +172,6 @@ public static function removeOrderFromSession() */ public static function updateOrder(Order $order) { - GeneralUtility::makeInstance(OrderRepository::class)->update($order); + GeneralUtility::makeInstance(ObjectManager::class)->get(OrderRepository::class)->update($order); } } diff --git a/Configuration/FlexForms/flexform_pi1.xml b/Configuration/FlexForms/flexform_pi1.xml index 96c4ac76..fff4c023 100644 --- a/Configuration/FlexForms/flexform_pi1.xml +++ b/Configuration/FlexForms/flexform_pi1.xml @@ -44,7 +44,7 @@ LLL:EXT:pxa_product_manager/Resources/Private/Language/locallang_be.xlf:flexform.mode.product_wish_list - Product->wishList;Product->finishOrder + Product->wishList;Product->finishOrder;Product->addCouponCodeToOrder LLL:EXT:pxa_product_manager/Resources/Private/Language/locallang_be.xlf:flexform.mode.product_compare_pre_view @@ -722,4 +722,4 @@ - \ No newline at end of file + diff --git a/ext_localconf.php b/ext_localconf.php index 9154c639..1edbf9da 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -11,7 +11,7 @@ function ($_EXTKEY) { 'Pixelant.' . $_EXTKEY, 'Pi1', [ - 'Product' => 'list, show, wishList, finishOrder, lazyList, comparePreView, compareView, groupedList, promotionList', + 'Product' => 'list, show, wishList, finishOrder, lazyList, comparePreView, compareView, groupedList, promotionList, addCouponCodeToOrder', 'Navigation' => 'show', 'AjaxProducts' => 'ajaxLazyList, latestVisited', 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct', @@ -19,7 +19,7 @@ function ($_EXTKEY) { ], // non-cacheable actions [ - 'Product' => 'wishList, finishOrder, comparePreView, compareView', + 'Product' => 'wishList, finishOrder, comparePreView, compareView, addCouponCodeToOrder', 'AjaxProducts' => 'ajaxLazyList, latestVisited', 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct' ] From cced52951ef45dc7e65ae06bde4dfd301202625f Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 13:23:58 +0100 Subject: [PATCH 11/62] [TASK] Basic coupon listing implemented --- Classes/Controller/ProductController.php | 7 +++++-- .../Private/Partials/WishList/RowsWithOrder.html | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Classes/Controller/ProductController.php b/Classes/Controller/ProductController.php index ea64b6aa..acb79bb8 100644 --- a/Classes/Controller/ProductController.php +++ b/Classes/Controller/ProductController.php @@ -232,6 +232,8 @@ public function wishListCartAction() */ public function wishListAction(bool $sendOrder = false) { + $order = OrderUtility::getSessionOrder(); + // Select the checkout system to use $checkoutToUse = $this->settings['wishList']['checkoutSystem'] ?: ConfigurationUtility::getExtManagerConfigurationByPath('checkoutSystem') ?: 'default'; @@ -278,9 +280,10 @@ public function wishListAction(bool $sendOrder = false) $this->view->assignMultiple([ 'checkout' => $checkout, - 'products' => $this->getProductsFromCookieList(ProductUtility::WISH_LIST_COOKIE_NAME), + 'products' => $order->getProducts(), 'orderProducts' => $orderState ?? [], - 'sendOrder' => $sendOrder + 'sendOrder' => $sendOrder, + 'coupons' => $order->getCoupons() ]); } diff --git a/Resources/Private/Partials/WishList/RowsWithOrder.html b/Resources/Private/Partials/WishList/RowsWithOrder.html index 0a609029..ab0ea845 100644 --- a/Resources/Private/Partials/WishList/RowsWithOrder.html +++ b/Resources/Private/Partials/WishList/RowsWithOrder.html @@ -21,6 +21,13 @@ + +
+ +
+
+
@@ -207,6 +214,10 @@
+ + {coupon.code} + +
From ab69732bedc8342ed7092f62c425e51381cf9d68 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 14:29:32 +0100 Subject: [PATCH 12/62] [TASK] Correct path used for wishlist limit --- Classes/Utility/OrderUtility.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index be7fa41f..8ca964ba 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -121,15 +121,15 @@ public static function removeProductUidFromSessionOrder(int $uid) */ public static function addProductToSessionOrder(Product $product) { - $settings = ConfigurationUtility::getSettingsByPath('pagePid'); + $wishListLimitFromSettings = ConfigurationUtility::getSettingsByPath('wishList/limit'); $order = self::getSessionOrder(); //If there are too many products, remove one. if ( - ((int) $settings['wishList']['limit'] > 0 && $order->getProducts()->count() + 1 > (int) $settings['wishList']['limit']) + ((int) $wishListLimitFromSettings > 0 && $order->getProducts()->count() + 1 > (int) $settings['wishList']['limit']) || - ((int) $settings['wishList']['limit'] === 0 && $order->getProducts()->count() + 1 > self::MAX_PRODUCTS) + ((int) $wishListLimitFromSettings === 0 && $order->getProducts()->count() + 1 > self::MAX_PRODUCTS) ) { $order->getProducts()->rewind(); $order->removeProduct($order->getProducts()->current()); From 251a5da2c6ab017605c28864e95d8e33af0d2aa7 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 15 Nov 2019 14:32:09 +0100 Subject: [PATCH 13/62] [TASK] Correct path used for wishlist limit --- Classes/Utility/OrderUtility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index 8ca964ba..a7292e67 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -127,7 +127,7 @@ public static function addProductToSessionOrder(Product $product) //If there are too many products, remove one. if ( - ((int) $wishListLimitFromSettings > 0 && $order->getProducts()->count() + 1 > (int) $settings['wishList']['limit']) + ((int) $wishListLimitFromSettings > 0 && $order->getProducts()->count() + 1 > (int) $wishListLimitFromSettings) || ((int) $wishListLimitFromSettings === 0 && $order->getProducts()->count() + 1 > self::MAX_PRODUCTS) ) { From 212571e8965fda178ec4eb6d57935b95671eec4b Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Fri, 15 Nov 2019 18:09:12 +0200 Subject: [PATCH 14/62] [TASK] Move FE price calculation to Backend --- Classes/Controller/AjaxJsonController.php | 31 ++++++ .../InvalidPriceCalculationException.php | 12 +++ Classes/Exception/OrderNotFoundException.php | 12 +++ Classes/Factory/PriceServiceFactory.php | 57 ++++++++++ Classes/Service/PriceService.php | 6 +- Configuration/TypoScript/setup.txt | 1 + .../Partials/WishList/RowsWithOrder.html | 5 +- .../JavaScript/ProductManager.Settings.js | 5 +- .../JavaScript/ProductManager.WishList.js | 102 ++++-------------- ext_localconf.php | 4 +- 10 files changed, 145 insertions(+), 90 deletions(-) create mode 100644 Classes/Exception/InvalidPriceCalculationException.php create mode 100644 Classes/Exception/OrderNotFoundException.php create mode 100644 Classes/Factory/PriceServiceFactory.php diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index 8d75b947..231736d6 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -27,6 +27,8 @@ ***************************************************************/ use Pixelant\PxaProductManager\Domain\Model\Product; +use Pixelant\PxaProductManager\Exception\InvalidPriceCalculationException; +use Pixelant\PxaProductManager\Factory\PriceServiceFactory; use Pixelant\PxaProductManager\Utility\MainUtility; use Pixelant\PxaProductManager\Utility\ProductUtility; use TYPO3\CMS\Extbase\Mvc\View\JsonView; @@ -178,4 +180,33 @@ public function addLatestVisitedProductAction(Product $product) $this->view->assign('value', ['success' => true]); } + + /** + * @return mixed + */ + public function totalOrderPricesAction() + { + try { + $priceService = (new PriceServiceFactory())->createFromSession(); + + $totalPrice = $priceService->calculateTotalPrice(); + $totalTaxPrice = $priceService->caluclateTotalTax(); + } catch (\Exception $e) { + $this->response->setStatus(500, 'Price calculation error'); + return null; + } + + if ($totalPrice <= 0 || $totalTaxPrice <= 0) { + $this->response->setStatus(500, 'Price calculation error'); + return null; + } + + $response = [ + 'totalPrice' => $totalPrice, + 'totalTaxPrice' => $totalTaxPrice + ]; + + $this->response->setStatus(200, 'OK'); + $this->view->assign('value', $response); + } } diff --git a/Classes/Exception/InvalidPriceCalculationException.php b/Classes/Exception/InvalidPriceCalculationException.php new file mode 100644 index 00000000..6fd74eed --- /dev/null +++ b/Classes/Exception/InvalidPriceCalculationException.php @@ -0,0 +1,12 @@ +setOrder($order); + return $priceService; + } +} diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index e412fd5d..feea2ea6 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -126,7 +126,8 @@ public function reset() */ public function calculateTotalPrice(): float { - + // TODO + return rand(20, 100); } /** @@ -136,7 +137,8 @@ public function calculateTotalPrice(): float */ public function caluclateTotalTax(): float { - + // TODO + return rand(1, 10); } /** diff --git a/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.txt index 2815ec72..6e99896f 100644 --- a/Configuration/TypoScript/setup.txt +++ b/Configuration/TypoScript/setup.txt @@ -303,6 +303,7 @@ PXA_PRODUCT_MANAGER_WISH_LIST { 3 = loadCompareList 4 = emptyCompareList 5 = loadWishList + 6 = totalOrderPrices } } } diff --git a/Resources/Private/Partials/WishList/RowsWithOrder.html b/Resources/Private/Partials/WishList/RowsWithOrder.html index ab0ea845..5a91192b 100644 --- a/Resources/Private/Partials/WishList/RowsWithOrder.html +++ b/Resources/Private/Partials/WishList/RowsWithOrder.html @@ -1,7 +1,10 @@ -
+
+> diff --git a/Resources/Public/JavaScript/ProductManager.Settings.js b/Resources/Public/JavaScript/ProductManager.Settings.js index 87408bd1..88ea282f 100644 --- a/Resources/Public/JavaScript/ProductManager.Settings.js +++ b/Resources/Public/JavaScript/ProductManager.Settings.js @@ -22,7 +22,8 @@ notInListClass: 'inactive-icon', initializationClass: 'ongoing-initialization', loadingClass: 'in-progress', - wishListButtonSingleView: '.btn-wish-list-single-view' + wishListButtonSingleView: '.btn-wish-list-single-view', + wishListContainer: '#pm-products-wishlist', }; if (ProductManager.settings.wishlistTSSettings) { @@ -70,4 +71,4 @@ }; w.ProductManager = ProductManager; -})(window); \ No newline at end of file +})(window); diff --git a/Resources/Public/JavaScript/ProductManager.WishList.js b/Resources/Public/JavaScript/ProductManager.WishList.js index cf613306..527fa6b4 100644 --- a/Resources/Public/JavaScript/ProductManager.WishList.js +++ b/Resources/Public/JavaScript/ProductManager.WishList.js @@ -53,7 +53,9 @@ ); ajaxLoadingInProgress = false; - _updatePriceAndTax(); + if ($(settings.wishListContainer).length > 0) { + _updatePriceAndTax(); + } _saveCurrentStateOfAmountOfProducts(); _trackOrderAmountChanges(); }; @@ -160,91 +162,24 @@ }); }; - /** - * Update total price if pricing enabled - * - * @returns {boolean} - * @private - */ - const _updateTotalPrice = function () { - if ($totalPrice.length === 0) { - return false; - } - - let sum = 0, - currencyFormat = $totalPrice.first().data('currency-format') || '', - numberFormat = $totalPrice.first().data('nubmer-format') || '', - format = ProductManager.Main.trimChar(numberFormat, '|').split('|'), - - decimals = parseInt(format[0]) || 2, - decimalSep = format[1] || '.', - thousandsSep = format[2] || ','; - - $orderItemsPrices.each(function () { - const $this = $(this); - - let productUid = parseInt($this.data('product-uid')); - if (productUid > 0) { - let $amountItem = $(_convertClassToIdWithProductId(settings.orderItemAmountClass, productUid)); - if ($amountItem.length === 1) { - let amount = parseInt($amountItem.val()); - sum += amount * parseFloat($this.data('price')); - } - } - }); - - $totalPrice.text( - sprintf( - currencyFormat, - ProductManager.Main.numberFormat(sum, decimals, decimalSep, thousandsSep) - ) - ); - }; + const _updatePriceAndTax = function () { + const uri = $(settings.wishListContainer).data('total-order-prices-ajax-uri'); - /** - * Update total tax if pricing enabled - * - * @returns {boolean} - * @private - */ - const _updateTotalTax = function () { - if ($totalTax.length === 0) { - return false; + if (uri.length <= 0) { + ProductManager.Messanger.showErrorMessage('Request failed: ' + 'Invalid url'); } - let sum = 0, - currencyFormat = $totalTax.first().data('currency-format') || '', - numberFormat = $totalTax.first().data('nubmer-format') || '', - format = ProductManager.Main.trimChar(numberFormat, '|').split('|'), - - decimals = parseInt(format[0]) || 2, - decimalSep = format[1] || '.', - thousandsSep = format[2] || ','; - - $orderItemsTaxes.each(function () { - const $this = $(this); - - let productUid = parseInt($this.data('product-uid')); - if (productUid > 0) { - let $amountItem = $(_convertClassToIdWithProductId(settings.orderItemAmountClass, productUid)); - if ($amountItem.length === 1) { - let amount = parseInt($amountItem.val()); - sum += amount * parseFloat($this.data('tax')); - } - } + $.ajax({ + url: uri, + dataType: 'json' + }).done(function (data) { + $totalPrice.text(data.totalPrice); + $totalTax.text(data.totalTaxPrice); + }).fail(function (jqXHR, textStatus) { + ProductManager.Messanger.showErrorMessage('Request failed: ' + textStatus); + }).always(function () { + ajaxLoadingInProgress = false; }); - - $totalTax.text( - sprintf( - currencyFormat, - ProductManager.Main.numberFormat(sum, decimals, decimalSep, thousandsSep) - ) - ); - }; - - const _updatePriceAndTax = function () { - _updateTotalPrice(); - _updateTotalTax(); }; /** @@ -259,6 +194,7 @@ } $orderItemsAmount.on('change', function () { + const $this = $(this); let value = parseInt($this.val()); @@ -370,4 +306,4 @@ })(); w.ProductManager = ProductManager; -})(window, $); \ No newline at end of file +})(window, $); diff --git a/ext_localconf.php b/ext_localconf.php index 1edbf9da..da9f4dad 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -14,14 +14,14 @@ function ($_EXTKEY) { 'Product' => 'list, show, wishList, finishOrder, lazyList, comparePreView, compareView, groupedList, promotionList, addCouponCodeToOrder', 'Navigation' => 'show', 'AjaxProducts' => 'ajaxLazyList, latestVisited', - 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct', + 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices', 'Filter' => 'showFilter' ], // non-cacheable actions [ 'Product' => 'wishList, finishOrder, comparePreView, compareView, addCouponCodeToOrder', 'AjaxProducts' => 'ajaxLazyList, latestVisited', - 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct' + 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices' ] ); // @codingStandardsIgnoreEnd From 20074438e79a8ba3ca03d934a111ed487d07253b Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 10:14:48 +0100 Subject: [PATCH 15/62] [TASK] Added Order::getProductQuantity(Product $product) Returns the quantity of a specific product in the order based on the supplied Product object. --- Classes/Domain/Model/Order.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 344760e7..001e2f1b 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -236,6 +236,17 @@ public function getProductsQuantity(): array return is_array($result) ? $result : []; } + /** + * Returns the quantity of a specific product in the order + * + * @param Product $product + * @return int + */ + public function getProductQuantity(Product $product) + { + return (int) $this->getProductsQuantity()[$product->getUid()]; + } + /** * Save products quantity as serialized string * From 704fbb618d33bf4f1d2118d5c7ca7f1b93daa94c Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 10:15:57 +0100 Subject: [PATCH 16/62] [TASK] Added static PriceService::formatForIso4217 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formats a value according to the lowest currency value, so €1.50 becomes the integer 150. --- Classes/Service/PriceService.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index e412fd5d..051c3558 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -178,4 +178,25 @@ public function calculatePriceBeforeCoupons(): float { } + + /** + * Format value an integer using the smallest unit of currency + * + * This adheres to the ISO 4217 standard used by most payment gateways + * https://en.wikipedia.org/wiki/ISO_4217 + * + * @param float $value Any currency value + * @param int $fractionalDigits Fractional digits in currency (default is 2) + * @param string $locale The PHP locale to use. Will override $fractionalDigits + * + * @return int + */ + public static function formatForIso4217(float $value, int $fractionalDigits = 2, string $locale = null) + { + if ($locale !== null) { + $fractionalDigits = localeconv($locale)['int_frac_digits']; + } + + return (int) pow($value, $fractionalDigits); + } } From e06abe164d4d9aabd9eaf36e450b850072dcb1c2 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 10:29:58 +0100 Subject: [PATCH 17/62] [TASK] Improved locale handling in PriceService::formatForIso4217() --- Classes/Service/PriceService.php | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index 051c3558..db5df01c 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -50,9 +50,9 @@ class PriceService protected $product; /** - * @var array + * @var Coupon */ - protected $coupons = []; + protected $coupon; /** * @return Order @@ -89,19 +89,19 @@ public function setProduct(Product $product): self } /** - * @return array + * @return Coupon */ - public function getCoupons(): array + public function getCoupon(): Coupon { - return $this->coupons; + return $this->coupon; } /** - * @param array $coupons + * @param Coupon $coupon */ - public function setCoupons(array $coupons): self + public function setCoupon(array $coupon): self { - $this->coupons = $coupons; + $this->coupon = $coupon; return $this; } @@ -114,7 +114,7 @@ public function reset() { $this->order = null; $this->product = null; - $this->coupons = []; + $this->coupon = null; return $this; } @@ -194,9 +194,17 @@ public function calculatePriceBeforeCoupons(): float public static function formatForIso4217(float $value, int $fractionalDigits = 2, string $locale = null) { if ($locale !== null) { - $fractionalDigits = localeconv($locale)['int_frac_digits']; + $oldLocale = setlocale(LC_ALL, 0); + setlocale(LC_ALL, $locale); + $fractionalDigits = localeconv()['int_frac_digits']; } - return (int) pow($value, $fractionalDigits); + $convertedValue = (int) pow($value, $fractionalDigits); + + if ($locale !== null) { + setlocale(LC_ALL, $oldLocale); + } + + return $convertedValue; } } From de5238599b9e713d6a25a1de296645c51c048f00 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 10:35:06 +0100 Subject: [PATCH 18/62] [TASK] Changed function name from plural to singular --- Classes/Service/PriceService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index db5df01c..5bbd5d2b 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -164,7 +164,7 @@ public function calculatePriceBeforeTax(): float * * @return float */ - public function calculatePriceBeforeTaxAndCoupons(): float + public function calculatePriceBeforeTaxAndCoupon(): float { } @@ -174,7 +174,7 @@ public function calculatePriceBeforeTaxAndCoupons(): float * * @return float */ - public function calculatePriceBeforeCoupons(): float + public function calculatePriceBeforeCoupon(): float { } From 86d9ac04a120e5cac552df0293cae98bcc006e9f Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 10:56:33 +0100 Subject: [PATCH 19/62] [TASK] Added getPriceForCheckout() and getTaxForCheckout() --- Classes/Domain/Model/Product.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Classes/Domain/Model/Product.php b/Classes/Domain/Model/Product.php index 83f6a70c..16e9d405 100644 --- a/Classes/Domain/Model/Product.php +++ b/Classes/Domain/Model/Product.php @@ -404,6 +404,16 @@ public function getPrice(): float return $this->price; } + /** + * Same as getPrice(), but override this method if you need to calculate price differently for the checkout + * + * @return float + */ + public function getPriceForCheckout(): float + { + return $this->getPrice(); + } + /** * Format price * @@ -1395,6 +1405,16 @@ public function getTax(): float return $this->getPrice() * ($this->getTaxRateRecursively() / 100); } + /** + * Same as getTax(), but override this method if you need to calculate price differently for the checkout + * + * @return float + */ + public function getTaxForCheckout() + { + return $this->getTax(); + } + /** * @return string */ From 77deae44583b2c67539ab9e7ca355a2282fd415f Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 11:54:40 +0100 Subject: [PATCH 20/62] [TASK] Full PriceService implementation --- Classes/Service/PriceService.php | 145 ++++++++++++++++++++++++++++--- 1 file changed, 134 insertions(+), 11 deletions(-) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index 3ce356c3..0c5658dd 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -57,7 +57,7 @@ class PriceService /** * @return Order */ - public function getOrder(): Order + public function getOrder(): ?Order { return $this->order; } @@ -74,7 +74,7 @@ public function setOrder(Order $order): self /** * @return Product */ - public function getProduct(): Product + public function getProduct(): ?Product { return $this->product; } @@ -91,7 +91,7 @@ public function setProduct(Product $product): self /** * @return Coupon */ - public function getCoupon(): Coupon + public function getCoupon(): ?Coupon { return $this->coupon; } @@ -124,10 +124,11 @@ public function reset() * * @return float */ - public function calculateTotalPrice(): float + public function calculatePrice(): float { - // TODO - return rand(20, 100); + return $this->calculateProductPriceBeforeTaxAndCoupon() + + $this->calculateTax() + + $this->calculateCouponValue(); } /** @@ -135,10 +136,22 @@ public function calculateTotalPrice(): float * * @return float */ - public function caluclateTotalTax(): float + public function calculateTax(): float { - // TODO - return rand(1, 10); + if ($this->order === null && $this->product === null) { + return 0.0; + } elseif ($this->order === null) { + return $this->product->getTax(); + } + + $value = 0.0; + + /** @var Product $product */ + foreach ($this->order->getProducts() as $product) { + $value += $product->getTaxForCheckout() * $this->order->getProductQuantity($product); + } + + return $value; } /** @@ -146,9 +159,19 @@ public function caluclateTotalTax(): float * * @return float */ - public function calculateTotalCouponValue(): float + public function calculateCouponValue(): float { + if (($this->order === null && $this->coupon === null) || ($this->order === null && $this->product === null)) { + return 0.0; + } + + $beforePrice = $this->calculateProductPriceBeforeTaxAndCoupon(); + + if ($this->order === null) { + return $this->applyCouponToValue($beforePrice) - $beforePrice; + } + return $this->applyOrderCouponsToValue($beforePrice) - $beforePrice; } /** @@ -158,7 +181,7 @@ public function calculateTotalCouponValue(): float */ public function calculatePriceBeforeTax(): float { - + return $this->calculatePriceBeforeTaxAndCoupon() - $this->calculateTax(); } /** @@ -168,7 +191,20 @@ public function calculatePriceBeforeTax(): float */ public function calculatePriceBeforeTaxAndCoupon(): float { + if ($this->order === null || $this->product !== null) { + return $this->calculateProductPriceBeforeTaxAndCoupon(); + } + + $total = 0.0; + + foreach ($this->order->getProducts() as $product) { + $this->setProduct($product); + $total += $this->calculateOrderTotalPriceForProductBeforeTaxAndCoupon(); + } + + $this->product = null; + return $total; } /** @@ -178,7 +214,39 @@ public function calculatePriceBeforeTaxAndCoupon(): float */ public function calculatePriceBeforeCoupon(): float { + return $this->calculatePrice() - $this->calculateCouponValue(); + } + + /** + * Get the product price + * + * @return float + */ + public function calculateProductPriceBeforeTaxAndCoupon(): float + { + if ($this->getProduct() === null) { + return 0.0; + } + + if($this->order === null) { + return $this->product->getPrice(); + } + + return $this->product->getPriceForCheckout(); + } + + /** + * Returns the product total (i.e. price * quantity) for product + * + * @return float + */ + public function calculateOrderTotalForProductBeforeTaxAndCoupon(): float + { + if ($this->order === null) { + return $this->calculateProductPriceBeforeTaxAndCoupon(); + } + return $this->order->getProductQuantity($this->getProduct()) * $this->calculateProductPriceBeforeTaxAndCoupon(); } /** @@ -209,4 +277,59 @@ public static function formatForIso4217(float $value, int $fractionalDigits = 2, return $convertedValue; } + + /** + * Apply the $this->coupon to the supplied value + * + * Returns zero if the resulting sum is less than zero + * + * @param float $value + * @return float + */ + protected function applyCouponToValue(float $value): float + { + if ($this->coupon === null) { + return $value; + } + + switch ($this->coupon->getType()) { + case Coupon::TYPE_CASH_REBATE: + $value -= $this->coupon->getValue(); + break; + case Coupon::TYPE_PERCENTAGE_REBATE: + $value -= $value * ($this->coupon->getValue() / 100); + break; + } + + //Make sure the coupon doesn't return a negative value + if ($value < 0) { + $value = 0.0; + } + + return $value; + } + + /** + * Applies the coupons in $this->order to the supplied value + * + * @param float $value + * @return float + */ + protected function applyOrderCouponsToValue(float $value): float + { + if ($this->order === null) { + return $value; + } + + $previousCoupon = $this->coupon; + + foreach ($this->order->getCoupons() as $coupon) { + $this->setCoupon($coupon); + $value = $this->applyCouponToValue($value); + } + + $this->coupon = $previousCoupon; + + return $value; + } } From 1d9a4cb7df2629470912fbeeca4b58e509f40f09 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 11:55:18 +0100 Subject: [PATCH 21/62] [TASK] Refactored PriceService calls --- Classes/Controller/AjaxJsonController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index 231736d6..ac4da032 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -189,8 +189,8 @@ public function totalOrderPricesAction() try { $priceService = (new PriceServiceFactory())->createFromSession(); - $totalPrice = $priceService->calculateTotalPrice(); - $totalTaxPrice = $priceService->caluclateTotalTax(); + $totalPrice = $priceService->calculatePrice(); + $totalTaxPrice = $priceService->calculateTax(); } catch (\Exception $e) { $this->response->setStatus(500, 'Price calculation error'); return null; From a7ccac09cfa7d4046fcbae16b054ccb36d4b9548 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 14:23:13 +0100 Subject: [PATCH 22/62] [TASK] Additional functions in PriceService --- Classes/Service/PriceService.php | 159 ++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 2 deletions(-) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index 0c5658dd..5a1eb6c3 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -119,6 +119,42 @@ public function reset() return $this; } + /** + * Resets the order + * + * @return $this; + */ + public function resetOrder(): self + { + $this->order = null; + + return $this; + } + + /** + * Resets the product + * + * @return $this; + */ + public function resetProduct(): self + { + $this->product = null; + + return $this; + } + + /** + * Resets the coupon + * + * @return $this; + */ + public function resetCoupon(): self + { + $this->coupon = null; + + return $this; + } + /** * Returns the total net price with all modifiers included * @@ -154,6 +190,26 @@ public function calculateTax(): float return $value; } + /** + * Returns the change in value before tax resulting from the applied coupons + * + * @return float + */ + public function calculateCouponValueBeforeTax(): float + { + if (($this->order === null && $this->coupon === null) || ($this->order === null && $this->product === null)) { + return 0.0; + } + + $beforePrice = $this->calculateProductPriceBeforeTaxAndCoupon(); + + if ($this->order === null) { + return $this->applyCouponToValue($beforePrice) - $beforePrice; + } + + return $this->applyOrderCouponsToValue($beforePrice) - $beforePrice; + } + /** * Returns the change in value resulting from the applied coupons * @@ -165,7 +221,7 @@ public function calculateCouponValue(): float return 0.0; } - $beforePrice = $this->calculateProductPriceBeforeTaxAndCoupon(); + $beforePrice = $this->calculateProductPrice(); if ($this->order === null) { return $this->applyCouponToValue($beforePrice) - $beforePrice; @@ -217,6 +273,60 @@ public function calculatePriceBeforeCoupon(): float return $this->calculatePrice() - $this->calculateCouponValue(); } + /** + * Calculates the tax for $this->product, including coupons + * + * @return float + */ + public function calculateProductTax() + { + if ($this->getProduct() === null) { + return 0.0; + } + + if($this->order === null) { + return $this->applyCouponToValue($this->product->getTax()); + } + + return $this->applyOrderCouponsToValue($this->product->getTaxForCheckout()); + } + + /** + * The product price without coupon, but including tax + * + * @return float + */ + public function calculateProductPriceBeforeCoupon(): float + { + if ($this->getProduct() === null) { + return 0.0; + } + + if($this->order === null) { + return $this->product->getPrice() + $this->product->getTax(); + } + + return $this->product->getPriceForCheckout() + $this->product->getTaxForCheckout(); + } + + /** + * The product price including tax and coupon + * + * @return float + */ + public function calculateProductPrice(): float + { + if ($this->getProduct() === null) { + return 0.0; + } + + if($this->order === null) { + return $this->applyCouponToValue($this->calculateProductPriceBeforeCoupon()); + } + + return $this->applyOrderCouponsToValue($this->calculateProductPriceBeforeCoupon()); + } + /** * Get the product price * @@ -236,7 +346,7 @@ public function calculateProductPriceBeforeTaxAndCoupon(): float } /** - * Returns the product total (i.e. price * quantity) for product + * Returns the product total (i.e. price * quantity) for product without tax and coupon codes * * @return float */ @@ -249,6 +359,51 @@ public function calculateOrderTotalForProductBeforeTaxAndCoupon(): float return $this->order->getProductQuantity($this->getProduct()) * $this->calculateProductPriceBeforeTaxAndCoupon(); } + /** + * Returns the product product in the order (i.e. price * quantity) + * + * @return float + */ + public function calculateOrderTotalForProduct(): float + { + if ($this->order === null) { + return $this->calculatePrice(); + } + + return $this->order->getProductQuantity($this->getProduct()) * $this->calculateProductPrice(); + } + + /** + * Returns the coupon value for the product in the order + * + * The resulting sum is the amount discounted from this product (including quantity) in the order. + * For discounts, it will be a positive sum. + * + * @return float + */ + public function calculateOrderTotalCouponValueForProduct(): float + { + if ($this->order === null) { + return $this->calculateCouponValue(); + } + + return $this->applyOrderCouponsToValue($this->calculateOrderTotalForProduct()) - $this->calculateOrderTotalForProduct(); + } + + /** + * Returns the total tax for within an order (i.e. tax * quantity) $this->product + * + * @return float + */ + public function calculateOrderTotalTaxForProduct(): float + { + if ($this->order === null) { + return $this->calculateProductTax(); + } + + return $this->applyOrderCouponsToValue($this->order->getProductQuantity($this->getProduct()) * $this->calculateProductTax()); + } + /** * Format value an integer using the smallest unit of currency * From a851639c531cd0c23de0324e1f49e7330affccfe Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 14:35:21 +0100 Subject: [PATCH 23/62] [TASK] Removed excess ">" --- Resources/Private/Partials/WishList/RowsWithOrder.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Private/Partials/WishList/RowsWithOrder.html b/Resources/Private/Partials/WishList/RowsWithOrder.html index 5a91192b..515764e6 100644 --- a/Resources/Private/Partials/WishList/RowsWithOrder.html +++ b/Resources/Private/Partials/WishList/RowsWithOrder.html @@ -4,7 +4,7 @@
-> + From da369c9791ab4b7a2317da04656b12d7efeb40f7 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 15:53:00 +0100 Subject: [PATCH 24/62] [TASK] Fixed issue with product counting in list --- Classes/Controller/AjaxJsonController.php | 30 +++++----- Classes/Controller/ProductController.php | 8 +-- Classes/Domain/Model/Order.php | 35 +++++++++++- Classes/Service/PriceService.php | 55 +++++++++++++++++-- Classes/Utility/OrderUtility.php | 4 +- .../Partials/WishList/RowsWithOrder.html | 2 +- 6 files changed, 103 insertions(+), 31 deletions(-) diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index ac4da032..2bc7da4c 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -30,6 +30,7 @@ use Pixelant\PxaProductManager\Exception\InvalidPriceCalculationException; use Pixelant\PxaProductManager\Factory\PriceServiceFactory; use Pixelant\PxaProductManager\Utility\MainUtility; +use Pixelant\PxaProductManager\Utility\OrderUtility; use Pixelant\PxaProductManager\Utility\ProductUtility; use TYPO3\CMS\Extbase\Mvc\View\JsonView; @@ -53,36 +54,36 @@ class AjaxJsonController extends AbstractController */ public function toggleWishListAction(Product $wishProduct = null) { + $order = OrderUtility::getSessionOrder(); + $response = [ - 'success' => false + 'success' => false, ]; $limit = (int)$this->settings['wishList']['limit']; if ($wishProduct !== null) { - $inWishList = ProductUtility::isProductInWishList($wishProduct); - - if (!$inWishList && count(ProductUtility::getWishList()) >= $limit) { + if ($order->getProductsQuantityTotal() + 1 > $limit) { $message = $this->translate('fe.error_limit'); } else { - MainUtility::{$inWishList ? 'removeValueFromListCookie' : 'addValueToListCookie'}( - ProductUtility::WISH_LIST_COOKIE_NAME, - $wishProduct->getUid(), - $limit - ); + $order->addProduct($wishProduct); + + $this->orderRepository->update($order); + + $response['success'] = true; + $response['inList'] = !$inWishList; $message = $this->translate( - $inWishList ? 'fe.remove_from_list' : 'fe.added_to_list', + 'fe.added_to_list', [ $this->translate('fe.wish_list') ] ); - - $response['success'] = true; - $response['inList'] = !$inWishList; } } + $response['itemCount'] = $order->getProductsQuantityTotal(); + $response['message'] = $message ?? $this->translate('fe.error_request'); $this->view->assign('value', $response); @@ -188,7 +189,6 @@ public function totalOrderPricesAction() { try { $priceService = (new PriceServiceFactory())->createFromSession(); - $totalPrice = $priceService->calculatePrice(); $totalTaxPrice = $priceService->calculateTax(); } catch (\Exception $e) { @@ -196,7 +196,7 @@ public function totalOrderPricesAction() return null; } - if ($totalPrice <= 0 || $totalTaxPrice <= 0) { + if ($totalPrice < 0 || $totalTaxPrice < 0) { $this->response->setStatus(500, 'Price calculation error'); return null; } diff --git a/Classes/Controller/ProductController.php b/Classes/Controller/ProductController.php index acb79bb8..f9448a17 100644 --- a/Classes/Controller/ProductController.php +++ b/Classes/Controller/ProductController.php @@ -272,16 +272,10 @@ public function wishListAction(bool $sendOrder = false) } } - if ($this->request->hasArgument('orderProducts')) { - $orderState = $this->request->getArgument('orderProducts'); - } else { - $orderState = ProductUtility::getOrderState(); - } - $this->view->assignMultiple([ 'checkout' => $checkout, 'products' => $order->getProducts(), - 'orderProducts' => $orderState ?? [], + 'orderProducts' => $order->getProductsQuantity(), 'sendOrder' => $sendOrder, 'coupons' => $order->getCoupons() ]); diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 001e2f1b..910ca27e 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -131,6 +131,7 @@ public function __construct() protected function initStorageObjects() { $this->products = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); + $this->coupons = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); } /** @@ -141,7 +142,11 @@ protected function initStorageObjects() */ public function addProduct(\Pixelant\PxaProductManager\Domain\Model\Product $product) { - $this->products->attach($product); + if ($this->getProductQuantity($product) < 1) { + $this->products->attach($product); + } + + $this->setProductQuantity($product, $this->getProductQuantity($product) + 1); } /** @@ -152,7 +157,11 @@ public function addProduct(\Pixelant\PxaProductManager\Domain\Model\Product $pro */ public function removeProduct(\Pixelant\PxaProductManager\Domain\Model\Product $productToRemove) { - $this->products->detach($productToRemove); + if ($this->getProductQuantity($productToRemove) <= 1) { + $this->products->detach($productToRemove); + } + + $this->setProductQuantity($productToRemove, $this->getProductQuantity($productToRemove) - 1); } /** @@ -236,6 +245,16 @@ public function getProductsQuantity(): array return is_array($result) ? $result : []; } + /** + * Returns the sum of products including the products quantity + * + * @return int + */ + public function getProductsQuantityTotal(): int + { + return array_sum($this->getProductsQuantity()); + } + /** * Returns the quantity of a specific product in the order * @@ -247,6 +266,18 @@ public function getProductQuantity(Product $product) return (int) $this->getProductsQuantity()[$product->getUid()]; } + /** + * Sets the quantity of a specific product + * @param Product $product + * @param int $quantity + */ + public function setProductQuantity(Product $product, int $quantity) + { + $productQuantities = $this->getProductsQuantity(); + $productQuantities[$product->getUid()] = $quantity; + $this->setProductsQuantity($productQuantities); + } + /** * Save products quantity as serialized string * diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index 5a1eb6c3..c15063b6 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -99,7 +99,7 @@ public function getCoupon(): ?Coupon /** * @param Coupon $coupon */ - public function setCoupon(array $coupon): self + public function setCoupon(Coupon $coupon): self { $this->coupon = $coupon; return $this; @@ -162,7 +162,7 @@ public function resetCoupon(): self */ public function calculatePrice(): float { - return $this->calculateProductPriceBeforeTaxAndCoupon() + return $this->calculatePriceBeforeTaxAndCoupon() + $this->calculateTax() + $this->calculateCouponValue(); } @@ -254,8 +254,8 @@ public function calculatePriceBeforeTaxAndCoupon(): float $total = 0.0; foreach ($this->order->getProducts() as $product) { - $this->setProduct($product); - $total += $this->calculateOrderTotalPriceForProductBeforeTaxAndCoupon(); + $this->product = $product; + $total += $this->calculateOrderTotalForProductBeforeTaxAndCoupon(); } $this->product = null; @@ -352,6 +352,10 @@ public function calculateProductPriceBeforeTaxAndCoupon(): float */ public function calculateOrderTotalForProductBeforeTaxAndCoupon(): float { + if ($this->product === null) { + return 0.0; + } + if ($this->order === null) { return $this->calculateProductPriceBeforeTaxAndCoupon(); } @@ -366,6 +370,10 @@ public function calculateOrderTotalForProductBeforeTaxAndCoupon(): float */ public function calculateOrderTotalForProduct(): float { + if ($this->product === null) { + return 0.0; + } + if ($this->order === null) { return $this->calculatePrice(); } @@ -383,6 +391,10 @@ public function calculateOrderTotalForProduct(): float */ public function calculateOrderTotalCouponValueForProduct(): float { + if ($this->product === null) { + return 0.0; + } + if ($this->order === null) { return $this->calculateCouponValue(); } @@ -397,6 +409,10 @@ public function calculateOrderTotalCouponValueForProduct(): float */ public function calculateOrderTotalTaxForProduct(): float { + if ($this->product === null) { + return 0.0; + } + if ($this->order === null) { return $this->calculateProductTax(); } @@ -487,4 +503,35 @@ protected function applyOrderCouponsToValue(float $value): float return $value; } + + /** + * Internal debugging function + * + * @internal + * @return array + */ + public function debugPrices():array + { + return [ + 'order' => $this->order !== null ? $this->order->getUid() : '', + 'product' => $this->product !== null ? $this->product->getUid() : '', + 'coupon' => $this->coupon !== null ? $this->coupon->getUid() : '', + 'productQuantitiesInOrder' => $this->order !== null ? $this->order->getProductsQuantity() : '', + 'calculateCouponValue' => $this->calculateCouponValue(), + 'calculateCouponValueBeforeTax' => $this->calculateCouponValueBeforeTax(), + 'calculateOrderTotalCouponValueForProduct' => $this->calculateOrderTotalCouponValueForProduct(), + 'calculateOrderTotalForProduct' => $this->calculateOrderTotalForProduct(), + 'calculateOrderTotalForProductBeforeTaxAndCoupon' => $this->calculateOrderTotalForProductBeforeTaxAndCoupon(), + 'calculateOrderTotalTaxForProduct' => $this->calculateOrderTotalTaxForProduct(), + 'calculatePrice' => $this->calculatePrice(), + 'calculatePriceBeforeCoupon' => $this->calculatePriceBeforeCoupon(), + 'calculatePriceBeforeTax' => $this->calculatePriceBeforeTax(), + 'calculatePriceBeforeTaxAndCoupon' => $this->calculatePriceBeforeTaxAndCoupon(), + 'calculateProductPrice' => $this->calculateProductPrice(), + 'calculateProductPriceBeforeCoupon' => $this->calculateProductPriceBeforeCoupon(), + 'calculateProductPriceBeforeTaxAndCoupon' => $this->calculateProductPriceBeforeTaxAndCoupon(), + 'calculateProductTax' => $this->calculateProductTax(), + 'calculateTax' => $this->calculateTax() + ]; + } } diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index a7292e67..f17fa025 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -127,9 +127,9 @@ public static function addProductToSessionOrder(Product $product) //If there are too many products, remove one. if ( - ((int) $wishListLimitFromSettings > 0 && $order->getProducts()->count() + 1 > (int) $wishListLimitFromSettings) + ((int) $wishListLimitFromSettings > 0 && $order->getProductsQuantityTotal() + 1 > (int) $wishListLimitFromSettings) || - ((int) $wishListLimitFromSettings === 0 && $order->getProducts()->count() + 1 > self::MAX_PRODUCTS) + ((int) $wishListLimitFromSettings === 0 && $order->getProductsQuantityTotal() + 1 > self::MAX_PRODUCTS) ) { $order->getProducts()->rewind(); $order->removeProduct($order->getProducts()->current()); diff --git a/Resources/Private/Partials/WishList/RowsWithOrder.html b/Resources/Private/Partials/WishList/RowsWithOrder.html index 515764e6..cf51c4e3 100644 --- a/Resources/Private/Partials/WishList/RowsWithOrder.html +++ b/Resources/Private/Partials/WishList/RowsWithOrder.html @@ -190,7 +190,7 @@ From c750163a13519fa0964a523ee74d977987deeb15 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 15:55:57 +0100 Subject: [PATCH 25/62] [TASK] Fixed issue where repository wasn't instanced correctly --- Classes/Utility/OrderUtility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index f17fa025..6885e217 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -62,7 +62,7 @@ class OrderUtility public static function getSessionOrder() { /** @var OrderRepository $orderRepository */ - $orderRepository = GeneralUtility::makeInstance(OrderRepository::class); + $orderRepository = GeneralUtility::makeInstance(ObjectManager::class)->get(OrderRepository::class); $orderUid = (int) $GLOBALS['TSFE']->fe_user->getKey('ses', self::SESSION_KEY); From 5fccaac6fdd0db237b4f9061e5fbe17390911810 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 16:21:55 +0100 Subject: [PATCH 26/62] [BUGFIX] Tax-related calculation errors are now gone --- Classes/Controller/AjaxJsonController.php | 2 +- Classes/Service/PriceService.php | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index 2bc7da4c..4a696680 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -71,7 +71,6 @@ public function toggleWishListAction(Product $wishProduct = null) $this->orderRepository->update($order); $response['success'] = true; - $response['inList'] = !$inWishList; $message = $this->translate( 'fe.added_to_list', @@ -189,6 +188,7 @@ public function totalOrderPricesAction() { try { $priceService = (new PriceServiceFactory())->createFromSession(); + $totalPrice = $priceService->calculatePrice(); $totalTaxPrice = $priceService->calculateTax(); } catch (\Exception $e) { diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index c15063b6..0a282767 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -162,9 +162,7 @@ public function resetCoupon(): self */ public function calculatePrice(): float { - return $this->calculatePriceBeforeTaxAndCoupon() - + $this->calculateTax() - + $this->calculateCouponValue(); + return $this->applyOrderCouponsToValue($this->calculatePriceBeforeTaxAndCoupon() + $this->calculateTax()); } /** @@ -339,10 +337,10 @@ public function calculateProductPriceBeforeTaxAndCoupon(): float } if($this->order === null) { - return $this->product->getPrice(); + return $this->product->getPrice() - $this->product->getTax(); } - return $this->product->getPriceForCheckout(); + return $this->product->getPriceForCheckout() - $this->product->getTaxForCheckout(); } /** From 61130e6733663ab90ce86fa958e7bad649a9be34 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sat, 16 Nov 2019 16:48:49 +0100 Subject: [PATCH 27/62] [TASK] Get correct cart count --- Resources/Public/JavaScript/ProductManager.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Resources/Public/JavaScript/ProductManager.js b/Resources/Public/JavaScript/ProductManager.js index d38b575d..d72256dd 100644 --- a/Resources/Public/JavaScript/ProductManager.js +++ b/Resources/Public/JavaScript/ProductManager.js @@ -157,22 +157,12 @@ * @param $cartCounters * @param modifier */ - updateCartCounter: function ($mainCartCounter, $cartCounters, modifier) { - modifier = modifier || 0; - - if ($mainCartCounter.length === 1) { - let currentValue = parseInt($mainCartCounter.text().trim()); - if (isNaN(currentValue)) { - currentValue = 0; - } - - let newValue = currentValue + modifier; - newValue = newValue > 0 ? newValue : 0; + updateCartCounter: function ($mainCartCounter, $cartCounters, newValue) { if ($cartCounters.length >= 1) { $cartCounters.text(newValue); } - } + }, /** @@ -341,4 +331,4 @@ $(document).ready(function () { ProductManager.Main.init(); -}); \ No newline at end of file +}); From 3f51a5e93bf8d0458e127db13faabc3ff8f31dc9 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 08:50:34 +0100 Subject: [PATCH 28/62] [BUGFIX] Corrected bug in formatForIso4217 --- Classes/Service/PriceService.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index 0a282767..bf99c656 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -235,7 +235,7 @@ public function calculateCouponValue(): float */ public function calculatePriceBeforeTax(): float { - return $this->calculatePriceBeforeTaxAndCoupon() - $this->calculateTax(); + return $this->calculatePriceBeforeTaxAndCoupon() + $this->calculateCouponValue(); } /** @@ -438,7 +438,7 @@ public static function formatForIso4217(float $value, int $fractionalDigits = 2, $fractionalDigits = localeconv()['int_frac_digits']; } - $convertedValue = (int) pow($value, $fractionalDigits); + $convertedValue = (int) $value * pow(10, $fractionalDigits); if ($locale !== null) { setlocale(LC_ALL, $oldLocale); @@ -522,6 +522,7 @@ public function debugPrices():array 'calculateOrderTotalForProductBeforeTaxAndCoupon' => $this->calculateOrderTotalForProductBeforeTaxAndCoupon(), 'calculateOrderTotalTaxForProduct' => $this->calculateOrderTotalTaxForProduct(), 'calculatePrice' => $this->calculatePrice(), + 'formatForIso4217(calculatePrice())' => self::formatForIso4217($this->calculatePrice()), 'calculatePriceBeforeCoupon' => $this->calculatePriceBeforeCoupon(), 'calculatePriceBeforeTax' => $this->calculatePriceBeforeTax(), 'calculatePriceBeforeTaxAndCoupon' => $this->calculatePriceBeforeTaxAndCoupon(), From 1958c6ad166932e609a9331134cf3bce65f3b456 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 08:58:23 +0100 Subject: [PATCH 29/62] [BUGFIX] Corrected bug in product price calculation where tax was included twice --- Classes/Service/PriceService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index bf99c656..3e9c6ef3 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -301,10 +301,10 @@ public function calculateProductPriceBeforeCoupon(): float } if($this->order === null) { - return $this->product->getPrice() + $this->product->getTax(); + return $this->product->getPrice(); } - return $this->product->getPriceForCheckout() + $this->product->getTaxForCheckout(); + return $this->product->getPriceForCheckout(); } /** @@ -314,7 +314,7 @@ public function calculateProductPriceBeforeCoupon(): float */ public function calculateProductPrice(): float { - if ($this->getProduct() === null) { + if ($this->product === null) { return 0.0; } From bfeef4717e19414816fba0af1a46c59c044afba1 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 09:00:01 +0100 Subject: [PATCH 30/62] [BUGFIX] Corrected bug in formatForIso4217 where value was cast to int too early --- Classes/Service/PriceService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index 3e9c6ef3..e86aae8f 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -438,7 +438,7 @@ public static function formatForIso4217(float $value, int $fractionalDigits = 2, $fractionalDigits = localeconv()['int_frac_digits']; } - $convertedValue = (int) $value * pow(10, $fractionalDigits); + $convertedValue = (int) ($value * pow(10, $fractionalDigits)); if ($locale !== null) { setlocale(LC_ALL, $oldLocale); From 3d10b05aa4f4e33dde1e602a2638f805cfdb31cb Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 09:39:37 +0100 Subject: [PATCH 31/62] [TASK] Changed task to being inclusive tax --- Classes/Domain/Model/Product.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Domain/Model/Product.php b/Classes/Domain/Model/Product.php index 16e9d405..abc8ba60 100644 --- a/Classes/Domain/Model/Product.php +++ b/Classes/Domain/Model/Product.php @@ -1402,7 +1402,8 @@ public function setCustomSorting(int $customSorting) */ public function getTax(): float { - return $this->getPrice() * ($this->getTaxRateRecursively() / 100); + // @TODO: Make it possible to define price as inclusive or inclusive tax + return $this->getPrice() - ($this->getPrice() / (($this->getTaxRateRecursively() / 100) + 1)); } /** From b22a0d341d93eef594ee26d8dec5a27b460067b6 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 10:33:50 +0100 Subject: [PATCH 32/62] [BUGFIX] Misc. fixes in price calculations --- Classes/Service/PriceService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php index e86aae8f..c80305f0 100644 --- a/Classes/Service/PriceService.php +++ b/Classes/Service/PriceService.php @@ -162,7 +162,7 @@ public function resetCoupon(): self */ public function calculatePrice(): float { - return $this->applyOrderCouponsToValue($this->calculatePriceBeforeTaxAndCoupon() + $this->calculateTax()); + return $this->applyOrderCouponsToValue($this->calculatePriceBeforeTaxAndCoupon()) + $this->calculateTax(); } /** @@ -185,7 +185,7 @@ public function calculateTax(): float $value += $product->getTaxForCheckout() * $this->order->getProductQuantity($product); } - return $value; + return $this->applyOrderCouponsToValue($value); } /** @@ -397,7 +397,7 @@ public function calculateOrderTotalCouponValueForProduct(): float return $this->calculateCouponValue(); } - return $this->applyOrderCouponsToValue($this->calculateOrderTotalForProduct()) - $this->calculateOrderTotalForProduct(); + return $this->calculateOrderTotalForProduct() - $this->applyOrderCouponsToValue($this->calculateOrderTotalForProduct()); } /** @@ -415,7 +415,7 @@ public function calculateOrderTotalTaxForProduct(): float return $this->calculateProductTax(); } - return $this->applyOrderCouponsToValue($this->order->getProductQuantity($this->getProduct()) * $this->calculateProductTax()); + return $this->order->getProductQuantity($this->getProduct()) * $this->calculateProductTax(); } /** From 8876dc2cd010bb80c6e429c0119b9f98e3d84c07 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 11:54:17 +0100 Subject: [PATCH 33/62] [BUGFIX] Corrected language label for 'usage_count' field --- Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php index a2ecce8d..29cf118c 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php @@ -130,7 +130,7 @@ ], 'usage_count' => [ 'exclude' => 1, - 'label' => $ll . '.usage_limit', + 'label' => $ll . '.usage_count', 'config' => [ 'type' => 'input', 'default' => 0, From 5ad3379c029c2350abb29c4104d890b36c06ea43 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 12:47:19 +0100 Subject: [PATCH 34/62] [TASK] Made it possible to remove items from order --- Classes/Controller/AjaxJsonController.php | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index 4a696680..d1cf7873 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -55,7 +55,6 @@ class AjaxJsonController extends AbstractController public function toggleWishListAction(Product $wishProduct = null) { $order = OrderUtility::getSessionOrder(); - $response = [ 'success' => false, ]; @@ -63,21 +62,27 @@ public function toggleWishListAction(Product $wishProduct = null) $limit = (int)$this->settings['wishList']['limit']; if ($wishProduct !== null) { - if ($order->getProductsQuantityTotal() + 1 > $limit) { - $message = $this->translate('fe.error_limit'); - } else { - $order->addProduct($wishProduct); - + if ($this->request->getArguments()['removeProduct']) { + $order->removeProduct($wishProduct); $this->orderRepository->update($order); - $response['success'] = true; - - $message = $this->translate( - 'fe.added_to_list', - [ - $this->translate('fe.wish_list') - ] - ); + } else { + if ($order->getProductsQuantityTotal() + 1 > $limit) { + $message = $this->translate('fe.error_limit'); + } else { + $order->addProduct($wishProduct); + + $this->orderRepository->update($order); + + $response['success'] = true; + + $message = $this->translate( + 'fe.added_to_list', + [ + $this->translate('fe.wish_list') + ] + ); + } } } From 0a583245246eaeb7e4b0bfd4235c1c341ee2d4ac Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 12:55:16 +0100 Subject: [PATCH 35/62] [TASK] Corrected coupon label --- Classes/Domain/Model/Coupon.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Classes/Domain/Model/Coupon.php b/Classes/Domain/Model/Coupon.php index 17edc9de..a28be286 100644 --- a/Classes/Domain/Model/Coupon.php +++ b/Classes/Domain/Model/Coupon.php @@ -84,7 +84,7 @@ class Coupon extends AbstractEntity */ public function getName(): string { - return $this->title; + return $this->name; } /** @@ -92,7 +92,7 @@ public function getName(): string */ public function setName(string $name) { - $this->title = $title; + $this->name = $name; } /** @@ -116,7 +116,7 @@ public function setCode(string $code) */ public function getUsageLimit(): int { - return $this->maxUses; + return $this->usageLimit; } /** @@ -124,7 +124,7 @@ public function getUsageLimit(): int */ public function setUsageLimit(int $usageLimit) { - $this->maxUses = $maxUses; + $this->usageLimit = $usageLimit; } /** @@ -132,7 +132,7 @@ public function setUsageLimit(int $usageLimit) */ public function getCostLimit(): float { - return $this->maxCost; + return $this->costLimit; } /** @@ -140,7 +140,7 @@ public function getCostLimit(): float */ public function setCostLimit(float $costLimit) { - $this->maxCost = $maxCost; + $this->costLimit = $costLimit; } /** From 6901ae643235418a82e4577744a66e8ebcf5c756 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 13:05:45 +0100 Subject: [PATCH 36/62] [TASK] Improved product removal. Still not removing the product from the order, though. --- Classes/Controller/AjaxJsonController.php | 2 ++ Classes/Domain/Model/Order.php | 15 +++++++++------ Classes/Utility/OrderUtility.php | 6 +++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index d1cf7873..9fc64063 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -64,7 +64,9 @@ public function toggleWishListAction(Product $wishProduct = null) if ($wishProduct !== null) { if ($this->request->getArguments()['removeProduct']) { $order->removeProduct($wishProduct); + $this->orderRepository->update($order); + $response['success'] = true; } else { if ($order->getProductsQuantityTotal() + 1 > $limit) { diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 910ca27e..1926d63e 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -157,11 +157,8 @@ public function addProduct(\Pixelant\PxaProductManager\Domain\Model\Product $pro */ public function removeProduct(\Pixelant\PxaProductManager\Domain\Model\Product $productToRemove) { - if ($this->getProductQuantity($productToRemove) <= 1) { - $this->products->detach($productToRemove); - } - - $this->setProductQuantity($productToRemove, $this->getProductQuantity($productToRemove) - 1); + $this->setProductQuantity($productToRemove, 0); + $this->products->detach($productToRemove); } /** @@ -274,7 +271,13 @@ public function getProductQuantity(Product $product) public function setProductQuantity(Product $product, int $quantity) { $productQuantities = $this->getProductsQuantity(); - $productQuantities[$product->getUid()] = $quantity; + + if ($quantity === 0) { + unset($productQuantities[$product->getUid()]); + } else { + $productQuantities[$product->getUid()] = $quantity; + } + $this->setProductsQuantity($productQuantities); } diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index 6885e217..753b4b43 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -132,7 +132,11 @@ public static function addProductToSessionOrder(Product $product) ((int) $wishListLimitFromSettings === 0 && $order->getProductsQuantityTotal() + 1 > self::MAX_PRODUCTS) ) { $order->getProducts()->rewind(); - $order->removeProduct($order->getProducts()->current()); + if ($order->getProductQuantity($order->getProducts()->current()) <= 1) { + $order->removeProduct($order->getProducts()->current()); + } else { + $order->setProductQuantity($order->getProducts()->current(), $order->getProductQuantity($order->getProducts()->current()) - 1); + } } $order->addProduct($product); From c5c81d06ea20e6bf755d45007b563aaad9916394 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 14:22:55 +0100 Subject: [PATCH 37/62] [BUGFIX] Changed target to source tags in locallang.xlf --- Resources/Private/Language/locallang.xlf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 69d77f53..7e1269b2 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -202,29 +202,29 @@ - Products + Products - Add discount code + Add discount code - Add discount code + Add discount code - Enter discount code + Enter discount code - No discount code was supplied. Please enter the code in the text field before clicking the Add discount code button. + No discount code was supplied. Please enter the code in the text field before clicking the Add discount code button. - The coupon code you supplied could not be found. Please check that you entered it correctly. + The coupon code you supplied could not be found. Please check that you entered it correctly. - You cannot add a coupon code twice. The coupon code you supplied was already in the list. + You cannot add a coupon code twice. The coupon code you supplied was already in the list. - The coupon code was successfully applied. + The coupon code was successfully applied. From 9fa9f54db7ebfd6201dc4351751e0254805bed74 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 14:45:07 +0100 Subject: [PATCH 38/62] [BUGFIX] Corrected language key in locallang.xlf --- Resources/Private/Language/locallang.xlf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 7e1269b2..5bafe816 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -223,7 +223,7 @@ You cannot add a coupon code twice. The coupon code you supplied was already in the list. - + The coupon code was successfully applied. From 9982841029e1fae09880fd9dfc204d3f234ac59b Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 16:00:24 +0100 Subject: [PATCH 39/62] [TASK] Session order as static property to avoid unnecessary DB queries --- Classes/Utility/OrderUtility.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index 753b4b43..fabbd851 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -53,6 +53,11 @@ class OrderUtility const MAX_PRODUCTS = 20; + /** + * @var Order $sessionOrder + */ + protected static $sessionOrder = null; + /** * Fetches the current session's order * @@ -68,10 +73,17 @@ public static function getSessionOrder() if ($orderUid > 0) { /** @var Order $order */ - $order = $orderRepository->findByUid($orderUid); + if (self::$sessionOrder !== null) { + $order = self::$sessionOrder; + } else { + $order = $orderRepository->findByUid($orderUid); + self::$sessionOrder = $order; + } if ($order !== null && !$order->isComplete()) { return $order; + } elseif ($order->isComplete()) { + self::$sessionOrder = null; } } From 9e5ac1ed36ce8e993045e4326f4cc43c94da67c8 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 16:06:22 +0100 Subject: [PATCH 40/62] [BUGFIX] Removing products from order now works as expected. Checking for product by UID rather than Object works best. --- Classes/Domain/Model/Order.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 1926d63e..153c7f7d 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -158,7 +158,13 @@ public function addProduct(\Pixelant\PxaProductManager\Domain\Model\Product $pro public function removeProduct(\Pixelant\PxaProductManager\Domain\Model\Product $productToRemove) { $this->setProductQuantity($productToRemove, 0); - $this->products->detach($productToRemove); + + foreach ($this->products as $product) { + if ($product->getUid() === $productToRemove->getUid()) { + $this->products->detach($product); + break; + } + } } /** From 748e194b71eaa00ec17c6f21cf2e84498551e868 Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 21:14:19 +0100 Subject: [PATCH 41/62] [TASK] Updated code field placeholder --- Resources/Private/Language/locallang.xlf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 5bafe816..f6ccce41 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -209,7 +209,7 @@ Add discount code - Add discount code + Enter code Enter discount code From 0af0321cbcd04ab49c3486d8ce1f4f5cb33a0d9d Mon Sep 17 00:00:00 2001 From: mabolek Date: Sun, 17 Nov 2019 21:55:13 +0100 Subject: [PATCH 42/62] [BUGFIX] Correct message is given when product is removed from list --- Classes/Controller/AjaxJsonController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index 9fc64063..ed3b39f6 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -68,6 +68,12 @@ public function toggleWishListAction(Product $wishProduct = null) $this->orderRepository->update($order); $response['success'] = true; + $message = $this->translate( + 'fe.remove_from_list', + [ + $this->translate('fe.wish_list') + ] + ); } else { if ($order->getProductsQuantityTotal() + 1 > $limit) { $message = $this->translate('fe.error_limit'); From b9bb0c477138f1dcf91d076229b1a7568e3fcded Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Wed, 20 Nov 2019 09:38:19 +0200 Subject: [PATCH 43/62] [TASK] WIP fix wishlist counters --- Classes/Controller/AjaxJsonController.php | 21 ++++++++++++-- Classes/Domain/Model/Order.php | 8 ++++++ Classes/Service/WishlistService.php | 23 +++++++++++++++ Classes/Utility/ProductUtility.php | 10 ++++++- Configuration/TypoScript/setup.txt | 1 + Resources/Private/Layouts/Default.html | 6 ++-- .../Templates/Product/WishListCart.html | 28 +++++++++++-------- .../JavaScript/ProductManager.Settings.js | 2 ++ Resources/Public/JavaScript/ProductManager.js | 19 +++++++++++-- ext_localconf.php | 4 +-- 10 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 Classes/Service/WishlistService.php diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index ed3b39f6..0fb7ec86 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -29,9 +29,11 @@ use Pixelant\PxaProductManager\Domain\Model\Product; use Pixelant\PxaProductManager\Exception\InvalidPriceCalculationException; use Pixelant\PxaProductManager\Factory\PriceServiceFactory; +use Pixelant\PxaProductManager\Service\WishlistService; use Pixelant\PxaProductManager\Utility\MainUtility; use Pixelant\PxaProductManager\Utility\OrderUtility; use Pixelant\PxaProductManager\Utility\ProductUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\View\JsonView; /** @@ -62,7 +64,10 @@ public function toggleWishListAction(Product $wishProduct = null) $limit = (int)$this->settings['wishList']['limit']; if ($wishProduct !== null) { - if ($this->request->getArguments()['removeProduct']) { + $inWishList = ProductUtility::isProductInWishList($wishProduct); + $response['inList'] = !$inWishList; + + if ($inWishList) { $order->removeProduct($wishProduct); $this->orderRepository->update($order); @@ -77,6 +82,7 @@ public function toggleWishListAction(Product $wishProduct = null) } else { if ($order->getProductsQuantityTotal() + 1 > $limit) { $message = $this->translate('fe.error_limit'); + unset($response['inList']); } else { $order->addProduct($wishProduct); @@ -94,8 +100,6 @@ public function toggleWishListAction(Product $wishProduct = null) } } - $response['itemCount'] = $order->getProductsQuantityTotal(); - $response['message'] = $message ?? $this->translate('fe.error_request'); $this->view->assign('value', $response); @@ -222,4 +226,15 @@ public function totalOrderPricesAction() $this->response->setStatus(200, 'OK'); $this->view->assign('value', $response); } + + /** + * @return int + */ + public function wishlistProductsCountAction() + { + /** @var WishlistService $wishlistService */ + $wishlistService = GeneralUtility::makeInstance(WishlistService::class); + $this->response->setStatus(200, 'OK'); + $this->view->assign('value', $wishlistService->productsCount() ?? 0); + } } diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 153c7f7d..7953b783 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -525,4 +525,12 @@ public function setTaxAtCheckout(float $taxAtCheckout) { $this->taxAtCheckout = $taxAtCheckout; } + + /** + * @return int + */ + public function getNumberOfProducts() + { + return $this->getProducts()->count(); + } } diff --git a/Classes/Service/WishlistService.php b/Classes/Service/WishlistService.php new file mode 100644 index 00000000..f4fa0f7f --- /dev/null +++ b/Classes/Service/WishlistService.php @@ -0,0 +1,23 @@ +getNumberOfProducts(); + } +} diff --git a/Classes/Utility/ProductUtility.php b/Classes/Utility/ProductUtility.php index 5b6ee7f3..a5954ea2 100644 --- a/Classes/Utility/ProductUtility.php +++ b/Classes/Utility/ProductUtility.php @@ -263,13 +263,21 @@ public static function getWishList(): array * * @param object|int $product * @return bool + * @throws UnknownProductException + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException */ public static function isProductInWishList($product): bool { if (!is_object($product)) { $product = self::getProductByUid($product); } - return OrderUtility::getSessionOrder()->getProducts()->contains($product); + + $wishlistUids = array_map(function ($item) { + return $item->getUid(); + }, OrderUtility::getSessionOrder()->getProducts()->toArray()); + + // TODO: objectStorage->contains() doesn't work here. Find out why + return in_array($product->getUid(), $wishlistUids); } /** diff --git a/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.txt index 6e99896f..c53406af 100644 --- a/Configuration/TypoScript/setup.txt +++ b/Configuration/TypoScript/setup.txt @@ -304,6 +304,7 @@ PXA_PRODUCT_MANAGER_WISH_LIST { 4 = emptyCompareList 5 = loadWishList 6 = totalOrderPrices + 7 = wishlistProductsCount } } } diff --git a/Resources/Private/Layouts/Default.html b/Resources/Private/Layouts/Default.html index fac8e8d3..b652b4ae 100644 --- a/Resources/Private/Layouts/Default.html +++ b/Resources/Private/Layouts/Default.html @@ -1,7 +1,9 @@
-
+
-
\ No newline at end of file +
diff --git a/Resources/Private/Templates/Product/WishListCart.html b/Resources/Private/Templates/Product/WishListCart.html index 4cfdb80c..a0cde09c 100644 --- a/Resources/Private/Templates/Product/WishListCart.html +++ b/Resources/Private/Templates/Product/WishListCart.html @@ -4,17 +4,21 @@ - - - - - - - - Wish list page is not set {$plugin.tx_pxaproductmanager.settings.wishList.pagePid} - +
+ + + + + + + + Wish list page is not set {$plugin.tx_pxaproductmanager.settings.wishList.pagePid} + +
diff --git a/Resources/Public/JavaScript/ProductManager.Settings.js b/Resources/Public/JavaScript/ProductManager.Settings.js index 88ea282f..052116cd 100644 --- a/Resources/Public/JavaScript/ProductManager.Settings.js +++ b/Resources/Public/JavaScript/ProductManager.Settings.js @@ -24,6 +24,8 @@ loadingClass: 'in-progress', wishListButtonSingleView: '.btn-wish-list-single-view', wishListContainer: '#pm-products-wishlist', + wishListCartContainer: '.pm-products-wishlist-cart:first', + productManagerMainWrapper: '.products-list-view-wrapper:first' }; if (ProductManager.settings.wishlistTSSettings) { diff --git a/Resources/Public/JavaScript/ProductManager.js b/Resources/Public/JavaScript/ProductManager.js index d72256dd..4ca3a85a 100644 --- a/Resources/Public/JavaScript/ProductManager.js +++ b/Resources/Public/JavaScript/ProductManager.js @@ -158,10 +158,23 @@ * @param modifier */ updateCartCounter: function ($mainCartCounter, $cartCounters, newValue) { + const uri = $(ProductManager.settings.wishList.wishListCartContainer).data('wishlist-product-count-ajax-uri'); - if ($cartCounters.length >= 1) { - $cartCounters.text(newValue); - } + if (!uri) { + ProductManager.Messanger.showErrorMessage('Request failed: ' + 'Invalid url'); + return false; + } + + $.ajax({ + url: uri, + dataType: 'json' + }).done(function (data) { + $cartCounters.text(data); + }).fail(function (jqXHR, textStatus) { + ProductManager.Messanger.showErrorMessage('Request failed: ' + textStatus); + }).always(function () { + ajaxLoadingInProgress = false; + }); }, diff --git a/ext_localconf.php b/ext_localconf.php index da9f4dad..62d1aa36 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -14,14 +14,14 @@ function ($_EXTKEY) { 'Product' => 'list, show, wishList, finishOrder, lazyList, comparePreView, compareView, groupedList, promotionList, addCouponCodeToOrder', 'Navigation' => 'show', 'AjaxProducts' => 'ajaxLazyList, latestVisited', - 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices', + 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices, wishlistProductsCount', 'Filter' => 'showFilter' ], // non-cacheable actions [ 'Product' => 'wishList, finishOrder, comparePreView, compareView, addCouponCodeToOrder', 'AjaxProducts' => 'ajaxLazyList, latestVisited', - 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices' + 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices, wishlistProductsCount' ] ); // @codingStandardsIgnoreEnd From 593be2f415665b59db94d08fe6392033df97eb7e Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Fri, 22 Nov 2019 14:37:53 +0200 Subject: [PATCH 44/62] [WIP] Add recurring payments renewals --- Classes/Domain/Model/Order.php | 56 ++++ Classes/Domain/Model/SubscriptionRenewal.php | 248 ++++++++++++++++++ .../Exception/NotARecurringOrderException.php | 9 + .../Service/SubscriptionRenewalService.php | 166 ++++++++++++ ...x_pxaproductmanager_domain_model_order.php | 19 +- ...nager_domain_model_subscriptionrenewal.php | 173 ++++++++++++ Resources/Private/Language/locallang_db.xlf | 119 ++++++--- ext_tables.sql | 30 +++ 8 files changed, 776 insertions(+), 44 deletions(-) create mode 100644 Classes/Domain/Model/SubscriptionRenewal.php create mode 100644 Classes/Exception/NotARecurringOrderException.php create mode 100644 Classes/Service/SubscriptionRenewalService.php create mode 100644 Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 153c7f7d..458d18d1 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -36,6 +36,19 @@ */ class Order extends AbstractEntity { + /** + * Recurring (Subscription) periods + */ + const RECURRING_FOR_WEEK = 1; + const RECURRING_FOR_MONTH = 2; + + const STATUS_ACTIVE = 1; + const STATUS_PAUSED = 2; + const STATUS_CANCELLED = 3; + + const WEEK_TIME_MODIFIER = '+5 days'; + const MONTH_TIME_MODIFIER = '+1 months'; + /** * @var bool */ @@ -111,6 +124,12 @@ class Order extends AbstractEntity */ protected $taxAtCheckout = 0.0; + /** + * + * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelant\PxaProductManager\Domain\Model\SubscriptionRenewal> + */ + protected $renewals = null; + /** * __construct */ @@ -132,6 +151,7 @@ protected function initStorageObjects() { $this->products = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); $this->coupons = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); + $this->renewals = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); } /** @@ -525,4 +545,40 @@ public function setTaxAtCheckout(float $taxAtCheckout) { $this->taxAtCheckout = $taxAtCheckout; } + + /** + * @return ObjectStorage + */ + public function getRenewals(): ObjectStorage + { + return $this->renewals; + } + + /** + * @param ObjectStorage $renewals + */ + public function setRenewals(ObjectStorage $renewals) + { + $this->renewals = $renewals; + } + + /** + * Add a renewal + * + * @param SubscriptionRenewal $renewal + */ + public function addRenewal(SubscriptionRenewal $renewal) + { + $this->renewals->attach($renewal); + } + + /** + * Remove a renewal + * + * @param SubscriptionRenewal $renewal + */ + public function removeRenewal(SubscriptionRenewal $renewal) + { + $this->renewals->detach($renewal); + } } diff --git a/Classes/Domain/Model/SubscriptionRenewal.php b/Classes/Domain/Model/SubscriptionRenewal.php new file mode 100644 index 00000000..128a1ca4 --- /dev/null +++ b/Classes/Domain/Model/SubscriptionRenewal.php @@ -0,0 +1,248 @@ +paymentDate; + } + + /** + * @param \DateTime $paymentDate + * @return SubscriptionRenewal + */ + public function setPaymentDate(\DateTime $paymentDate): SubscriptionRenewal + { + $this->paymentDate = $paymentDate; + return $this; + } + + /** + * @return \DateTime + */ + public function getPaymentNextTry(): \DateTime + { + return $this->paymentNextTry; + } + + /** + * @param \DateTime $paymentNextTry + * @return SubscriptionRenewal + */ + public function setPaymentNextTry(\DateTime $paymentNextTry): SubscriptionRenewal + { + $this->paymentNextTry = $paymentNextTry; + return $this; + } + + /** + * @return bool + */ + public function isPaymentDone(): bool + { + return $this->paymentDone; + } + + /** + * @param bool $paymentDone + * @return SubscriptionRenewal + */ + public function setPaymentDone(bool $paymentDone): SubscriptionRenewal + { + $this->paymentDone = $paymentDone; + return $this; + } + + /** + * @return \DateTime + */ + public function getShipmentDate(): \DateTime + { + return $this->shipmentDate; + } + + /** + * @param \DateTime $shipmentDate + * @return SubscriptionRenewal + */ + public function setShipmentDate(\DateTime $shipmentDate): SubscriptionRenewal + { + $this->shipmentDate = $shipmentDate; + return $this; + } + + /** + * @return \DateTime + */ + public function getShipmentNextTry(): \DateTime + { + return $this->shipmentNextTry; + } + + /** + * @param \DateTime $shipmentNextTry + * @return SubscriptionRenewal + */ + public function setShipmentNextTry(\DateTime $shipmentNextTry): SubscriptionRenewal + { + $this->shipmentNextTry = $shipmentNextTry; + return $this; + } + + /** + * @return bool + */ + public function isShipmentDone(): bool + { + return $this->shipmentDone; + } + + /** + * @param bool $shipmentDone + * @return SubscriptionRenewal + */ + public function setShipmentDone(bool $shipmentDone): SubscriptionRenewal + { + $this->shipmentDone = $shipmentDone; + return $this; + } + + /** + * @return int + */ + public function getPaymentAttemptsLeft(): int + { + return $this->paymentAttemptsLeft; + } + + /** + * @param int $paymentAttemptsLeft + * @return SubscriptionRenewal + */ + public function setPaymentAttemptsLeft(int $paymentAttemptsLeft): SubscriptionRenewal + { + $this->paymentAttemptsLeft = $paymentAttemptsLeft; + return $this; + } + + /** + * @return int + */ + public function getShipmentAttemptsLeft(): int + { + return $this->shipmentAttemptsLeft; + } + + /** + * @param int $shipmentAttemptsLeft + * @return SubscriptionRenewal + */ + public function setShipmentAttemptsLeft(int $shipmentAttemptsLeft): SubscriptionRenewal + { + $this->shipmentAttemptsLeft = $shipmentAttemptsLeft; + return $this; + } + + /** + * @return bool + */ + public function hasMorePaymentAttempts() + { + return $this->paymentAttemptsLeft > 0; + } + + /** + * @return bool + */ + public function hasMoreShipmentAttempts() + { + return $this->shipmentAttemptsLeft > 0; + } + + /** + * @return int + */ + public function decrementPaymentAttempt() + { + $attemptsLeft = $this->getPaymentAttemptsLeft(); + $attemptsLeft = ($attemptsLeft <= 0) ? 0 : $attemptsLeft - 1; + $this->setPaymentAttemptsLeft($attemptsLeft); + return $attemptsLeft; + } + + /** + * @return \DateTime + */ + public function makeNextPaymentTryTomorrow() + { + $nextTry = $this->getPaymentNextTry(); + $nextTry->modify('+1 day'); + $this->setPaymentNextTry($nextTry); + return $nextTry; + } +} diff --git a/Classes/Exception/NotARecurringOrderException.php b/Classes/Exception/NotARecurringOrderException.php new file mode 100644 index 00000000..10761d89 --- /dev/null +++ b/Classes/Exception/NotARecurringOrderException.php @@ -0,0 +1,9 @@ +today = \DateTime::createFromFormat('Y-m-d', '2019-11-22'); + $this->today = \DateTime::createFromFormat('Y-m-d', '2020-01-21'); + $this->order = $order; + if ($this->order->getRecurringPeriod() <= 0) { + throw new NotARecurringOrderException(); + } + } + + /** + * @return SubscriptionRenewal|null + */ + public function getNextRenewal() + { + $renewal = $this->getLatestRenewal(); + + if (!$renewal) { + $renewal = $this->createNextRenewal($renewal); + } + + return $renewal; + } + + /** + * @return SubscriptionRenewal|null + */ + protected function getLatestRenewal() + { + return array_pop($this->order->getRenewals()->toArray()); + } + + /** + * @param \DateTime $date + * @return SubscriptionRenewal + */ + public function addRenewal(\DateTime $date) + { + $newRenewal = $this->createRenewal($date); + + $this->order->addRenewal($newRenewal); + GeneralUtility::makeInstance(ObjectManager::class)->get(OrderRepository::class)->update($this->order); + GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); + return $newRenewal; + } + + /** + * @param SubscriptionRenewal|null $latestRenewal + * @return SubscriptionRenewal + */ + public function createNextRenewal(SubscriptionRenewal $latestRenewal = null) + { + if (!$latestRenewal) { + $latestRenewal = $this->getLatestRenewal(); + } + + // If no renewals exist yet + if (!$latestRenewal) { + return $this->addRenewal($this->order->getCrdate()); + } else { + $date = $this->getNextRenewalDate($latestRenewal); + return $this->addRenewal($date); + } + } + + /** + * @param SubscriptionRenewal $lastRenewal + * @return \DateTime + */ + public function getNextRenewalDate(SubscriptionRenewal $lastRenewal) + { + $nextRenewalDate = clone($lastRenewal->getPaymentDate()); + + switch ($this->order->getRecurringPeriod()) { + case Order::RECURRING_FOR_WEEK: + $timeModifier = Order::WEEK_TIME_MODIFIER; + break; + case Order::RECURRING_FOR_MONTH: + default: + $timeModifier = Order::MONTH_TIME_MODIFIER; + break; + } + + return $nextRenewalDate->modify($timeModifier); + } + + /** + * @param \DateTime $date + * @return SubscriptionRenewal + */ + protected function createRenewal(\DateTime $date) + { + $renewal = (new SubscriptionRenewal()) + ->setPaymentDate($date) + ->setPaymentNextTry($date) + ->setPaymentDone(false) + ->setPaymentAttemptsLeft($this->maxPaymentAttempts) + ->setShipmentDate($date) + ->setShipmentNextTry($date) + ->setShipmentDone(false) + ->setShipmentAttemptsLeft($this->maxShipmentAttempts) + ; + + $renewal->setPid($this->order->getPid()); + return $renewal; + } + + /** + * @param SubscriptionRenewal $renewal + * @return bool + */ + public function isOngoingPayment(SubscriptionRenewal $renewal) + { + return !$renewal->isPaymentDone() && $renewal->getPaymentAttemptsLeft() > 0; + } + + /** + * @param SubscriptionRenewal $renewal + * @return bool + * @throws \Exception + */ + public function isItTimeToMakePayment(SubscriptionRenewal $renewal) + { + return $this->today > $renewal->getPaymentNextTry(); + } +} diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php index 319a2484..4b5db3d1 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php @@ -25,13 +25,14 @@ 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/cart_tca.svg' ], 'interface' => [ - 'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, serialized_products_quantity, serialized_order_fields, external_id, fe_user, checkout_type, price_at_checkout, tax_at_checkout', + 'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, serialized_products_quantity, serialized_order_fields, external_id, fe_user, checkout_type, price_at_checkout, tax_at_checkout, renewals', ], 'types' => [ '1' => [ 'showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, fe_user, checkout_type, price_at_checkout, tax_at_checkout --div--;' . $ll . '.order_fields,|order_fields|, - --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime' + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime, + --div--;' . $ll . '.tabs.recurring_payments, renewals', ], ], 'columns' => [ @@ -246,5 +247,19 @@ 'eval' => 'double2' ], ], + 'renewals' => [ + 'exclude' => 1, + 'label' => $ll . '.renewals', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'tx_pxaproductmanager_domain_model_subscriptionrenewal', + 'foreign_field' => 'order', + 'foreign_sortby' => 'payment_date', + 'appearance' => [ + 'collapseAll' => 1, + 'expandSingle' => 1, + ], + ] + ] ] ]; diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php new file mode 100644 index 00000000..38cd4ab6 --- /dev/null +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php @@ -0,0 +1,173 @@ + [ + 'title' => $ll, + 'label' => 'payment_date', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'cruser_id' => 'cruser_id', + 'dividers2tabs' => true, + 'delete' => 'deleted', + 'default_sortby' => 'payment_date', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + ], + 'searchFields' => 'payment_date, shipment_date', +// 'hideTable' => true, + 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/subscription_renewal_tca.svg' + ], + 'interface' => [ + 'showRecordFieldList' => 'payment_date, payment_next_try, payment_done, payment_attempts_left, shipment_date, + shipment_next_try, shipment_done, shipment_attempts_left' + ], + 'types' => [ + '1' => [ + 'showitem' => 'payment_date, payment_next_try, payment_done, payment_attempts_left, shipment_date, + shipment_next_try, shipment_done, shipment_attempts_left' + ], + ], + 'columns' => [ + 'hidden' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.hidden', + 'config' => [ + 'type' => 'check', + 'items' => [ + '1' => [ + '0' => $llCore . 'locallang_core.xlf:labels.enabled' + ] + ], + ], + ], + 'starttime' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.starttime', + 'config' => [ + 'type' => 'input', + 'size' => 13, + 'eval' => 'datetime,int', + 'renderType' => 'inputDateTime', + 'default' => 0, + ] + ], + 'endtime' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.endtime', + 'config' => [ + 'type' => 'input', + 'size' => 13, + 'eval' => 'datetime,int', + 'default' => 0, + 'renderType' => 'inputDateTime', + 'range' => [ + 'upper' => mktime(0, 0, 0, 1, 1, 2038) + ] + ], + ], + 'payment_date' => [ + 'exclude' => true, + 'label' => $ll . '.payment_date', + 'config' => [ + 'type' => 'input', + 'renderType' => 'inputDateTime', + 'size' => 15, + 'eval' => 'datetime', + 'default' => time(), +// 'readOnly' => 1, +// 'format' => 'date' + ], + ], + 'payment_next_try' => [ + 'exclude' => true, + 'label' => $ll . '.payment_next_try', + 'config' => [ + 'type' => 'input', + 'renderType' => 'inputDateTime', + 'size' => 15, + 'eval' => 'datetime', + 'default' => time(), +// 'readOnly' => 1 + ], + ], + 'payment_done' => [ + 'exclude' => true, + 'label' => $ll . '.payment_done', + 'config' => [ + 'type' => 'check', + 'items' => [ + '1' => [ + '0' => $llCore . 'locallang_core.xlf:labels.enabled' + ] + ], +// 'readOnly' => 1 + ], + ], + 'payment_attempts_left' => [ + 'exclude' => true, + 'label' => $ll . '.payment_attempts_left', + 'config' => [ + 'type' => 'input', + 'size' => 30, +// 'readOnly' => 1 + ], + ], + 'shipment_date' => [ + 'exclude' => true, + 'label' => $ll . '.shipment_date', + 'config' => [ + 'type' => 'input', + 'renderType' => 'inputDateTime', + 'size' => 15, + 'eval' => 'datetime', + 'default' => time(), +// 'format' => 'date', +// 'readOnly' => 1 + ], + ], + 'shipment_next_try' => [ + 'exclude' => true, + 'label' => $ll . '.shipment_next_try', + 'config' => [ + 'type' => 'input', + 'renderType' => 'inputDateTime', + 'size' => 15, + 'eval' => 'datetime', + 'default' => time(), +// 'readOnly' => 1 + ], + ], + 'shipment_done' => [ + 'exclude' => true, + 'label' => $ll . '.shipment_done', + 'config' => [ + 'type' => 'check', + 'items' => [ + '1' => [ + '0' => $llCore . 'locallang_core.xlf:labels.enabled' + ] + ], +// 'readOnly' => 1 + ], + ], + 'shipment_attempts_left' => [ + 'exclude' => true, + 'label' => $ll . '.payment_attempts_left', + 'config' => [ + 'type' => 'input', + 'size' => 30, +// 'readOnly' => 1 + ], + ], + 'order' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + ] +]; diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index f174b483..da9cfbb4 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -264,49 +264,55 @@ Checkout type - - Coupons - - - Price at checkout - - - Tax at checkout - + + Coupons + + + Price at checkout + + + Tax at checkout + + + Renewals + + + Recurring payments + - - Price coupon - - - Name - - - Coupon code (uppercase, dash, and underscore allowed) - - - Type - - - Cash rebate - - - Percentage rebate - - - Value - - - Max uses (0 = no limit) - - - Cost limit (0.00 = no limit) - - - Usage count - - - Total cost - + + Price coupon + + + Name + + + Coupon code (uppercase, dash, and underscore allowed) + + + Type + + + Cash rebate + + + Percentage rebate + + + Value + + + Max uses (0 = no limit) + + + Cost limit (0.00 = no limit) + + + Usage count + + + Total cost + @@ -441,6 +447,35 @@ Default
+ + + Subscription renewal + + + Payment date + + + Payment next try + + + Payment done + + + Payment attempts left + + + Shipment date + + + Shipment next try + + + Shipment done + + + Shipment attempts left + + diff --git a/ext_tables.sql b/ext_tables.sql index 5333c874..92c7f858 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -436,6 +436,7 @@ CREATE TABLE tx_pxaproductmanager_domain_model_order ( coupons int(11) unsigned DEFAULT '0' NOT NULL, price_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, tax_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, + renewals int(11) unsigned DEFAULT '0' NOT NULL, tstamp int(11) unsigned DEFAULT '0' NOT NULL, crdate int(11) unsigned DEFAULT '0' NOT NULL, @@ -575,3 +576,32 @@ CREATE TABLE tx_pxaproductmanager_domain_model_coupon ( KEY couponcode (code), KEY parent (pid) ); + +# +# Table structure for table 'tx_pxaproductmanager_domain_model_subscriptionrenewal' +# +CREATE TABLE tx_pxaproductmanager_domain_model_subscriptionrenewal ( + uid int(11) NOT NULL auto_increment, + pid int(11) DEFAULT '0' NOT NULL, + + payment_date int(11) unsigned DEFAULT '0' NOT NULL, + payment_next_try int(11) unsigned DEFAULT '0' NOT NULL, + payment_done tinyint(4) unsigned DEFAULT '0' NOT NULL, + payment_attempts_left int(11) unsigned DEFAULT '0' NOT NULL, + shipment_date int(11) unsigned DEFAULT '0' NOT NULL, + shipment_next_try int(11) unsigned DEFAULT '0' NOT NULL, + shipment_done tinyint(4) unsigned DEFAULT '0' NOT NULL, + shipment_attempts_left int(11) unsigned DEFAULT '0' NOT NULL, + order int(11) unsigned DEFAULT '0' NOT NULL, + + tstamp int(11) unsigned DEFAULT '0' NOT NULL, + crdate int(11) unsigned DEFAULT '0' NOT NULL, + cruser_id int(11) unsigned DEFAULT '0' NOT NULL, + deleted tinyint(4) unsigned DEFAULT '0' NOT NULL, + hidden tinyint(4) unsigned DEFAULT '0' NOT NULL, + starttime int(11) unsigned DEFAULT '0' NOT NULL, + endtime int(11) unsigned DEFAULT '0' NOT NULL, + + PRIMARY KEY (uid), + KEY parent (pid) +); From 6e439d04e77e754b5c6058902617c0ad44280f97 Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Mon, 25 Nov 2019 08:43:06 +0200 Subject: [PATCH 45/62] [TASK] Add payment is and status fields; Add today date faker setting --- Classes/Domain/Model/SubscriptionRenewal.php | 46 +++++++++++++++++++ .../Service/SubscriptionRenewalService.php | 16 ++++++- ...nager_domain_model_subscriptionrenewal.php | 28 +++++++++-- Configuration/TypoScript/setup.txt | 10 ++++ Resources/Private/Language/locallang_db.xlf | 6 +++ ext_tables.sql | 3 ++ 6 files changed, 103 insertions(+), 6 deletions(-) diff --git a/Classes/Domain/Model/SubscriptionRenewal.php b/Classes/Domain/Model/SubscriptionRenewal.php index 128a1ca4..cc3d62dd 100644 --- a/Classes/Domain/Model/SubscriptionRenewal.php +++ b/Classes/Domain/Model/SubscriptionRenewal.php @@ -36,6 +36,16 @@ class SubscriptionRenewal extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity */ protected $paymentAttemptsLeft = null; + /** + * @var string + */ + protected $paymentId; + + /** + * @var string + */ + protected $paymentStatus; + /** * $shipmentDate * @@ -190,6 +200,42 @@ public function setPaymentAttemptsLeft(int $paymentAttemptsLeft): SubscriptionRe return $this; } + /** + * @return string + */ + public function getPaymentId(): string + { + return $this->paymentId; + } + + /** + * @param string $paymentId + * @return SubscriptionRenewal + */ + public function setPaymentId(string $paymentId): SubscriptionRenewal + { + $this->paymentId = $paymentId; + return $this; + } + + /** + * @return string + */ + public function getPaymentStatus(): string + { + return $this->paymentStatus; + } + + /** + * @param string $paymentStatus + * @return SubscriptionRenewal + */ + public function setPaymentStatus(string $paymentStatus): SubscriptionRenewal + { + $this->paymentStatus = $paymentStatus; + return $this; + } + /** * @return int */ diff --git a/Classes/Service/SubscriptionRenewalService.php b/Classes/Service/SubscriptionRenewalService.php index 1fd375a1..0a495d3f 100644 --- a/Classes/Service/SubscriptionRenewalService.php +++ b/Classes/Service/SubscriptionRenewalService.php @@ -7,6 +7,7 @@ use Pixelant\PxaProductManager\Domain\Model\SubscriptionRenewal; use Pixelant\PxaProductManager\Domain\Repository\OrderRepository; use Pixelant\PxaProductManager\Exception\NotARecurringOrderException; +use Pixelant\PxaProductManager\Utility\ConfigurationUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; @@ -37,11 +38,22 @@ class SubscriptionRenewalService * SubscriptionRenewalService constructor. * @param Order $order * @throws NotARecurringOrderException + * @throws \TYPO3\CMS\Extbase\Configuration\Exception\InvalidConfigurationTypeException */ public function __construct(Order $order) { - $this->today = \DateTime::createFromFormat('Y-m-d', '2019-11-22'); - $this->today = \DateTime::createFromFormat('Y-m-d', '2020-01-21'); + $settings = ConfigurationUtility::getSettings(); + + $this->today = new \DateTime(); + + if ($settings['payments']['debug'] === '1' && $settings['payments']['todayDate']) { + try { + $this->today = \DateTime::createFromFormat('Y-m-d', $settings['payments']['todayDate']); + } catch (\Exception $e) { + // Do nothing + } + } + $this->order = $order; if ($this->order->getRecurringPeriod() <= 0) { throw new NotARecurringOrderException(); diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php index 38cd4ab6..6465a1d1 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php @@ -23,13 +23,15 @@ 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/subscription_renewal_tca.svg' ], 'interface' => [ - 'showRecordFieldList' => 'payment_date, payment_next_try, payment_done, payment_attempts_left, shipment_date, - shipment_next_try, shipment_done, shipment_attempts_left' + 'showRecordFieldList' => 'payment_date, payment_next_try, payment_done, payment_attempts_left, payment_id, + payment_status, shipment_date, shipment_next_try, shipment_done, + shipment_attempts_left' ], 'types' => [ '1' => [ - 'showitem' => 'payment_date, payment_next_try, payment_done, payment_attempts_left, shipment_date, - shipment_next_try, shipment_done, shipment_attempts_left' + 'showitem' => 'payment_date, payment_next_try, payment_done, payment_attempts_left, payment_id, + payment_status, shipment_date, shipment_next_try, shipment_done, + shipment_attempts_left' ], ], 'columns' => [ @@ -117,6 +119,24 @@ // 'readOnly' => 1 ], ], + 'payment_id' => [ + 'exclude' => 0, + 'label' => $ll . '.payment_id', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'eval' => 'trim' + ] + ], + 'payment_status' => [ + 'exclude' => 0, + 'label' => $ll . '.payment_status', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'eval' => 'trim' + ] + ], 'shipment_date' => [ 'exclude' => true, 'label' => $ll . '.shipment_date', diff --git a/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.txt index 6e99896f..c9356031 100644 --- a/Configuration/TypoScript/setup.txt +++ b/Configuration/TypoScript/setup.txt @@ -154,6 +154,16 @@ plugin.tx_pxaproductmanager { # don't compare images and links ignoreAttributeTypesInCompareView = 6,7 + payments { + ## To debug the payments. Used for faking today date and few other things + debug = 0 + + ### !!! WARNING !!! Be careful. This will replace the today date with whatever you specify + ### It is added to test the subscription renewals + ### Only works if 'debug' option is enabled + ### The format is Y-m-d + todayDate = + } } _LOCAL_LANG { diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index da9cfbb4..01c4fa52 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -463,6 +463,12 @@ Payment attempts left + + Payment id + + + Payment status + Shipment date diff --git a/ext_tables.sql b/ext_tables.sql index 92c7f858..569b61fa 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -588,12 +588,15 @@ CREATE TABLE tx_pxaproductmanager_domain_model_subscriptionrenewal ( payment_next_try int(11) unsigned DEFAULT '0' NOT NULL, payment_done tinyint(4) unsigned DEFAULT '0' NOT NULL, payment_attempts_left int(11) unsigned DEFAULT '0' NOT NULL, + payment_id varchar(255) DEFAULT '' NOT NULL, + payment_status varchar(255) DEFAULT '' NOT NULL, shipment_date int(11) unsigned DEFAULT '0' NOT NULL, shipment_next_try int(11) unsigned DEFAULT '0' NOT NULL, shipment_done tinyint(4) unsigned DEFAULT '0' NOT NULL, shipment_attempts_left int(11) unsigned DEFAULT '0' NOT NULL, order int(11) unsigned DEFAULT '0' NOT NULL, + tstamp int(11) unsigned DEFAULT '0' NOT NULL, crdate int(11) unsigned DEFAULT '0' NOT NULL, cruser_id int(11) unsigned DEFAULT '0' NOT NULL, From 48076287282f0357fa60580d4a4607a812218ac6 Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Mon, 25 Nov 2019 10:06:10 +0200 Subject: [PATCH 46/62] [TASK] Small refactoring --- Classes/Domain/Model/Order.php | 4 ++-- Classes/Domain/Model/SubscriptionRenewal.php | 12 ++++++++++++ Classes/Service/SubscriptionRenewalService.php | 8 ++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 458d18d1..c18f47e9 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -422,9 +422,9 @@ public function removeOrderField(string $name) } /** - * @return \DateTime + * @return \DateTime|null */ - public function getCrdate(): \DateTime + public function getCrdate(): ?\DateTime { return $this->crdate; } diff --git a/Classes/Domain/Model/SubscriptionRenewal.php b/Classes/Domain/Model/SubscriptionRenewal.php index cc3d62dd..f661e33e 100644 --- a/Classes/Domain/Model/SubscriptionRenewal.php +++ b/Classes/Domain/Model/SubscriptionRenewal.php @@ -291,4 +291,16 @@ public function makeNextPaymentTryTomorrow() $this->setPaymentNextTry($nextTry); return $nextTry; } + + /** + * @param string $paymentId + * @param string $paymentStatus + */ + public function registerPayment(string $paymentId, string $paymentStatus) + { + $this->setPaymentDone(true); + $this->setPaymentAttemptsLeft(0); + $this->setPaymentId($paymentId); + $this->setPaymentStatus($paymentStatus); + } } diff --git a/Classes/Service/SubscriptionRenewalService.php b/Classes/Service/SubscriptionRenewalService.php index 0a495d3f..30c04472 100644 --- a/Classes/Service/SubscriptionRenewalService.php +++ b/Classes/Service/SubscriptionRenewalService.php @@ -77,7 +77,7 @@ public function getNextRenewal() /** * @return SubscriptionRenewal|null */ - protected function getLatestRenewal() + public function getLatestRenewal() { return array_pop($this->order->getRenewals()->toArray()); } @@ -108,10 +108,10 @@ public function createNextRenewal(SubscriptionRenewal $latestRenewal = null) // If no renewals exist yet if (!$latestRenewal) { - return $this->addRenewal($this->order->getCrdate()); + return $this->addRenewal($this->order->getCrdate() ?? new \DateTime()); } else { - $date = $this->getNextRenewalDate($latestRenewal); - return $this->addRenewal($date); + $nextDate = $this->getNextRenewalDate($latestRenewal); + return $this->addRenewal($nextDate); } } From fc64ac3319f1c28a34562bbdc95971cba412a21b Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Tue, 26 Nov 2019 16:15:09 +0200 Subject: [PATCH 47/62] [TASK] Add subscriptions --- Classes/Domain/Model/Subscription.php | 278 ++++++++++++++++++ .../Repository/SubscriptionRepository.php | 15 + Classes/Service/SubscriptionService.php | 104 +++++++ ...x_pxaproductmanager_domain_model_order.php | 11 +- ...oductmanager_domain_model_subscription.php | 166 +++++++++++ Resources/Private/Language/locallang_db.xlf | 44 +++ ext_tables.sql | 29 ++ 7 files changed, 644 insertions(+), 3 deletions(-) create mode 100644 Classes/Domain/Model/Subscription.php create mode 100644 Classes/Domain/Repository/SubscriptionRepository.php create mode 100644 Classes/Service/SubscriptionService.php create mode 100644 Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php diff --git a/Classes/Domain/Model/Subscription.php b/Classes/Domain/Model/Subscription.php new file mode 100644 index 00000000..3681cd48 --- /dev/null +++ b/Classes/Domain/Model/Subscription.php @@ -0,0 +1,278 @@ + + */ + protected $orders; + + /** + * @var string + */ + protected $serializedProductsQuantity = ''; + + /** + * @var int + */ + protected $subscriptionPeriod = 1; + + /** + * Subscription constructor. + */ + public function __construct() + { + //Do not remove the next line: It would break the functionality + $this->initStorageObjects(); + } + + /** + * initStorageObjects + */ + protected function initStorageObjects() + { + $this->orders = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); + } + + /** + * @return \DateTime + */ + public function getRenewDate(): \DateTime + { + return $this->renewDate; + } + + /** + * @param \DateTime $renewDate + * @return Subscription + */ + public function setRenewDate(\DateTime $renewDate): Subscription + { + $this->renewDate = $renewDate; + return $this; + } + + /** + * @return \DateTime + */ + public function getNextTry(): \DateTime + { + return $this->nextTry; + } + + /** + * @param \DateTime $nextTry + * @return Subscription + */ + public function setNextTry(\DateTime $nextTry): Subscription + { + $this->nextTry = $nextTry; + return $this; + } + + /** + * @return int|null + */ + public function getStatus(): ?int + { + return $this->status; + } + + /** + * @param int|null $status + * @return Subscription + */ + public function setStatus(?int $status): Subscription + { + $this->status = $status; + return $this; + } + + /** + * @return string + */ + public function getLastRenewStatus(): string + { + return $this->lastRenewStatus; + } + + /** + * @param string $lastRenewStatus + * @return Subscription + */ + public function setLastRenewStatus(string $lastRenewStatus): Subscription + { + $this->lastRenewStatus = $lastRenewStatus; + return $this; + } + + /** + * @return int + */ + public function getAttemptsLeft(): int + { + return $this->attemptsLeft; + } + + /** + * @param int $attemptsLeft + * @return Subscription + */ + public function setAttemptsLeft(int $attemptsLeft): Subscription + { + $this->attemptsLeft = $attemptsLeft; + return $this; + } + + /** + * Adds a Order + * + * @param \Pixelant\PxaProductManager\Domain\Model\Order $order + * @return Subscription + */ + public function addOrder(\Pixelant\PxaProductManager\Domain\Model\Order $order) + { + $this->orders->attach($order); + return $this; + } + + /** + * Removes a Order + * + * @param \Pixelant\PxaProductManager\Domain\Model\Order $orderToRemove The Order to be removed + * @return Subscription + */ + public function removeOrder(\Pixelant\PxaProductManager\Domain\Model\Order $orderToRemove) + { + $this->orders->detach($orderToRemove); + return $this; + } + + /** + * Returns the orders + * + * @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelant\PxaProductManager\Domain\Model\Order> $orders + */ + public function getOrders() + { + return $this->orders; + } + + /** + * Sets the orders + * + * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelant\PxaProductManager\Domain\Model\Order> $orders + * @return Subscription + */ + public function setOrders(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $orders) + { + $this->orders = $orders; + return $this; + } + + /** + * @return string + */ + public function getSerializedProductsQuantity(): string + { + return $this->serializedProductsQuantity; + } + + /** + * @param string $serializedProductsQuantity + * @return Subscription + */ + public function setSerializedProductsQuantity(string $serializedProductsQuantity) + { + $this->serializedProductsQuantity = $serializedProductsQuantity; + return $this; + } + + /** + * Save products quantity as serialized string + * + * @param array $productsQuantity + * @return Subscription + */ + public function setProductsQuantity(array $productsQuantity) + { + $this->setSerializedProductsQuantity( + serialize($productsQuantity) + ); + + return $this; + } + + /** + * @return int + */ + public function getSubscriptionPeriod(): int + { + return $this->subscriptionPeriod; + } + + /** + * @param int $subscriptionPeriod + * @return Subscription + */ + public function setSubscriptionPeriod(int $subscriptionPeriod): Subscription + { + $this->subscriptionPeriod = $subscriptionPeriod; + return $this; + } +} diff --git a/Classes/Domain/Repository/SubscriptionRepository.php b/Classes/Domain/Repository/SubscriptionRepository.php new file mode 100644 index 00000000..3dcf47a7 --- /dev/null +++ b/Classes/Domain/Repository/SubscriptionRepository.php @@ -0,0 +1,15 @@ +subscriptionRepository = $subscriptionRepository; + } + + /** + * @return Subscription + */ + protected function createEmptySubscription() + { + return MainUtility::getObjectManager()->get(Subscription::class); + } + + /** + * @param Order $order + * @param bool $persist + * @return Subscription + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException + */ + public function createFromOrder(Order $order, bool $persist = false) + { + $subscription = ($this->createEmptySubscription()) + ->setRenewDate(new \DateTime()) + ->setNextTry(new \DateTime()) + ->setStatus(Subscription::STATUS_ACTIVE) + ->setAttemptsLeft($this->numberOfAttempts) + ->setSerializedProductsQuantity($order->getSerializedProductsQuantity()) + ->addOrder($order) + ->setLastRenewStatus(Subscription::RENEW_STATUS_SUCCESS); + + $subscription->setPid($order->getPid()); + + $this->subscriptionRepository->add($subscription); + + if ($persist) { + GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); + } + + return $subscription; + } + + /** + * @param Subscription $subscription + * @return Subscription + */ + public function updateRenewalDates(Subscription $subscription) + { + $renewalDate = $subscription->getRenewDate(); + switch ($subscription->getSubscriptionPeriod()) { + case Subscription::RECURRING_FOR_WEEK: + $timeModifier = Subscription::WEEK_TIME_MODIFIER; + break; + case Subscription::RECURRING_FOR_MONTH: + default: + $timeModifier = Subscription::MONTH_TIME_MODIFIER; + break; + } + + $renewalDate->modify($timeModifier); + $subscription->setRenewDate($renewalDate); + $subscription->setNextTry($renewalDate); + return $subscription; + } + + /** + * @param Subscription $subscription + * @return Subscription + */ + public function prepareForNextRenew(Subscription $subscription) + { + $subscription = $this->updateRenewalDates($subscription); + $subscription->setAttemptsLeft($this->numberOfAttempts); + + return $subscription; + } +} diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php index 4b5db3d1..e6f7558a 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php @@ -25,14 +25,14 @@ 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/cart_tca.svg' ], 'interface' => [ - 'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, serialized_products_quantity, serialized_order_fields, external_id, fe_user, checkout_type, price_at_checkout, tax_at_checkout, renewals', + 'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, serialized_products_quantity, serialized_order_fields, external_id, fe_user, checkout_type, price_at_checkout, tax_at_checkout, renewals, subscription', ], 'types' => [ '1' => [ 'showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, fe_user, checkout_type, price_at_checkout, tax_at_checkout --div--;' . $ll . '.order_fields,|order_fields|, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime, - --div--;' . $ll . '.tabs.recurring_payments, renewals', + --div--;' . $ll . '.tabs.recurring_payments, renewals, subscription', ], ], 'columns' => [ @@ -260,6 +260,11 @@ 'expandSingle' => 1, ], ] - ] + ], + 'subscription' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], ] ]; diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php new file mode 100644 index 00000000..08ad7f40 --- /dev/null +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php @@ -0,0 +1,166 @@ + [ + 'title' => $ll, + 'label' => 'renew_date', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'cruser_id' => 'cruser_id', + 'dividers2tabs' => true, + 'delete' => 'deleted', + 'default_sortby' => 'renew_date', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + ], + 'searchFields' => 'renew_date, shipment_date', +// 'hideTable' => true, + 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/subscription_tca.svg' + ], + 'interface' => [ + 'showRecordFieldList' => 'renew_date, next_try, status, last_renew_status, attempts_left, + serialized_products_quantity, subscription_period, orders' + ], + 'types' => [ + '1' => [ + 'showitem' => 'renew_date, next_try, status, last_renew_status, attempts_left, serialized_products_quantity, + subscription_period, orders' + ], + ], + 'columns' => [ + 'hidden' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.hidden', + 'config' => [ + 'type' => 'check', + 'items' => [ + '1' => [ + '0' => $llCore . 'locallang_core.xlf:labels.enabled' + ] + ], + ], + ], + 'starttime' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.starttime', + 'config' => [ + 'type' => 'input', + 'size' => 13, + 'eval' => 'datetime,int', + 'renderType' => 'inputDateTime', + 'default' => 0, + ] + ], + 'endtime' => [ + 'exclude' => true, + 'label' => $llCore . 'locallang_general.xlf:LGL.endtime', + 'config' => [ + 'type' => 'input', + 'size' => 13, + 'eval' => 'datetime,int', + 'default' => 0, + 'renderType' => 'inputDateTime', + 'range' => [ + 'upper' => mktime(0, 0, 0, 1, 1, 2038) + ] + ], + ], + 'renew_date' => [ + 'exclude' => true, + 'label' => $ll . '.renew_date', + 'config' => [ + 'type' => 'input', + 'renderType' => 'inputDateTime', + 'size' => 15, + 'eval' => 'datetime', + 'default' => time(), + ], + ], + 'next_try' => [ + 'exclude' => true, + 'label' => $ll . '.next_try', + 'config' => [ + 'type' => 'input', + 'renderType' => 'inputDateTime', + 'size' => 15, + 'eval' => 'datetime', + 'default' => time(), + ], + ], + 'attempts_left' => [ + 'exclude' => true, + 'label' => $ll . '.attempts_left', + 'config' => [ + 'type' => 'input', + 'size' => 30, + ], + ], + 'last_renew_status' => [ + 'exclude' => 0, + 'label' => $ll . '.last_renew_status', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'eval' => 'trim' + ] + ], + 'status' => [ + 'exclude' => 0, + 'label' => $ll . '.status', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + [$ll . '.status.1', \Pixelant\PxaProductManager\Domain\Model\Subscription::STATUS_ACTIVE], + [$ll . '.status.2', \Pixelant\PxaProductManager\Domain\Model\Subscription::STATUS_PAUSED], + [$ll . '.status.3', \Pixelant\PxaProductManager\Domain\Model\Subscription::STATUS_CANCELED] + ] + ] + ], + 'orders' => [ + 'exclude' => 1, + 'label' => $ll . '.orders', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'tx_pxaproductmanager_domain_model_order', + 'foreign_field' => 'subscription', + 'foreign_sortby' => 'crdate', + 'appearance' => [ + 'collapseAll' => 1, + 'expandSingle' => 1, + ], + ] + ], + 'serialized_products_quantity' => [ + 'exclude' => 1, + 'label' => $ll . '.serialized_products_quantity', + 'config' => [ + 'type' => 'passthrough' + ] + ], + 'subscription_period' => [ + 'exclude' => 1, + 'label' => $ll . '.subscription_period', + 'config' => [ + 'type' => 'select', + 'items' => [ + [ + $ll . '.subscription_period.' . + \Pixelant\PxaProductManager\Domain\Model\Subscription::RECURRING_FOR_WEEK, + \Pixelant\PxaProductManager\Domain\Model\Subscription::RECURRING_FOR_WEEK + ], + [ + $ll . '.subscription_period.' . + \Pixelant\PxaProductManager\Domain\Model\Subscription::RECURRING_FOR_MONTH, + \Pixelant\PxaProductManager\Domain\Model\Subscription::RECURRING_FOR_MONTH + ] + ] + ] + ], + ] +]; diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 01c4fa52..22e47b57 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -482,6 +482,50 @@ Shipment attempts left + + + Subscription + + + Renew date + + + Next try + + + Status + + + Active + + + Paused + + + Cancelled + + + Last renew status + + + Attempts left + + + Orders + + + Serialized products quantity + + + Subscription period + + + Week subscription + + + Month subscription + + diff --git a/ext_tables.sql b/ext_tables.sql index 569b61fa..074518e1 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -437,6 +437,7 @@ CREATE TABLE tx_pxaproductmanager_domain_model_order ( price_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, tax_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, renewals int(11) unsigned DEFAULT '0' NOT NULL, + subscription int(11) unsigned DEFAULT '0' NOT NULL, tstamp int(11) unsigned DEFAULT '0' NOT NULL, crdate int(11) unsigned DEFAULT '0' NOT NULL, @@ -608,3 +609,31 @@ CREATE TABLE tx_pxaproductmanager_domain_model_subscriptionrenewal ( PRIMARY KEY (uid), KEY parent (pid) ); + +# +# Table structure for table 'tx_pxaproductmanager_domain_model_subscription' +# +CREATE TABLE tx_pxaproductmanager_domain_model_subscription ( + uid int(11) NOT NULL auto_increment, + pid int(11) DEFAULT '0' NOT NULL, + + renew_date int(11) unsigned DEFAULT '0' NOT NULL, + next_try int(11) unsigned DEFAULT '0' NOT NULL, + status smallint(5) unsigned DEFAULT '0' NOT NULL, + last_renew_status varchar(255) DEFAULT '' NOT NULL, + attempts_left int(11) unsigned DEFAULT '0' NOT NULL, + orders int(11) unsigned DEFAULT '0' NOT NULL, + serialized_products_quantity blob, + subscription_period smallint(5) unsigned DEFAULT '0' NOT NULL, + + tstamp int(11) unsigned DEFAULT '0' NOT NULL, + crdate int(11) unsigned DEFAULT '0' NOT NULL, + cruser_id int(11) unsigned DEFAULT '0' NOT NULL, + deleted tinyint(4) unsigned DEFAULT '0' NOT NULL, + hidden tinyint(4) unsigned DEFAULT '0' NOT NULL, + starttime int(11) unsigned DEFAULT '0' NOT NULL, + endtime int(11) unsigned DEFAULT '0' NOT NULL, + + PRIMARY KEY (uid), + KEY parent (pid) +); From 606bc9709d19518e893514d785ee0d935dc02eeb Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Wed, 27 Nov 2019 08:50:24 +0200 Subject: [PATCH 48/62] [TASK] Add first order getter, today date debug setting --- Classes/Domain/Model/Subscription.php | 17 +++++++++ .../Repository/SubscriptionRepository.php | 12 ++++++- .../Exception/EmptySubscriptionException.php | 12 +++++++ Classes/Service/SubscriptionService.php | 35 +++++++++++++++++++ ...oductmanager_domain_model_subscription.php | 2 +- 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 Classes/Exception/EmptySubscriptionException.php diff --git a/Classes/Domain/Model/Subscription.php b/Classes/Domain/Model/Subscription.php index 3681cd48..83bcc740 100644 --- a/Classes/Domain/Model/Subscription.php +++ b/Classes/Domain/Model/Subscription.php @@ -2,6 +2,8 @@ namespace Pixelant\PxaProductManager\Domain\Model; +use Pixelant\PxaProductManager\Exception\EmptySubscriptionException; + /** * Class Subscription * @package Pixelant\PxaProductManager\Domain\Model @@ -275,4 +277,19 @@ public function setSubscriptionPeriod(int $subscriptionPeriod): Subscription $this->subscriptionPeriod = $subscriptionPeriod; return $this; } + + /** + * @return Order + * @throws EmptySubscriptionException + */ + public function getFirstOrder() + { + /** @var Order $firstOrder */ + $firstOrder = $this->getOrders()->toArray()[0] ?? null; + if (!$firstOrder) { + throw new EmptySubscriptionException('Subscription has no orders'); + } + + return $firstOrder; + } } diff --git a/Classes/Domain/Repository/SubscriptionRepository.php b/Classes/Domain/Repository/SubscriptionRepository.php index 3dcf47a7..d9ceafef 100644 --- a/Classes/Domain/Repository/SubscriptionRepository.php +++ b/Classes/Domain/Repository/SubscriptionRepository.php @@ -3,6 +3,8 @@ namespace Pixelant\PxaProductManager\Domain\Repository; +use Pixelant\PxaProductManager\Utility\MainUtility; +use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings; use TYPO3\CMS\Extbase\Persistence\Repository; /** @@ -11,5 +13,13 @@ */ class SubscriptionRepository extends Repository { - + /** + * initializeObject + */ + public function initializeObject() + { + $defaultQuerySettings = MainUtility::getObjectManager()->get(Typo3QuerySettings::class); + $defaultQuerySettings->setRespectStoragePage(false); + $this->setDefaultQuerySettings($defaultQuerySettings); + } } diff --git a/Classes/Exception/EmptySubscriptionException.php b/Classes/Exception/EmptySubscriptionException.php new file mode 100644 index 00000000..cb3780e7 --- /dev/null +++ b/Classes/Exception/EmptySubscriptionException.php @@ -0,0 +1,12 @@ +today = new \DateTime(); + + if ($settings['payments']['debug'] === '1' && $settings['payments']['todayDate']) { + try { + $this->today = \DateTime::createFromFormat('Y-m-d', $settings['payments']['todayDate']); + } catch (\Exception $e) { + // Do nothing + } + } + } + /** * @param SubscriptionRepository $subscriptionRepository */ @@ -101,4 +127,13 @@ public function prepareForNextRenew(Subscription $subscription) return $subscription; } + + /** + * @param Subscription $subscription + * @return bool + */ + public function isItRenewalTime(Subscription $subscription) + { + return $this->today > $subscription->getNextTry(); + } } diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php index 08ad7f40..0c8235f6 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php @@ -129,7 +129,7 @@ 'type' => 'inline', 'foreign_table' => 'tx_pxaproductmanager_domain_model_order', 'foreign_field' => 'subscription', - 'foreign_sortby' => 'crdate', + 'foreign_default_sortby' => 'crdate', 'appearance' => [ 'collapseAll' => 1, 'expandSingle' => 1, From 223fe62dc03dfa2d6d367ca12f75b76a3112ffe5 Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Wed, 27 Nov 2019 12:20:58 +0200 Subject: [PATCH 49/62] [TASK] Add subscription order getter --- Classes/Domain/Model/Order.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index c18f47e9..d245b2fa 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -130,6 +130,11 @@ class Order extends AbstractEntity */ protected $renewals = null; + /** + * @var int + */ + protected $subscription = 0; + /** * __construct */ @@ -581,4 +586,22 @@ public function removeRenewal(SubscriptionRenewal $renewal) { $this->renewals->detach($renewal); } + + /** + * @return int + */ + public function getSubscription(): int + { + return $this->subscription; + } + + /** + * @param int $subscription + * @return Order + */ + public function setSubscription(int $subscription): Order + { + $this->subscription = $subscription; + return $this; + } } From 22e9d8eb6f40533b074a464d5f76de135bba71ef Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Wed, 27 Nov 2019 16:27:31 +0200 Subject: [PATCH 50/62] [TASK] Add external id to searchables --- .../TCA/tx_pxaproductmanager_domain_model_order.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php index e6f7558a..6851374f 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php @@ -20,7 +20,7 @@ 'starttime' => 'starttime', 'endtime' => 'endtime', ], - 'searchFields' => 'products', + 'searchFields' => 'products, external_id', #'hideTable' => true, 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/cart_tca.svg' ], @@ -29,10 +29,9 @@ ], 'types' => [ '1' => [ - 'showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, fe_user, checkout_type, price_at_checkout, tax_at_checkout + 'showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, fe_user, checkout_type, price_at_checkout, tax_at_checkout, external_id, --div--;' . $ll . '.order_fields,|order_fields|, - --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime, - --div--;' . $ll . '.tabs.recurring_payments, renewals, subscription', + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime', ], ], 'columns' => [ @@ -186,7 +185,10 @@ 'exclude' => 1, 'label' => 'External id', 'config' => [ - 'type' => 'passthrough' + 'type' => 'input', + 'size' => 30, + 'eval' => 'trim', + 'readOnly' => true ] ], 'crdate' => [ From 6e26933cd45723f2967ae0d8ff55482b2624e08a Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Thu, 28 Nov 2019 09:28:08 +0200 Subject: [PATCH 51/62] [TASK] Change subscription TCA --- ...x_pxaproductmanager_domain_model_order.php | 36 ++++++++++--------- ...oductmanager_domain_model_subscription.php | 2 +- Resources/Private/Language/locallang_db.xlf | 6 ++++ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php index 6851374f..0a203efc 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php @@ -25,11 +25,11 @@ 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/cart_tca.svg' ], 'interface' => [ - 'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, serialized_products_quantity, serialized_order_fields, external_id, fe_user, checkout_type, price_at_checkout, tax_at_checkout, renewals, subscription', + 'showRecordFieldList' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, serialized_products_quantity, serialized_order_fields, external_id, fe_user, checkout_type, price_at_checkout, tax_at_checkout, subscription', ], 'types' => [ '1' => [ - 'showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, fe_user, checkout_type, price_at_checkout, tax_at_checkout, external_id, + 'showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource, hidden, complete, products, fe_user, checkout_type, price_at_checkout, tax_at_checkout, external_id, subscription, --div--;' . $ll . '.order_fields,|order_fields|, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access, starttime, endtime', ], @@ -249,23 +249,27 @@ 'eval' => 'double2' ], ], - 'renewals' => [ + 'subscription' => [ 'exclude' => 1, - 'label' => $ll . '.renewals', + 'label' => $ll . '.subscription', 'config' => [ - 'type' => 'inline', - 'foreign_table' => 'tx_pxaproductmanager_domain_model_subscriptionrenewal', - 'foreign_field' => 'order', - 'foreign_sortby' => 'payment_date', - 'appearance' => [ - 'collapseAll' => 1, - 'expandSingle' => 1, + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'tx_pxaproductmanager_domain_model_subscription', + 'size' => 1, + 'maxitems' => 1, + 'default' => 0, + 'items' => [ + [$ll . '.subscription.single', 0] ], - ] - ], - 'subscription' => [ - 'config' => [ - 'type' => 'passthrough', + 'fieldControl' => [ + 'editPopup' => [ + 'disabled' => true + ], + 'addRecord' => [ + 'disabled' => false, + ] + ] ], ], ] diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php index 0c8235f6..992e7e11 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php @@ -6,7 +6,7 @@ return [ 'ctrl' => [ 'title' => $ll, - 'label' => 'renew_date', + 'label' => 'uid', 'tstamp' => 'tstamp', 'crdate' => 'crdate', 'cruser_id' => 'cruser_id', diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 22e47b57..685c3be0 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -279,6 +279,12 @@ Recurring payments + + Subscription + + + Single purchase + Price coupon From 7a073f732d2fad20c4794471487fc160f9bdf5a9 Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Thu, 28 Nov 2019 13:01:38 +0200 Subject: [PATCH 52/62] [TASK] Cleanup --- Classes/Domain/Model/Order.php | 43 --- Classes/Domain/Model/SubscriptionRenewal.php | 306 ------------------ .../Service/SubscriptionRenewalService.php | 178 ---------- ...nager_domain_model_subscriptionrenewal.php | 193 ----------- Resources/Private/Language/locallang_db.xlf | 37 --- ext_tables.sql | 33 -- 6 files changed, 790 deletions(-) delete mode 100644 Classes/Domain/Model/SubscriptionRenewal.php delete mode 100644 Classes/Service/SubscriptionRenewalService.php delete mode 100644 Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index d245b2fa..ea81d423 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -124,12 +124,6 @@ class Order extends AbstractEntity */ protected $taxAtCheckout = 0.0; - /** - * - * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelant\PxaProductManager\Domain\Model\SubscriptionRenewal> - */ - protected $renewals = null; - /** * @var int */ @@ -156,7 +150,6 @@ protected function initStorageObjects() { $this->products = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); $this->coupons = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); - $this->renewals = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); } /** @@ -551,42 +544,6 @@ public function setTaxAtCheckout(float $taxAtCheckout) $this->taxAtCheckout = $taxAtCheckout; } - /** - * @return ObjectStorage - */ - public function getRenewals(): ObjectStorage - { - return $this->renewals; - } - - /** - * @param ObjectStorage $renewals - */ - public function setRenewals(ObjectStorage $renewals) - { - $this->renewals = $renewals; - } - - /** - * Add a renewal - * - * @param SubscriptionRenewal $renewal - */ - public function addRenewal(SubscriptionRenewal $renewal) - { - $this->renewals->attach($renewal); - } - - /** - * Remove a renewal - * - * @param SubscriptionRenewal $renewal - */ - public function removeRenewal(SubscriptionRenewal $renewal) - { - $this->renewals->detach($renewal); - } - /** * @return int */ diff --git a/Classes/Domain/Model/SubscriptionRenewal.php b/Classes/Domain/Model/SubscriptionRenewal.php deleted file mode 100644 index f661e33e..00000000 --- a/Classes/Domain/Model/SubscriptionRenewal.php +++ /dev/null @@ -1,306 +0,0 @@ -paymentDate; - } - - /** - * @param \DateTime $paymentDate - * @return SubscriptionRenewal - */ - public function setPaymentDate(\DateTime $paymentDate): SubscriptionRenewal - { - $this->paymentDate = $paymentDate; - return $this; - } - - /** - * @return \DateTime - */ - public function getPaymentNextTry(): \DateTime - { - return $this->paymentNextTry; - } - - /** - * @param \DateTime $paymentNextTry - * @return SubscriptionRenewal - */ - public function setPaymentNextTry(\DateTime $paymentNextTry): SubscriptionRenewal - { - $this->paymentNextTry = $paymentNextTry; - return $this; - } - - /** - * @return bool - */ - public function isPaymentDone(): bool - { - return $this->paymentDone; - } - - /** - * @param bool $paymentDone - * @return SubscriptionRenewal - */ - public function setPaymentDone(bool $paymentDone): SubscriptionRenewal - { - $this->paymentDone = $paymentDone; - return $this; - } - - /** - * @return \DateTime - */ - public function getShipmentDate(): \DateTime - { - return $this->shipmentDate; - } - - /** - * @param \DateTime $shipmentDate - * @return SubscriptionRenewal - */ - public function setShipmentDate(\DateTime $shipmentDate): SubscriptionRenewal - { - $this->shipmentDate = $shipmentDate; - return $this; - } - - /** - * @return \DateTime - */ - public function getShipmentNextTry(): \DateTime - { - return $this->shipmentNextTry; - } - - /** - * @param \DateTime $shipmentNextTry - * @return SubscriptionRenewal - */ - public function setShipmentNextTry(\DateTime $shipmentNextTry): SubscriptionRenewal - { - $this->shipmentNextTry = $shipmentNextTry; - return $this; - } - - /** - * @return bool - */ - public function isShipmentDone(): bool - { - return $this->shipmentDone; - } - - /** - * @param bool $shipmentDone - * @return SubscriptionRenewal - */ - public function setShipmentDone(bool $shipmentDone): SubscriptionRenewal - { - $this->shipmentDone = $shipmentDone; - return $this; - } - - /** - * @return int - */ - public function getPaymentAttemptsLeft(): int - { - return $this->paymentAttemptsLeft; - } - - /** - * @param int $paymentAttemptsLeft - * @return SubscriptionRenewal - */ - public function setPaymentAttemptsLeft(int $paymentAttemptsLeft): SubscriptionRenewal - { - $this->paymentAttemptsLeft = $paymentAttemptsLeft; - return $this; - } - - /** - * @return string - */ - public function getPaymentId(): string - { - return $this->paymentId; - } - - /** - * @param string $paymentId - * @return SubscriptionRenewal - */ - public function setPaymentId(string $paymentId): SubscriptionRenewal - { - $this->paymentId = $paymentId; - return $this; - } - - /** - * @return string - */ - public function getPaymentStatus(): string - { - return $this->paymentStatus; - } - - /** - * @param string $paymentStatus - * @return SubscriptionRenewal - */ - public function setPaymentStatus(string $paymentStatus): SubscriptionRenewal - { - $this->paymentStatus = $paymentStatus; - return $this; - } - - /** - * @return int - */ - public function getShipmentAttemptsLeft(): int - { - return $this->shipmentAttemptsLeft; - } - - /** - * @param int $shipmentAttemptsLeft - * @return SubscriptionRenewal - */ - public function setShipmentAttemptsLeft(int $shipmentAttemptsLeft): SubscriptionRenewal - { - $this->shipmentAttemptsLeft = $shipmentAttemptsLeft; - return $this; - } - - /** - * @return bool - */ - public function hasMorePaymentAttempts() - { - return $this->paymentAttemptsLeft > 0; - } - - /** - * @return bool - */ - public function hasMoreShipmentAttempts() - { - return $this->shipmentAttemptsLeft > 0; - } - - /** - * @return int - */ - public function decrementPaymentAttempt() - { - $attemptsLeft = $this->getPaymentAttemptsLeft(); - $attemptsLeft = ($attemptsLeft <= 0) ? 0 : $attemptsLeft - 1; - $this->setPaymentAttemptsLeft($attemptsLeft); - return $attemptsLeft; - } - - /** - * @return \DateTime - */ - public function makeNextPaymentTryTomorrow() - { - $nextTry = $this->getPaymentNextTry(); - $nextTry->modify('+1 day'); - $this->setPaymentNextTry($nextTry); - return $nextTry; - } - - /** - * @param string $paymentId - * @param string $paymentStatus - */ - public function registerPayment(string $paymentId, string $paymentStatus) - { - $this->setPaymentDone(true); - $this->setPaymentAttemptsLeft(0); - $this->setPaymentId($paymentId); - $this->setPaymentStatus($paymentStatus); - } -} diff --git a/Classes/Service/SubscriptionRenewalService.php b/Classes/Service/SubscriptionRenewalService.php deleted file mode 100644 index 30c04472..00000000 --- a/Classes/Service/SubscriptionRenewalService.php +++ /dev/null @@ -1,178 +0,0 @@ -today = new \DateTime(); - - if ($settings['payments']['debug'] === '1' && $settings['payments']['todayDate']) { - try { - $this->today = \DateTime::createFromFormat('Y-m-d', $settings['payments']['todayDate']); - } catch (\Exception $e) { - // Do nothing - } - } - - $this->order = $order; - if ($this->order->getRecurringPeriod() <= 0) { - throw new NotARecurringOrderException(); - } - } - - /** - * @return SubscriptionRenewal|null - */ - public function getNextRenewal() - { - $renewal = $this->getLatestRenewal(); - - if (!$renewal) { - $renewal = $this->createNextRenewal($renewal); - } - - return $renewal; - } - - /** - * @return SubscriptionRenewal|null - */ - public function getLatestRenewal() - { - return array_pop($this->order->getRenewals()->toArray()); - } - - /** - * @param \DateTime $date - * @return SubscriptionRenewal - */ - public function addRenewal(\DateTime $date) - { - $newRenewal = $this->createRenewal($date); - - $this->order->addRenewal($newRenewal); - GeneralUtility::makeInstance(ObjectManager::class)->get(OrderRepository::class)->update($this->order); - GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); - return $newRenewal; - } - - /** - * @param SubscriptionRenewal|null $latestRenewal - * @return SubscriptionRenewal - */ - public function createNextRenewal(SubscriptionRenewal $latestRenewal = null) - { - if (!$latestRenewal) { - $latestRenewal = $this->getLatestRenewal(); - } - - // If no renewals exist yet - if (!$latestRenewal) { - return $this->addRenewal($this->order->getCrdate() ?? new \DateTime()); - } else { - $nextDate = $this->getNextRenewalDate($latestRenewal); - return $this->addRenewal($nextDate); - } - } - - /** - * @param SubscriptionRenewal $lastRenewal - * @return \DateTime - */ - public function getNextRenewalDate(SubscriptionRenewal $lastRenewal) - { - $nextRenewalDate = clone($lastRenewal->getPaymentDate()); - - switch ($this->order->getRecurringPeriod()) { - case Order::RECURRING_FOR_WEEK: - $timeModifier = Order::WEEK_TIME_MODIFIER; - break; - case Order::RECURRING_FOR_MONTH: - default: - $timeModifier = Order::MONTH_TIME_MODIFIER; - break; - } - - return $nextRenewalDate->modify($timeModifier); - } - - /** - * @param \DateTime $date - * @return SubscriptionRenewal - */ - protected function createRenewal(\DateTime $date) - { - $renewal = (new SubscriptionRenewal()) - ->setPaymentDate($date) - ->setPaymentNextTry($date) - ->setPaymentDone(false) - ->setPaymentAttemptsLeft($this->maxPaymentAttempts) - ->setShipmentDate($date) - ->setShipmentNextTry($date) - ->setShipmentDone(false) - ->setShipmentAttemptsLeft($this->maxShipmentAttempts) - ; - - $renewal->setPid($this->order->getPid()); - return $renewal; - } - - /** - * @param SubscriptionRenewal $renewal - * @return bool - */ - public function isOngoingPayment(SubscriptionRenewal $renewal) - { - return !$renewal->isPaymentDone() && $renewal->getPaymentAttemptsLeft() > 0; - } - - /** - * @param SubscriptionRenewal $renewal - * @return bool - * @throws \Exception - */ - public function isItTimeToMakePayment(SubscriptionRenewal $renewal) - { - return $this->today > $renewal->getPaymentNextTry(); - } -} diff --git a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php deleted file mode 100644 index 6465a1d1..00000000 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_subscriptionrenewal.php +++ /dev/null @@ -1,193 +0,0 @@ - [ - 'title' => $ll, - 'label' => 'payment_date', - 'tstamp' => 'tstamp', - 'crdate' => 'crdate', - 'cruser_id' => 'cruser_id', - 'dividers2tabs' => true, - 'delete' => 'deleted', - 'default_sortby' => 'payment_date', - 'enablecolumns' => [ - 'disabled' => 'hidden', - 'starttime' => 'starttime', - 'endtime' => 'endtime', - ], - 'searchFields' => 'payment_date, shipment_date', -// 'hideTable' => true, - 'iconfile' => 'EXT:pxa_product_manager/Resources/Public/Icons/Svg/subscription_renewal_tca.svg' - ], - 'interface' => [ - 'showRecordFieldList' => 'payment_date, payment_next_try, payment_done, payment_attempts_left, payment_id, - payment_status, shipment_date, shipment_next_try, shipment_done, - shipment_attempts_left' - ], - 'types' => [ - '1' => [ - 'showitem' => 'payment_date, payment_next_try, payment_done, payment_attempts_left, payment_id, - payment_status, shipment_date, shipment_next_try, shipment_done, - shipment_attempts_left' - ], - ], - 'columns' => [ - 'hidden' => [ - 'exclude' => true, - 'label' => $llCore . 'locallang_general.xlf:LGL.hidden', - 'config' => [ - 'type' => 'check', - 'items' => [ - '1' => [ - '0' => $llCore . 'locallang_core.xlf:labels.enabled' - ] - ], - ], - ], - 'starttime' => [ - 'exclude' => true, - 'label' => $llCore . 'locallang_general.xlf:LGL.starttime', - 'config' => [ - 'type' => 'input', - 'size' => 13, - 'eval' => 'datetime,int', - 'renderType' => 'inputDateTime', - 'default' => 0, - ] - ], - 'endtime' => [ - 'exclude' => true, - 'label' => $llCore . 'locallang_general.xlf:LGL.endtime', - 'config' => [ - 'type' => 'input', - 'size' => 13, - 'eval' => 'datetime,int', - 'default' => 0, - 'renderType' => 'inputDateTime', - 'range' => [ - 'upper' => mktime(0, 0, 0, 1, 1, 2038) - ] - ], - ], - 'payment_date' => [ - 'exclude' => true, - 'label' => $ll . '.payment_date', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputDateTime', - 'size' => 15, - 'eval' => 'datetime', - 'default' => time(), -// 'readOnly' => 1, -// 'format' => 'date' - ], - ], - 'payment_next_try' => [ - 'exclude' => true, - 'label' => $ll . '.payment_next_try', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputDateTime', - 'size' => 15, - 'eval' => 'datetime', - 'default' => time(), -// 'readOnly' => 1 - ], - ], - 'payment_done' => [ - 'exclude' => true, - 'label' => $ll . '.payment_done', - 'config' => [ - 'type' => 'check', - 'items' => [ - '1' => [ - '0' => $llCore . 'locallang_core.xlf:labels.enabled' - ] - ], -// 'readOnly' => 1 - ], - ], - 'payment_attempts_left' => [ - 'exclude' => true, - 'label' => $ll . '.payment_attempts_left', - 'config' => [ - 'type' => 'input', - 'size' => 30, -// 'readOnly' => 1 - ], - ], - 'payment_id' => [ - 'exclude' => 0, - 'label' => $ll . '.payment_id', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim' - ] - ], - 'payment_status' => [ - 'exclude' => 0, - 'label' => $ll . '.payment_status', - 'config' => [ - 'type' => 'input', - 'size' => 30, - 'eval' => 'trim' - ] - ], - 'shipment_date' => [ - 'exclude' => true, - 'label' => $ll . '.shipment_date', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputDateTime', - 'size' => 15, - 'eval' => 'datetime', - 'default' => time(), -// 'format' => 'date', -// 'readOnly' => 1 - ], - ], - 'shipment_next_try' => [ - 'exclude' => true, - 'label' => $ll . '.shipment_next_try', - 'config' => [ - 'type' => 'input', - 'renderType' => 'inputDateTime', - 'size' => 15, - 'eval' => 'datetime', - 'default' => time(), -// 'readOnly' => 1 - ], - ], - 'shipment_done' => [ - 'exclude' => true, - 'label' => $ll . '.shipment_done', - 'config' => [ - 'type' => 'check', - 'items' => [ - '1' => [ - '0' => $llCore . 'locallang_core.xlf:labels.enabled' - ] - ], -// 'readOnly' => 1 - ], - ], - 'shipment_attempts_left' => [ - 'exclude' => true, - 'label' => $ll . '.payment_attempts_left', - 'config' => [ - 'type' => 'input', - 'size' => 30, -// 'readOnly' => 1 - ], - ], - 'order' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], - ] -]; diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 685c3be0..a3579564 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -273,9 +273,6 @@ Tax at checkout - - Renewals - Recurring payments @@ -454,40 +451,6 @@ - - Subscription renewal - - - Payment date - - - Payment next try - - - Payment done - - - Payment attempts left - - - Payment id - - - Payment status - - - Shipment date - - - Shipment next try - - - Shipment done - - - Shipment attempts left - - Subscription diff --git a/ext_tables.sql b/ext_tables.sql index 074518e1..53fae97a 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -436,7 +436,6 @@ CREATE TABLE tx_pxaproductmanager_domain_model_order ( coupons int(11) unsigned DEFAULT '0' NOT NULL, price_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, tax_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, - renewals int(11) unsigned DEFAULT '0' NOT NULL, subscription int(11) unsigned DEFAULT '0' NOT NULL, tstamp int(11) unsigned DEFAULT '0' NOT NULL, @@ -578,38 +577,6 @@ CREATE TABLE tx_pxaproductmanager_domain_model_coupon ( KEY parent (pid) ); -# -# Table structure for table 'tx_pxaproductmanager_domain_model_subscriptionrenewal' -# -CREATE TABLE tx_pxaproductmanager_domain_model_subscriptionrenewal ( - uid int(11) NOT NULL auto_increment, - pid int(11) DEFAULT '0' NOT NULL, - - payment_date int(11) unsigned DEFAULT '0' NOT NULL, - payment_next_try int(11) unsigned DEFAULT '0' NOT NULL, - payment_done tinyint(4) unsigned DEFAULT '0' NOT NULL, - payment_attempts_left int(11) unsigned DEFAULT '0' NOT NULL, - payment_id varchar(255) DEFAULT '' NOT NULL, - payment_status varchar(255) DEFAULT '' NOT NULL, - shipment_date int(11) unsigned DEFAULT '0' NOT NULL, - shipment_next_try int(11) unsigned DEFAULT '0' NOT NULL, - shipment_done tinyint(4) unsigned DEFAULT '0' NOT NULL, - shipment_attempts_left int(11) unsigned DEFAULT '0' NOT NULL, - order int(11) unsigned DEFAULT '0' NOT NULL, - - - tstamp int(11) unsigned DEFAULT '0' NOT NULL, - crdate int(11) unsigned DEFAULT '0' NOT NULL, - cruser_id int(11) unsigned DEFAULT '0' NOT NULL, - deleted tinyint(4) unsigned DEFAULT '0' NOT NULL, - hidden tinyint(4) unsigned DEFAULT '0' NOT NULL, - starttime int(11) unsigned DEFAULT '0' NOT NULL, - endtime int(11) unsigned DEFAULT '0' NOT NULL, - - PRIMARY KEY (uid), - KEY parent (pid) -); - # # Table structure for table 'tx_pxaproductmanager_domain_model_subscription' # From 745431282aa16bee7bf111bc0734baaaaba42d5b Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Fri, 29 Nov 2019 09:20:05 +0200 Subject: [PATCH 53/62] [TASK] Fix totatl price formatting --- .../JavaScript/ProductManager.WishList.js | 19 +++++++++++++++++++ Resources/Public/JavaScript/ProductManager.js | 10 ++++++++++ 2 files changed, 29 insertions(+) diff --git a/Resources/Public/JavaScript/ProductManager.WishList.js b/Resources/Public/JavaScript/ProductManager.WishList.js index 527fa6b4..ac31a487 100644 --- a/Resources/Public/JavaScript/ProductManager.WishList.js +++ b/Resources/Public/JavaScript/ProductManager.WishList.js @@ -298,6 +298,25 @@ return settings; }; + /** + * + * @param price + * @param $element + * @returns {string} + */ + const formattedPrice = function (price, $element) { + if ($totalPrice.first().length <= 0) { + return ''; + } + + const priceCurrencyFormat = $element.first().data('currency-format') || ''; + const priceNumberFormat = $element.first().data('nubmer-format') || ''; + return sprintf( + priceCurrencyFormat, + ProductManager.Main.formatNumberFromFormatString(price, priceNumberFormat) + ); + }; + return { init: init, initButtons: initButtons, diff --git a/Resources/Public/JavaScript/ProductManager.js b/Resources/Public/JavaScript/ProductManager.js index d72256dd..804a0d66 100644 --- a/Resources/Public/JavaScript/ProductManager.js +++ b/Resources/Public/JavaScript/ProductManager.js @@ -194,6 +194,16 @@ return sign + (j ? i.substr(0, j) + t : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : ''); }, + formatNumberFromFormatString: function (number, format) { + format = this.trimChar(format, '|').split('|'); + + const decimals = parseInt(format[0]) || 2; + const decimalSep = format[1] || '.'; + const thousandsSep = format[2] || ','; + + return this.numberFormat(number, decimals, decimalSep, thousandsSep); + }, + /** * Write to status by key * From c0d56aa259ceccb0a246f16a3739958b817bbac6 Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Fri, 29 Nov 2019 14:22:53 +0200 Subject: [PATCH 54/62] [TASK] Add get wishlist items request functions --- .../Product/OptionsBar/WishListButton.html | 2 +- .../JavaScript/ProductManager.WishList.js | 79 ++++++++++++++----- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/Resources/Private/Partials/Product/OptionsBar/WishListButton.html b/Resources/Private/Partials/Product/OptionsBar/WishListButton.html index dd7d6efe..d3f80206 100644 --- a/Resources/Private/Partials/Product/OptionsBar/WishListButton.html +++ b/Resources/Private/Partials/Product/OptionsBar/WishListButton.html @@ -11,4 +11,4 @@
- \ No newline at end of file + diff --git a/Resources/Public/JavaScript/ProductManager.WishList.js b/Resources/Public/JavaScript/ProductManager.WishList.js index 527fa6b4..852785a5 100644 --- a/Resources/Public/JavaScript/ProductManager.WishList.js +++ b/Resources/Public/JavaScript/ProductManager.WishList.js @@ -35,6 +35,8 @@ $totalPrice, $totalTax; + let wishlist = []; + /** * Main wish list function * @@ -266,26 +268,28 @@ } }); - $buttons.each(function () { - let button = $(this), - productUid = parseInt(button.data('product-uid')), - text = '', - className = ''; - - if (ProductManager.Main.isInList(productsWishList, productUid)) { - text = button.data('remove-from-list-text'); - className = settings.inListClass; - } else { - text = button.data('add-to-list-text'); - className = settings.notInListClass; - } + _getWhilist(function (wishlistProducts) { + $buttons.each(function () { + let button = $(this), + productUid = parseInt(button.data('product-uid')), + text = '', + className = ''; + + if (ProductManager.Main.isInList(wishlistProducts, productUid)) { + text = button.data('remove-from-list-text'); + className = settings.inListClass; + } else { + text = button.data('add-to-list-text'); + className = settings.notInListClass; + } - button - .attr('title', text) - .removeClass(settings.loadingClass) - .removeClass(settings.initializationClass) - .addClass(className) - .find(settings.wishListButtonSingleView).text(text); + button + .attr('title', text) + .removeClass(settings.loadingClass) + .removeClass(settings.initializationClass) + .addClass(className) + .find(settings.wishListButtonSingleView).text(text); + }); }); }; @@ -298,6 +302,43 @@ return settings; }; + const _getWhilist = function (callback) { + if (wishlist.length > 0) { + callback(wishlist); + return true; + } + + _loadWhishlistState(callback); + }; + + const _loadWhishlistState = function (callback) { + const uri = $(ProductManager.settings.productManagerMainWrapper).data('load-whishlist-ajax-uri'); + + if (!uri) { + ProductManager.Messanger.showErrorMessage('Request failed: ' + 'Invalid url'); + return false; + } + + $.ajax({ + url: uri, + dataType: 'json' + }).done(function (data) { + if (data.wishList === undefined) { + return false; + } + + wishlist = Array.from(data.wishList).map(function (product) { + return product.uid; + }); + + callback(wishlist); + }).fail(function (jqXHR, textStatus) { + ProductManager.Messanger.showErrorMessage('Request failed: ' + textStatus); + }).always(function () { + ajaxLoadingInProgress = false; + }); + }; + return { init: init, initButtons: initButtons, From c80274155f25c065b897eb08a5be1990417c61d8 Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Mon, 2 Dec 2019 08:34:47 +0200 Subject: [PATCH 55/62] [TASK] Fix total price calculation --- Classes/Controller/AjaxJsonController.php | 39 +++++++++++++++++++ Configuration/TypoScript/setup.txt | 1 + .../Partials/WishList/RowsWithOrder.html | 3 +- ext_localconf.php | 4 +- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index 0fb7ec86..939048cb 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -237,4 +237,43 @@ public function wishlistProductsCountAction() $this->response->setStatus(200, 'OK'); $this->view->assign('value', $wishlistService->productsCount() ?? 0); } + + /** + * @return bool + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException + */ + public function updateOrderQuantitiesAction() + { + if (!GeneralUtility::_GP('quantities')) { + $this->response->setStatus(400, 'Quantities are required'); + return false; + } + + $quantities = GeneralUtility::_GP('quantities'); + + // REST violation yay!!! :D + $order = OrderUtility::getSessionOrder(); + + if (!$order) { + $this->response->setStatus(400, 'Order not found'); + return false; + } + + // Dont want to just set the whole quantities field in case it will contain incorrect data + /** @var Product $product */ + foreach ($order->getProducts() as $product) { + $uid = $product->getUid(); + if (array_key_exists($uid, $quantities)) { + $order->setProductQuantity($product, $quantities[$uid]); + } + } + + $this->orderRepository->update($order); + + $response = null; + $this->response->setStatus(200, 'OK'); + $this->view->assign('value', $response); + return true; + } } diff --git a/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.txt index c39bd7ed..e676ae46 100644 --- a/Configuration/TypoScript/setup.txt +++ b/Configuration/TypoScript/setup.txt @@ -315,6 +315,7 @@ PXA_PRODUCT_MANAGER_WISH_LIST { 5 = loadWishList 6 = totalOrderPrices 7 = wishlistProductsCount + 8 = updateOrderQuantities } } } diff --git a/Resources/Private/Partials/WishList/RowsWithOrder.html b/Resources/Private/Partials/WishList/RowsWithOrder.html index cf51c4e3..78b2cdbe 100644 --- a/Resources/Private/Partials/WishList/RowsWithOrder.html +++ b/Resources/Private/Partials/WishList/RowsWithOrder.html @@ -3,7 +3,8 @@
+ data-total-order-prices-ajax-uri="{f:uri.action(controller: 'AjaxJson', action: 'totalOrderPrices', absolute: '1', pageType: '{settings.wishList.pageType}')}" + data-update-order-quantities-ajax-uri="{f:uri.action(controller: 'AjaxJson', action: 'updateOrderQuantities', absolute: '1', pageType: '{settings.wishList.pageType}')}"> 'list, show, wishList, finishOrder, lazyList, comparePreView, compareView, groupedList, promotionList, addCouponCodeToOrder', 'Navigation' => 'show', 'AjaxProducts' => 'ajaxLazyList, latestVisited', - 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices, wishlistProductsCount', + 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices, wishlistProductsCount, updateOrderQuantities', 'Filter' => 'showFilter' ], // non-cacheable actions [ 'Product' => 'wishList, finishOrder, comparePreView, compareView, addCouponCodeToOrder', 'AjaxProducts' => 'ajaxLazyList, latestVisited', - 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices, wishlistProductsCount' + 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices, wishlistProductsCount, updateOrderQuantities' ] ); // @codingStandardsIgnoreEnd From 0ef73d95ea582a553649ca803f73a496be4360fe Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Mon, 2 Dec 2019 09:35:11 +0200 Subject: [PATCH 56/62] [TASK] Fix total price recalculation on product quantity change --- .../Partials/WishList/RowsWithOrder.html | 10 +- .../JavaScript/ProductManager.WishList.js | 104 ++++++++++++++---- 2 files changed, 89 insertions(+), 25 deletions(-) diff --git a/Resources/Private/Partials/WishList/RowsWithOrder.html b/Resources/Private/Partials/WishList/RowsWithOrder.html index 78b2cdbe..6a02cc4a 100644 --- a/Resources/Private/Partials/WishList/RowsWithOrder.html +++ b/Resources/Private/Partials/WishList/RowsWithOrder.html @@ -41,6 +41,7 @@ +
@@ -50,10 +51,11 @@ -
-
-
-
+ +
+
+
+ )
diff --git a/Resources/Public/JavaScript/ProductManager.WishList.js b/Resources/Public/JavaScript/ProductManager.WishList.js index 224ab52b..0f43537c 100644 --- a/Resources/Public/JavaScript/ProductManager.WishList.js +++ b/Resources/Public/JavaScript/ProductManager.WishList.js @@ -37,6 +37,9 @@ let wishlist = []; + let updateOrderRequest = null; + let updateOrderDispatcher = null; + /** * Main wish list function * @@ -171,12 +174,17 @@ ProductManager.Messanger.showErrorMessage('Request failed: ' + 'Invalid url'); } + $totalPrice.addClass(settings.loadingClass); + $totalTax.addClass(settings.loadingClass); + $.ajax({ url: uri, dataType: 'json' }).done(function (data) { - $totalPrice.text(data.totalPrice); - $totalTax.text(data.totalTaxPrice); + $totalPrice.find('.value').text(formattedPrice(data.totalPrice, $totalPrice)); + $totalPrice.removeClass(settings.loadingClass); + $totalTax.find('.value').text(formattedPrice(data.totalTaxPrice, $totalTax)); + $totalTax.removeClass(settings.loadingClass); }).fail(function (jqXHR, textStatus) { ProductManager.Messanger.showErrorMessage('Request failed: ' + textStatus); }).always(function () { @@ -204,8 +212,16 @@ $this.val(1); } - _updatePriceAndTax(); - _saveCurrentStateOfAmountOfProducts(); + if(updateOrderDispatcher) { + clearTimeout(updateOrderDispatcher); + } + + updateOrderDispatcher = setTimeout(function() { + _updateOrder(function () { + _updatePriceAndTax(); + _saveCurrentStateOfAmountOfProducts(); + }); + }, 500); }); }; @@ -216,20 +232,7 @@ * @private */ const _saveCurrentStateOfAmountOfProducts = function () { - let currentState = {}; - - if ($orderItemsAmount.length <= 0) { - return false; - } - - $orderItemsAmount.each(function () { - const $this = $(this); - let productUid = parseInt($this.data('product-uid')); - - if (productUid > 0) { - currentState[productUid] = parseInt($this.val()); - } - }); + const currentState = _getCurrentOrderState(); ProductManager.Main.setCookie( ORDER_STATE_COOKIE_NAME, @@ -258,8 +261,6 @@ * @public */ const initButtons = function ($buttons) { - const productsWishList = ProductManager.Main.getCookie('pxa_pm_wish_list') || ''; - $buttons.on('click', function (e) { e.preventDefault(); @@ -337,7 +338,7 @@ }).always(function () { ajaxLoadingInProgress = false; }); - } + }; /** * @@ -358,6 +359,67 @@ ); }; + /** + * Get current state + * + * @returns {{}|boolean} + * @private + */ + const _getCurrentOrderState = function () { + let currentState = {}; + + if ($orderItemsAmount.length <= 0) { + return false; + } + + $orderItemsAmount.each(function () { + const $this = $(this); + let productUid = parseInt($this.data('product-uid')); + + if (productUid > 0) { + currentState[productUid] = parseInt($this.val()); + } + }); + + return currentState; + }; + + /** + * + * @param callback + * @returns {boolean} + * @private + */ + const _updateOrder = function(callback) { + const currentState = _getCurrentOrderState(); + const uri = $(settings.wishListContainer).data('update-order-quantities-ajax-uri'); + + if (!uri) { + ProductManager.Messanger.showErrorMessage('Request failed: ' + 'Invalid url'); + return false; + } + + if (updateOrderRequest !== null) { + updateOrderRequest.abort(); + } + + updateOrderRequest = $.ajax({ + type: 'POST', + url: uri, + data: { + 'quantities': currentState + } + }).done(function (data) { + callback(); + }).fail(function (jqXHR, textStatus) { + if (status !== 0 && textStatus !== 'abort') { + ProductManager.Messanger.showErrorMessage('Request failed: ' + textStatus); + } + }).always(function () { + ajaxLoadingInProgress = false; + }); + }; + return { init: init, initButtons: initButtons, From 47e0e702e0be3927879a6e36e08dec55be6669f8 Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Tue, 3 Dec 2019 08:29:32 +0200 Subject: [PATCH 57/62] [TASK] Make subscription getter return subscription record --- Classes/Domain/Model/Order.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index fa0a2b13..3cedb318 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -125,9 +125,9 @@ class Order extends AbstractEntity protected $taxAtCheckout = 0.0; /** - * @var int + * @var \Pixelant\PxaProductManager\Domain\Model\Subscription */ - protected $subscription = 0; + protected $subscription = null; /** * __construct @@ -553,18 +553,18 @@ public function getNumberOfProducts() } /** - * @return int + * @return Subscription|null */ - public function getSubscription(): int + public function getSubscription(): ?Subscription { return $this->subscription; } /** - * @param int $subscription + * @param \Pixelant\PxaProductManager\Domain\Model\Subscription $subscription * @return Order */ - public function setSubscription(int $subscription): Order + public function setSubscription(Subscription $subscription): Order { $this->subscription = $subscription; return $this; From 91c34a2a29d63e0aa894bb71d8bca555107ff775 Mon Sep 17 00:00:00 2001 From: Pavlo Zaporozkyi Date: Tue, 3 Dec 2019 15:59:25 +0200 Subject: [PATCH 58/62] [TASK] Fix order duplication --- Classes/Service/WishlistService.php | 12 ++++++++---- Classes/Utility/OrderUtility.php | 10 ++++++++++ Classes/Utility/ProductUtility.php | 3 ++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Classes/Service/WishlistService.php b/Classes/Service/WishlistService.php index f4fa0f7f..754f84af 100644 --- a/Classes/Service/WishlistService.php +++ b/Classes/Service/WishlistService.php @@ -11,12 +11,16 @@ */ class WishlistService { - public function hasProduct() - { - } - + /** + * @return int + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException + */ public function productsCount() { + if (! OrderUtility::sessionOrderExists()) { + return 0; + } + $order = OrderUtility::getSessionOrder(); return $order->getNumberOfProducts(); } diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index fabbd851..ecf68e8a 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -101,6 +101,16 @@ public static function getSessionOrder() return $order; } + + /** + * @return bool + */ + public static function sessionOrderExists() + { + $orderUid = (int) $GLOBALS['TSFE']->fe_user->getKey('ses', self::SESSION_KEY); + return $orderUid > 0; + } + /** * Adds a product to the session order using the product's UID * diff --git a/Classes/Utility/ProductUtility.php b/Classes/Utility/ProductUtility.php index a5954ea2..3c5f97d8 100644 --- a/Classes/Utility/ProductUtility.php +++ b/Classes/Utility/ProductUtility.php @@ -252,10 +252,11 @@ public static function formatPrice(float $price): string * Uids of wish list * * @return array + * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException */ public static function getWishList(): array { - return OrderUtility::getSessionOrder()->getProducts()->getArray(); + return OrderUtility::sessionOrderExists() ? OrderUtility::getSessionOrder()->getProducts()->getArray() : []; } /** From 20f1abd4c6a371742c0f956f0036b2dc0739c9c7 Mon Sep 17 00:00:00 2001 From: mabolek Date: Tue, 7 Apr 2020 15:42:20 +0200 Subject: [PATCH 59/62] [FEATURE] Persistable order state hash (#180) --- Classes/Domain/Model/Order.php | 34 ++++++++++++++++++++++++ Classes/Utility/OrderUtility.php | 44 ++++++++++++++++++++++++++++++++ ext_tables.sql | 1 + 3 files changed, 79 insertions(+) diff --git a/Classes/Domain/Model/Order.php b/Classes/Domain/Model/Order.php index 3cedb318..25124027 100644 --- a/Classes/Domain/Model/Order.php +++ b/Classes/Domain/Model/Order.php @@ -26,6 +26,7 @@ * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use Pixelant\PxaProductManager\Utility\OrderUtility; use TYPO3\CMS\Extbase\Domain\Model\FrontendUser; use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; use TYPO3\CMS\Extbase\Persistence\ObjectStorage; @@ -129,6 +130,13 @@ class Order extends AbstractEntity */ protected $subscription = null; + /** + * The order state hash + * + * @var string + */ + protected $stateHash = ''; + /** * __construct */ @@ -569,4 +577,30 @@ public function setSubscription(Subscription $subscription): Order $this->subscription = $subscription; return $this; } + + /** + * Get the order's state hash + * + * @see OrderUtility::calculateOrderStateHash() + * + * @return string + */ + public function getStateHash(): string + { + return $this->stateHash; + } + + /** + * Set the order's state hash + * + * @see OrderUtility::calculateOrderStateHash() + * + * @param string $stateHash + */ + public function setStateHash(string $stateHash) + { + $this->stateHash = $stateHash; + } + + } diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index ecf68e8a..15022c24 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -3,6 +3,7 @@ namespace Pixelant\PxaProductManager\Utility; +use Pixelant\PxaProductManager\Domain\Model\Coupon; use Pixelant\PxaProductManager\Domain\Model\Order; use Pixelant\PxaProductManager\Domain\Model\Product; use Pixelant\PxaProductManager\Domain\Repository\OrderRepository; @@ -200,4 +201,47 @@ public static function updateOrder(Order $order) { GeneralUtility::makeInstance(ObjectManager::class)->get(OrderRepository::class)->update($order); } + + /** + * Returns a hash based on products, product count, and coupons + * + * Hookable with $GLOBALS['TYPO3_CONF_VARS']['EXT']['pxa_product_manager']['Utility/OrderUtility.php']['calculateOrderStateHash'] + * which receives $hashBaseArray as parameter and $order as reference. + * + * @param Order $order + * + * @return string + */ + public static function calculateOrderStateHash(Order $order): string + { + $hashBaseArray = []; + + //Add products and product quantities + $hashBaseArray['products'] = []; + /** @var Product $product */ + foreach ($order->getProducts() as $product) { + $hashBaseArray['products'][] = [ + 'uid' => $product->getUid(), + 'quantity' => $order->getProductQuantity($product) + ]; + } + + //Add coupons + $hashBaseArray['coupons'] = []; + /** @var Coupon $coupon */ + foreach ($order->getCoupons() as $coupon) { + $hashBaseArray['coupons'] = [ + 'uid' => $coupon->getUid() + ]; + } + + //Handle $hashBaseArray additions through a hook + if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXT']['pxa_product_manager']['Utility/OrderUtility.php']['calculateOrderStateHash'])) { + foreach($GLOBALS['TYPO3_CONF_VARS']['EXT']['pxa_product_manager']['Utility/OrderUtility.php']['calculateOrderStateHash'] as $functionReference) { + $hashBaseArray = GeneralUtility::callUserFunction($functionReference, $hashBaseArray, $order); + } + } + + return md5(serialize($hashBaseArray)); + } } diff --git a/ext_tables.sql b/ext_tables.sql index 53fae97a..65ed8f98 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -437,6 +437,7 @@ CREATE TABLE tx_pxaproductmanager_domain_model_order ( price_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, tax_at_checkout double(11,2) DEFAULT '0.00' NOT NULL, subscription int(11) unsigned DEFAULT '0' NOT NULL, + state_hash varchar(32) DEFAULT '' NOT NULL, tstamp int(11) unsigned DEFAULT '0' NOT NULL, crdate int(11) unsigned DEFAULT '0' NOT NULL, From 28123eb56454570e39d0a4ae936c9220b2a49176 Mon Sep 17 00:00:00 2001 From: mabolek Date: Wed, 3 Jun 2020 15:33:07 +0200 Subject: [PATCH 60/62] [BUGFIX] Ensure $order is set before checking if it's complete --- Classes/Utility/OrderUtility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Utility/OrderUtility.php b/Classes/Utility/OrderUtility.php index 15022c24..6c06bb94 100644 --- a/Classes/Utility/OrderUtility.php +++ b/Classes/Utility/OrderUtility.php @@ -83,7 +83,7 @@ public static function getSessionOrder() if ($order !== null && !$order->isComplete()) { return $order; - } elseif ($order->isComplete()) { + } elseif ($order !== null && $order->isComplete()) { self::$sessionOrder = null; } } From 3f2f6101292a11bda43df35ba0bb82e16e445ef6 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 5 Jun 2020 12:06:39 +0200 Subject: [PATCH 61/62] [FEATURE] Configurable maximum coupons per order (#190) --- Classes/Controller/ProductController.php | 20 ++++++++++++ Classes/ViewHelpers/SettingsViewHelper.php | 34 +++++++++++++++++++++ Configuration/TypoScript/setup.txt | 5 +++ Resources/Private/Language/locallang.xlf | 6 ++++ Resources/Private/Language/sv.locallang.xlf | 29 ++++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 Classes/ViewHelpers/SettingsViewHelper.php diff --git a/Classes/Controller/ProductController.php b/Classes/Controller/ProductController.php index f9448a17..f721053d 100644 --- a/Classes/Controller/ProductController.php +++ b/Classes/Controller/ProductController.php @@ -287,6 +287,16 @@ public function wishListAction(bool $sendOrder = false) * @throws \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException */ public function addCouponCodeToOrderAction($couponCode = '') { + if (!$this->settings['coupons']['enable']) { + $this->addFlashMessage( + $this->translate('fe.couponCode.notEnabled'), + '', + FlashMessage::ERROR + ); + + $this->redirect('wishList'); + } + if($couponCode === '') { $this->addFlashMessage( $this->translate('fe.couponCode.noCodeSuppliedWarning'), @@ -324,6 +334,16 @@ public function addCouponCodeToOrderAction($couponCode = '') { $this->redirect('wishList'); } + if ($this->settings['coupons']['orderMax'] && $order->getCoupons()->count() > $this->settings['coupons']['orderMax']) { + $this->addFlashMessage( + $this->translate('fe.couponCode.orderMaxReached', [$this->settings['coupons']['orderMax']]), + '', + FlashMessage::ERROR + ); + + $this->redirect('wishList'); + } + $order->addCoupon($coupon); $this->orderRepository->update($order); diff --git a/Classes/ViewHelpers/SettingsViewHelper.php b/Classes/ViewHelpers/SettingsViewHelper.php new file mode 100644 index 00000000..dcc5d688 --- /dev/null +++ b/Classes/ViewHelpers/SettingsViewHelper.php @@ -0,0 +1,34 @@ + The coupon code was successfully applied. + + Attempted to add a discount code, but the feature is not enabled. + + + You cannot add more discount codes to this order. The maximum number of coupons (%s) has been reached. + diff --git a/Resources/Private/Language/sv.locallang.xlf b/Resources/Private/Language/sv.locallang.xlf index 371169b6..6d55834a 100644 --- a/Resources/Private/Language/sv.locallang.xlf +++ b/Resources/Private/Language/sv.locallang.xlf @@ -196,6 +196,35 @@ Produkter + + + Lägg till rabattkod + + + Ange kod + + + Ange rabattkod + + + Ingen rabattkod är angiven. Ange koden i textfältet innan du klickar på knappen Lägg till rabattkod. + + + Den rabattkod du angav kunde inte hittas. Kontrollera att du har angett den korrekt. + + + Du kan inte lägga till en rabattkod två gånger. Den kod du angav fanns redan i listan. + + + Rabattkoden lades till. + + + Försökte lägga till en rabattkod, men funktionen är inte aktiverad. + + + Du kan inte lägga till fler rabattkoder i den här beställningen. Det maximala antalet koder (%s) har uppnåtts. + + Inga resultat From c0f4d27b0f5b8a1efdd85a7d8c50fdc9a9ad0eb5 Mon Sep 17 00:00:00 2001 From: mabolek Date: Fri, 26 Jun 2020 14:30:04 +0200 Subject: [PATCH 62/62] [FEATURE] Return total price also without coupon --- Classes/Controller/AjaxJsonController.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index 939048cb..2d5ef241 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -208,6 +208,14 @@ public function totalOrderPricesAction() $totalPrice = $priceService->calculatePrice(); $totalTaxPrice = $priceService->calculateTax(); + + $orderWithoutCoupon = clone $priceService->getOrder(); + foreach ($orderWithoutCoupon->getCoupons() as $coupon) { + $orderWithoutCoupon->removeCoupon($coupon); + } + + $totalPriceThereafter = $priceService->calculatePrice(); + $totalTaxPriceThereafter = $priceService->calculateTax(); } catch (\Exception $e) { $this->response->setStatus(500, 'Price calculation error'); return null; @@ -220,7 +228,9 @@ public function totalOrderPricesAction() $response = [ 'totalPrice' => $totalPrice, - 'totalTaxPrice' => $totalTaxPrice + 'totalTaxPrice' => $totalTaxPrice, + 'totalPriceThereafter' => $totalPriceThereafter, + 'totalTaxPriceThereafter' => $totalTaxPriceThereafter ]; $this->response->setStatus(200, 'OK');