diff --git a/Classes/Controller/AjaxJsonController.php b/Classes/Controller/AjaxJsonController.php index 8d75b947..2d5ef241 100644 --- a/Classes/Controller/AjaxJsonController.php +++ b/Classes/Controller/AjaxJsonController.php @@ -27,8 +27,13 @@ ***************************************************************/ 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; /** @@ -51,33 +56,47 @@ 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); + $response['inList'] = !$inWishList; - if (!$inWishList && count(ProductUtility::getWishList()) >= $limit) { - $message = $this->translate('fe.error_limit'); - } else { - MainUtility::{$inWishList ? 'removeValueFromListCookie' : 'addValueToListCookie'}( - ProductUtility::WISH_LIST_COOKIE_NAME, - $wishProduct->getUid(), - $limit - ); + if ($inWishList) { + $order->removeProduct($wishProduct); + $this->orderRepository->update($order); + + $response['success'] = true; $message = $this->translate( - $inWishList ? 'fe.remove_from_list' : 'fe.added_to_list', + 'fe.remove_from_list', [ $this->translate('fe.wish_list') ] ); + } else { + if ($order->getProductsQuantityTotal() + 1 > $limit) { + $message = $this->translate('fe.error_limit'); + unset($response['inList']); + } else { + $order->addProduct($wishProduct); - $response['success'] = true; - $response['inList'] = !$inWishList; + $this->orderRepository->update($order); + + $response['success'] = true; + + $message = $this->translate( + 'fe.added_to_list', + [ + $this->translate('fe.wish_list') + ] + ); + } } } @@ -178,4 +197,93 @@ public function addLatestVisitedProductAction(Product $product) $this->view->assign('value', ['success' => true]); } + + /** + * @return mixed + */ + public function totalOrderPricesAction() + { + try { + $priceService = (new PriceServiceFactory())->createFromSession(); + + $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; + } + + if ($totalPrice < 0 || $totalTaxPrice < 0) { + $this->response->setStatus(500, 'Price calculation error'); + return null; + } + + $response = [ + 'totalPrice' => $totalPrice, + 'totalTaxPrice' => $totalTaxPrice, + 'totalPriceThereafter' => $totalPriceThereafter, + 'totalTaxPriceThereafter' => $totalTaxPriceThereafter + ]; + + $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); + } + + /** + * @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/Classes/Controller/ProductController.php b/Classes/Controller/ProductController.php index b564de08..f721053d 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; @@ -229,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'; @@ -267,20 +272,91 @@ 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' => $this->getProductsFromCookieList(ProductUtility::WISH_LIST_COOKIE_NAME), - 'orderProducts' => $orderState ?? [], - 'sendOrder' => $sendOrder + 'products' => $order->getProducts(), + 'orderProducts' => $order->getProductsQuantity(), + 'sendOrder' => $sendOrder, + 'coupons' => $order->getCoupons() ]); } + /** + * @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 = '') { + 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'), + '', + FlashMessage::WARNING + ); + + $this->redirect('wishList'); + } + + /** @var CouponRepository $couponRepository */ + $couponRepository = $this->objectManager->get(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'); + } + + 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); + + $this->addFlashMessage( + $this->translate('fe.couponCode.newCodeAdded'), + '', + FlashMessage::OK + ); + + $this->redirect('wishList'); + } + /** * Finish order text * Show some successful texts @@ -919,11 +995,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); diff --git a/Classes/Domain/Model/Coupon.php b/Classes/Domain/Model/Coupon.php new file mode 100644 index 00000000..a28be286 --- /dev/null +++ b/Classes/Domain/Model/Coupon.php @@ -0,0 +1,210 @@ +name; + } + + /** + * @param string $title + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @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->usageLimit; + } + + /** + * @param int $maxUses + */ + public function setUsageLimit(int $usageLimit) + { + $this->usageLimit = $usageLimit; + } + + /** + * @return float + */ + public function getCostLimit(): float + { + return $this->costLimit; + } + + /** + * @param float $maxCost + */ + public function setCostLimit(float $costLimit) + { + $this->costLimit = $costLimit; + } + + /** + * @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..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; @@ -36,6 +37,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 */ @@ -90,6 +104,39 @@ 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; + + /** + * @var \Pixelant\PxaProductManager\Domain\Model\Subscription + */ + protected $subscription = null; + + /** + * The order state hash + * + * @var string + */ + protected $stateHash = ''; + /** * __construct */ @@ -110,6 +157,7 @@ public function __construct() protected function initStorageObjects() { $this->products = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); + $this->coupons = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage(); } /** @@ -120,7 +168,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); } /** @@ -131,7 +183,14 @@ public function addProduct(\Pixelant\PxaProductManager\Domain\Model\Product $pro */ public function removeProduct(\Pixelant\PxaProductManager\Domain\Model\Product $productToRemove) { - $this->products->detach($productToRemove); + $this->setProductQuantity($productToRemove, 0); + + foreach ($this->products as $product) { + if ($product->getUid() === $productToRemove->getUid()) { + $this->products->detach($product); + break; + } + } } /** @@ -215,6 +274,45 @@ 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 + * + * @param Product $product + * @return int + */ + 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(); + + if ($quantity === 0) { + unset($productQuantities[$product->getUid()]); + } else { + $productQuantities[$product->getUid()] = $quantity; + } + + $this->setProductsQuantity($productQuantities); + } + /** * Save products quantity as serialized string * @@ -330,9 +428,9 @@ public function removeOrderField(string $name) } /** - * @return \DateTime + * @return \DateTime|null */ - public function getCrdate(): \DateTime + public function getCrdate(): ?\DateTime { return $this->crdate; } @@ -377,4 +475,132 @@ 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; + } + + /** + * 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 + * + * @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; + } + + /** + * @return int + */ + public function getNumberOfProducts() + { + return $this->getProducts()->count(); + } + + /** + * @return Subscription|null + */ + public function getSubscription(): ?Subscription + { + return $this->subscription; + } + + /** + * @param \Pixelant\PxaProductManager\Domain\Model\Subscription $subscription + * @return Order + */ + 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/Domain/Model/Product.php b/Classes/Domain/Model/Product.php index 83f6a70c..abc8ba60 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 * @@ -1392,7 +1402,18 @@ 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)); + } + + /** + * 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(); } /** diff --git a/Classes/Domain/Model/Subscription.php b/Classes/Domain/Model/Subscription.php new file mode 100644 index 00000000..83bcc740 --- /dev/null +++ b/Classes/Domain/Model/Subscription.php @@ -0,0 +1,295 @@ + + */ + 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; + } + + /** + * @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/CouponRepository.php b/Classes/Domain/Repository/CouponRepository.php new file mode 100644 index 00000000..28058896 --- /dev/null +++ b/Classes/Domain/Repository/CouponRepository.php @@ -0,0 +1,60 @@ +createQuery(); + + /** @var Coupon|null $coupon */ + $coupon = $query + ->matching($query->like('code', $code, false)) + ->execute() + ->getFirst(); + + return $coupon; + } +} diff --git a/Classes/Domain/Repository/SubscriptionRepository.php b/Classes/Domain/Repository/SubscriptionRepository.php new file mode 100644 index 00000000..d9ceafef --- /dev/null +++ b/Classes/Domain/Repository/SubscriptionRepository.php @@ -0,0 +1,25 @@ +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 @@ +setOrder($order); + return $priceService; + } +} diff --git a/Classes/Service/PriceService.php b/Classes/Service/PriceService.php new file mode 100644 index 00000000..c80305f0 --- /dev/null +++ b/Classes/Service/PriceService.php @@ -0,0 +1,536 @@ +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 Coupon + */ + public function getCoupon(): ?Coupon + { + return $this->coupon; + } + + /** + * @param Coupon $coupon + */ + public function setCoupon(Coupon $coupon): self + { + $this->coupon = $coupon; + return $this; + } + + /** + * Reset the service properties + * + * @return $this + */ + public function reset() + { + $this->order = null; + $this->product = null; + $this->coupon = null; + + 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 + * + * @return float + */ + public function calculatePrice(): float + { + return $this->applyOrderCouponsToValue($this->calculatePriceBeforeTaxAndCoupon()) + $this->calculateTax(); + } + + /** + * Returns the total tax + * + * @return float + */ + public function calculateTax(): float + { + 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 $this->applyOrderCouponsToValue($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 + * + * @return float + */ + public function calculateCouponValue(): float + { + if (($this->order === null && $this->coupon === null) || ($this->order === null && $this->product === null)) { + return 0.0; + } + + $beforePrice = $this->calculateProductPrice(); + + if ($this->order === null) { + return $this->applyCouponToValue($beforePrice) - $beforePrice; + } + + return $this->applyOrderCouponsToValue($beforePrice) - $beforePrice; + } + + /** + * Returns the price before tax, but including coupons + * + * @return float + */ + public function calculatePriceBeforeTax(): float + { + return $this->calculatePriceBeforeTaxAndCoupon() + $this->calculateCouponValue(); + } + + /** + * Returns the price before tax and coupons + * + * @return 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->product = $product; + $total += $this->calculateOrderTotalForProductBeforeTaxAndCoupon(); + } + + $this->product = null; + + return $total; + } + + /** + * Returns the price including tax, but excluding coupons + * + * @return float + */ + 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(); + } + + return $this->product->getPriceForCheckout(); + } + + /** + * The product price including tax and coupon + * + * @return float + */ + public function calculateProductPrice(): float + { + if ($this->product === null) { + return 0.0; + } + + if($this->order === null) { + return $this->applyCouponToValue($this->calculateProductPriceBeforeCoupon()); + } + + return $this->applyOrderCouponsToValue($this->calculateProductPriceBeforeCoupon()); + } + + /** + * 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() - $this->product->getTax(); + } + + return $this->product->getPriceForCheckout() - $this->product->getTaxForCheckout(); + } + + /** + * Returns the product total (i.e. price * quantity) for product without tax and coupon codes + * + * @return float + */ + public function calculateOrderTotalForProductBeforeTaxAndCoupon(): float + { + if ($this->product === null) { + return 0.0; + } + + if ($this->order === null) { + return $this->calculateProductPriceBeforeTaxAndCoupon(); + } + + 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->product === null) { + return 0.0; + } + + 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->product === null) { + return 0.0; + } + + if ($this->order === null) { + return $this->calculateCouponValue(); + } + + return $this->calculateOrderTotalForProduct() - $this->applyOrderCouponsToValue($this->calculateOrderTotalForProduct()); + } + + /** + * Returns the total tax for within an order (i.e. tax * quantity) $this->product + * + * @return float + */ + public function calculateOrderTotalTaxForProduct(): float + { + if ($this->product === null) { + return 0.0; + } + + if ($this->order === null) { + return $this->calculateProductTax(); + } + + return $this->order->getProductQuantity($this->getProduct()) * $this->calculateProductTax(); + } + + /** + * 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) { + $oldLocale = setlocale(LC_ALL, 0); + setlocale(LC_ALL, $locale); + $fractionalDigits = localeconv()['int_frac_digits']; + } + + $convertedValue = (int) ($value * pow(10, $fractionalDigits)); + + if ($locale !== null) { + setlocale(LC_ALL, $oldLocale); + } + + 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; + } + + /** + * 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(), + 'formatForIso4217(calculatePrice())' => self::formatForIso4217($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/Service/SubscriptionService.php b/Classes/Service/SubscriptionService.php new file mode 100644 index 00000000..6eb53c30 --- /dev/null +++ b/Classes/Service/SubscriptionService.php @@ -0,0 +1,139 @@ +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 + */ + public function injectSubscriptionRepository(SubscriptionRepository $subscriptionRepository) + { + $this->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; + } + + /** + * @param Subscription $subscription + * @return bool + */ + public function isItRenewalTime(Subscription $subscription) + { + return $this->today > $subscription->getNextTry(); + } +} diff --git a/Classes/Service/WishlistService.php b/Classes/Service/WishlistService.php new file mode 100644 index 00000000..754f84af --- /dev/null +++ b/Classes/Service/WishlistService.php @@ -0,0 +1,27 @@ +getNumberOfProducts(); + } +} 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..6c06bb94 --- /dev/null +++ b/Classes/Utility/OrderUtility.php @@ -0,0 +1,247 @@ +, 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; + + /** + * @var Order $sessionOrder + */ + protected static $sessionOrder = null; + + /** + * 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(ObjectManager::class)->get(OrderRepository::class); + + $orderUid = (int) $GLOBALS['TSFE']->fe_user->getKey('ses', self::SESSION_KEY); + + if ($orderUid > 0) { + /** @var Order $order */ + if (self::$sessionOrder !== null) { + $order = self::$sessionOrder; + } else { + $order = $orderRepository->findByUid($orderUid); + self::$sessionOrder = $order; + } + + if ($order !== null && !$order->isComplete()) { + return $order; + } elseif ($order !== null && $order->isComplete()) { + self::$sessionOrder = null; + } + } + + $order = MainUtility::getObjectManager()->get(Order::class); + + $orderRepository->add($order); + + /** @var PersistenceManagerInterface $persistanceManager */ + $persistanceManager = GeneralUtility::makeInstance(PersistenceManager::class); + + //Make sure we get a UID + $persistanceManager->persistAll(); + + $GLOBALS['TSFE']->fe_user->setKey('ses', self::SESSION_KEY, $order->getUid()); + + 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 + * + * @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) + { + $wishListLimitFromSettings = ConfigurationUtility::getSettingsByPath('wishList/limit'); + + $order = self::getSessionOrder(); + + //If there are too many products, remove one. + if ( + ((int) $wishListLimitFromSettings > 0 && $order->getProductsQuantityTotal() + 1 > (int) $wishListLimitFromSettings) + || + ((int) $wishListLimitFromSettings === 0 && $order->getProductsQuantityTotal() + 1 > self::MAX_PRODUCTS) + ) { + $order->getProducts()->rewind(); + 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); + + 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(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/Classes/Utility/ProductUtility.php b/Classes/Utility/ProductUtility.php index f8d84920..3c5f97d8 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; @@ -251,12 +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 { - $list = $_COOKIE[self::WISH_LIST_COOKIE_NAME] ?: ''; - - return GeneralUtility::intExplode(',', $list, true); + return OrderUtility::sessionOrderExists() ? OrderUtility::getSessionOrder()->getProducts()->getArray() : []; } /** @@ -264,12 +264,43 @@ 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 { - $list = $_COOKIE[self::WISH_LIST_COOKIE_NAME] ?: ''; + if (!is_object($product)) { + $product = self::getProductByUid($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); + } + + /** + * 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; } /** 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 @@ + 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/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php b/Configuration/TCA/tx_pxaproductmanager_domain_model_coupon.php new file mode 100644 index 00000000..29cf118c --- /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_count', + '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..0a203efc 100644 --- a/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_order.php @@ -20,18 +20,18 @@ 'starttime' => 'starttime', 'endtime' => 'endtime', ], - 'searchFields' => 'products', + 'searchFields' => 'products, external_id', #'hideTable' => true, '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, subscription', ], '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, external_id, subscription, --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', ], ], 'columns' => [ @@ -185,7 +185,10 @@ 'exclude' => 1, 'label' => 'External id', 'config' => [ - 'type' => 'passthrough' + 'type' => 'input', + 'size' => 30, + 'eval' => 'trim', + 'readOnly' => true ] ], 'crdate' => [ @@ -203,6 +206,71 @@ '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' + ], + ], + 'subscription' => [ + 'exclude' => 1, + 'label' => $ll . '.subscription', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'tx_pxaproductmanager_domain_model_subscription', + 'size' => 1, + 'maxitems' => 1, + 'default' => 0, + 'items' => [ + [$ll . '.subscription.single', 0] + ], + '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 new file mode 100644 index 00000000..992e7e11 --- /dev/null +++ b/Configuration/TCA/tx_pxaproductmanager_domain_model_subscription.php @@ -0,0 +1,166 @@ + [ + 'title' => $ll, + 'label' => 'uid', + '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_default_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/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.txt index 2815ec72..ee0df2b1 100644 --- a/Configuration/TypoScript/setup.txt +++ b/Configuration/TypoScript/setup.txt @@ -59,6 +59,11 @@ plugin.tx_pxaproductmanager { } } + coupons { + enable = 1 + orderMax = 1 + } + email { senderName = {$plugin.tx_pxaproductmanager.settings.email.senderName} senderEmail = {$plugin.tx_pxaproductmanager.settings.email.senderEmail} @@ -154,6 +159,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 { @@ -303,6 +318,9 @@ PXA_PRODUCT_MANAGER_WISH_LIST { 3 = loadCompareList 4 = emptyCompareList 5 = loadWishList + 6 = totalOrderPrices + 7 = wishlistProductsCount + 8 = updateOrderQuantities } } } diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 5622fbfb..7f115d0d 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -202,9 +202,37 @@ - Products + Products + + Add discount code + + + Enter code + + + 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. + + + 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. + + No results diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 87b19959..a3579564 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -264,6 +264,58 @@ Checkout type + + Coupons + + + Price at checkout + + + Tax at checkout + + + Recurring payments + + + Subscription + + + Single purchase + + + + 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 + @@ -398,6 +450,51 @@ Default + + + + Subscription + + + Renew date + + + Next try + + + Status + + + Active + + + Paused + + + Cancelled + + + Last renew status + + + Attempts left + + + Orders + + + Serialized products quantity + + + Subscription period + + + Week subscription + + + Month subscription + + - \ No newline at end of file + 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 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/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/Private/Partials/WishList/RowsWithOrder.html b/Resources/Private/Partials/WishList/RowsWithOrder.html index 2c7fa932..6a02cc4a 100644 --- a/Resources/Private/Partials/WishList/RowsWithOrder.html +++ b/Resources/Private/Partials/WishList/RowsWithOrder.html @@ -1,7 +1,11 @@ -
+
+ @@ -21,6 +25,13 @@
+ +
+ +
+
+
@@ -30,6 +41,7 @@ +
@@ -39,10 +51,11 @@ -
-
-
-
+ +
+
+
+ )
@@ -90,6 +103,8 @@
+ +
0 - @@ -178,7 +193,7 @@ @@ -205,8 +220,26 @@
+ + {coupon.code} + + + +
+
+ +
+ + + +
+
+
+
+
+ - \ 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..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} + +
- \ No newline at end of file + diff --git a/Resources/Public/JavaScript/ProductManager.Settings.js b/Resources/Public/JavaScript/ProductManager.Settings.js index 87408bd1..052116cd 100644 --- a/Resources/Public/JavaScript/ProductManager.Settings.js +++ b/Resources/Public/JavaScript/ProductManager.Settings.js @@ -22,7 +22,10 @@ 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', + wishListCartContainer: '.pm-products-wishlist-cart:first', + productManagerMainWrapper: '.products-list-view-wrapper:first' }; if (ProductManager.settings.wishlistTSSettings) { @@ -70,4 +73,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..0f43537c 100644 --- a/Resources/Public/JavaScript/ProductManager.WishList.js +++ b/Resources/Public/JavaScript/ProductManager.WishList.js @@ -35,6 +35,11 @@ $totalPrice, $totalTax; + let wishlist = []; + + let updateOrderRequest = null; + let updateOrderDispatcher = null; + /** * Main wish list function * @@ -53,7 +58,9 @@ ); ajaxLoadingInProgress = false; - _updatePriceAndTax(); + if ($(settings.wishListContainer).length > 0) { + _updatePriceAndTax(); + } _saveCurrentStateOfAmountOfProducts(); _trackOrderAmountChanges(); }; @@ -160,91 +167,29 @@ }); }; - /** - * 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('|'), + $totalPrice.addClass(settings.loadingClass); + $totalTax.addClass(settings.loadingClass); - 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.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 () { + ajaxLoadingInProgress = false; }); - - $totalTax.text( - sprintf( - currencyFormat, - ProductManager.Main.numberFormat(sum, decimals, decimalSep, thousandsSep) - ) - ); - }; - - const _updatePriceAndTax = function () { - _updateTotalPrice(); - _updateTotalTax(); }; /** @@ -259,6 +204,7 @@ } $orderItemsAmount.on('change', function () { + const $this = $(this); let value = parseInt($this.val()); @@ -266,8 +212,16 @@ $this.val(1); } - _updatePriceAndTax(); - _saveCurrentStateOfAmountOfProducts(); + if(updateOrderDispatcher) { + clearTimeout(updateOrderDispatcher); + } + + updateOrderDispatcher = setTimeout(function() { + _updateOrder(function () { + _updatePriceAndTax(); + _saveCurrentStateOfAmountOfProducts(); + }); + }, 500); }); }; @@ -278,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, @@ -320,8 +261,6 @@ * @public */ const initButtons = function ($buttons) { - const productsWishList = ProductManager.Main.getCookie('pxa_pm_wish_list') || ''; - $buttons.on('click', function (e) { e.preventDefault(); @@ -330,26 +269,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); + }); }); }; @@ -362,6 +303,123 @@ 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; + }); + }; + + /** + * + * @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) + ); + }; + + /** + * 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, @@ -370,4 +428,4 @@ })(); w.ProductManager = ProductManager; -})(window, $); \ No newline at end of file +})(window, $); diff --git a/Resources/Public/JavaScript/ProductManager.js b/Resources/Public/JavaScript/ProductManager.js index d38b575d..e2cc2911 100644 --- a/Resources/Public/JavaScript/ProductManager.js +++ b/Resources/Public/JavaScript/ProductManager.js @@ -157,22 +157,25 @@ * @param $cartCounters * @param modifier */ - updateCartCounter: function ($mainCartCounter, $cartCounters, modifier) { - modifier = modifier || 0; + updateCartCounter: function ($mainCartCounter, $cartCounters, newValue) { + const uri = $(ProductManager.settings.wishList.wishListCartContainer).data('wishlist-product-count-ajax-uri'); - if ($mainCartCounter.length === 1) { - let currentValue = parseInt($mainCartCounter.text().trim()); - if (isNaN(currentValue)) { - currentValue = 0; - } + if (!uri) { + ProductManager.Messanger.showErrorMessage('Request failed: ' + 'Invalid url'); + return false; + } - let newValue = currentValue + modifier; - newValue = newValue > 0 ? newValue : 0; + $.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; + }); - if ($cartCounters.length >= 1) { - $cartCounters.text(newValue); - } - } }, /** @@ -204,6 +207,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 * @@ -341,4 +354,4 @@ $(document).ready(function () { ProductManager.Main.init(); -}); \ No newline at end of file +}); diff --git a/ext_localconf.php b/ext_localconf.php index 9154c639..d5390c97 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -11,17 +11,17 @@ 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', + 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices, wishlistProductsCount, updateOrderQuantities', 'Filter' => 'showFilter' ], // non-cacheable actions [ - 'Product' => 'wishList, finishOrder, comparePreView, compareView', + 'Product' => 'wishList, finishOrder, comparePreView, compareView, addCouponCodeToOrder', 'AjaxProducts' => 'ajaxLazyList, latestVisited', - 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct' + 'AjaxJson' => 'toggleWishList, toggleCompareList, loadCompareList, emptyCompareList, loadWishList, addLatestVisitedProduct, totalOrderPrices, wishlistProductsCount, updateOrderQuantities' ] ); // @codingStandardsIgnoreEnd diff --git a/ext_tables.sql b/ext_tables.sql index c330305d..65ed8f98 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -429,10 +429,15 @@ 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, + 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, @@ -464,6 +469,19 @@ CREATE TABLE tx_pxaproductmanager_order_product_mm ( KEY uid_foreign (uid_foreign) ); +# +# Table structure for table 'tx_pxaproductmanager_order_coupon_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, + 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 +548,60 @@ 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) +); + +# +# 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) +);