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
+
+