diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..ef61a94 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,34 @@ +name: Module checks +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: setup php + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + tools: composer:v2 + - uses: actions/checkout@v2 + - name: validate composer json + run: composer validate + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/${{ env.namespace }}-source/vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + - name: PHPUnit and PHPCS + run: | + echo '{"http-basic": {"repo.magento.com": {"username": "${{ secrets.REPO_USERNAME }}","password": "${{ secrets.REPO_PASS }}"}}}' > auth.json + composer create-project --repository-url=https://repo.magento.com/ magento/project-community-edition=2.4.3 m24 + mkdir -p m24/app/code/Buckaroo/SecondChance/ + rsync -r --exclude='m24' ./ m24/app/code/Buckaroo/SecondChance/ + ./m24/vendor/bin/phpcs --standard=Magento2 m24/app/code/Buckaroo/SecondChance/ \ No newline at end of file diff --git a/Api/Data/SecondChanceInterface.php b/Api/Data/SecondChanceInterface.php new file mode 100644 index 0000000..4c2736f --- /dev/null +++ b/Api/Data/SecondChanceInterface.php @@ -0,0 +1,61 @@ +logger = $logger; + $this->secondChanceRepository = $secondChanceRepository; + } + + /** + * Process action + * + * @return \Magento\Framework\App\ResponseInterface + * @throws \Exception + */ + public function execute() + { + if ($token = $this->getRequest()->getParam('token')) { + $this->secondChanceRepository->getSecondChanceByToken($token); + } + return $this->_redirect('checkout', ['_fragment' => 'payment']); + } +} diff --git a/Cron/SecondChance.php b/Cron/SecondChance.php new file mode 100644 index 0000000..1620d94 --- /dev/null +++ b/Cron/SecondChance.php @@ -0,0 +1,72 @@ +configProvider = $configProvider; + $this->storeRepository = $storeRepository; + $this->logging = $logging; + $this->secondChanceRepository = $secondChanceRepository; + } + + public function execute() + { + $stores = $this->storeRepository->getList(); + foreach ($stores as $store) { + if ($this->configProvider->isSecondChanceEnabled($store)) { + foreach ([2, 1] as $step) { + $this->secondChanceRepository->getSecondChanceCollection($step, $store); + } + } + } + return $this; + } +} diff --git a/Cron/SecondChancePrune.php b/Cron/SecondChancePrune.php new file mode 100644 index 0000000..15abd8d --- /dev/null +++ b/Cron/SecondChancePrune.php @@ -0,0 +1,60 @@ +storeRepository = $storeRepository; + $this->logging = $logging; + $this->secondChanceRepository = $secondChanceRepository; + } + + public function execute() + { + $stores = $this->storeRepository->getList(); + foreach ($stores as $store) { + $this->secondChanceRepository->deleteOlderRecords($store); + } + return $this; + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e84158 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Buckaroo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Model/Config/Backend/SecondChance.php b/Model/Config/Backend/SecondChance.php new file mode 100644 index 0000000..dc349e5 --- /dev/null +++ b/Model/Config/Backend/SecondChance.php @@ -0,0 +1,55 @@ +getValue(); + $item = $this->toArray(); + $interval = $item['field'] == 'second_chance_timing2' ? 72 : 24; + + if (empty($value)) { + return parent::save(); + } + + if (!is_int($value) || $value < 0 || $value > $interval) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + "Please enter a valid integer within 0 and $interval interval" + ) + ); + } + + $this->setValue($value); + + return parent::save(); + } +} diff --git a/Model/Config/Source/Email/Template.php b/Model/Config/Source/Email/Template.php new file mode 100644 index 0000000..c195558 --- /dev/null +++ b/Model/Config/Source/Email/Template.php @@ -0,0 +1,87 @@ +_coreRegistry = $coreRegistry; + $this->_templatesFactory = $templatesFactory; + $this->_emailConfig = $emailConfig; + } + + /** + * Generate list of email templates + * + * @return array + */ + public function toOptionArray() + { + /** + * @var $collection \Magento\Email\Model\ResourceModel\Template\Collection +*/ + if (!($collection = $this->_coreRegistry->registry('config_system_email_template'))) { + $collection = $this->_templatesFactory->create(); + $collection->load(); + $this->_coreRegistry->register('config_system_email_template', $collection); + } + $options = $collection->toOptionArray(); + $templateId = explode('/', $this->getPath()); + $templateId = end($templateId); + + $templateLabel = $this->_emailConfig->getTemplateLabel($templateId); + $templateLabel = __('%1 (Default)', $templateLabel); + array_unshift($options, ['value' => $templateId, 'label' => $templateLabel]); + array_walk( + $options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); + return $options; + } +} diff --git a/Model/ConfigProvider/SecondChance.php b/Model/ConfigProvider/SecondChance.php new file mode 100644 index 0000000..e5ac1fe --- /dev/null +++ b/Model/ConfigProvider/SecondChance.php @@ -0,0 +1,199 @@ +storeConfig = $storeConfig; + } + + public function isSecondChanceEnabled($store = null): bool + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_ENABLE_SECOND_CHANCE, + ScopeInterface::SCOPE_STORES, + $store + ); + return (bool) $config; + } + + public function getSecondChancePruneDays($store = null): string + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_PRUNE_DAYS, + ScopeInterface::SCOPE_STORES, + $store + ); + return (string) $config; + } + + public function getFinalStatus(): int + { + return static::XML_PATH_SECOND_FINAL_STATUS; + } + + public function isSecondChanceEmail($store = null): bool + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_EMAIL, + ScopeInterface::SCOPE_STORES, + $store + ); + return (bool) $config; + } + + public function isSecondChanceEmail2($store = null): bool + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_EMAIL2, + ScopeInterface::SCOPE_STORES, + $store + ); + return (bool) $config; + } + + public function getSecondChanceTiming($store = null): string + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_TIMING, + ScopeInterface::SCOPE_STORES, + $store + ); + return (string) $config; + } + + public function getSecondChanceTiming2($store = null): string + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_TIMING2, + ScopeInterface::SCOPE_STORES, + $store + ); + return (string) $config; + } + + public function getSecondChanceTemplate($store = null): string + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_TEMPLATE, + ScopeInterface::SCOPE_STORES, + $store + ) ?? self::XML_PATH_SECOND_CHANCE_DEFAULT_TEMPLATE; + return (string) $config; + } + + public function getSecondChanceTemplate2($store = null): string + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_TEMPLATE2, + ScopeInterface::SCOPE_STORES, + $store + ) ?? self::XML_PATH_SECOND_CHANCE_DEFAULT_TEMPLATE2; + return (string) $config; + } + + public function getFromEmail($store = null): string + { + $config = $this->storeConfig->getValue( + 'trans_email/ident_sales/email', + ScopeInterface::SCOPE_STORE, + $store + ); + return (string) $config; + } + + public function getFromName($store = null): string + { + $config = $this->storeConfig->getValue( + 'trans_email/ident_sales/name', + ScopeInterface::SCOPE_STORE, + $store + ); + return (string) $config; + } + + public function getNoSendSecondChance($store = null): bool + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_NO_SEND, + ScopeInterface::SCOPE_STORES, + $store + ); + return (bool) $config; + } + + public function isMultipleEmailsSend($store = null): string + { + $config = $this->storeConfig->getValue( + static::XML_PATH_SECOND_CHANCE_MULTIPLE_EMAILS_SEND, + ScopeInterface::SCOPE_STORES, + $store + ); + return (string) $config; + } +} diff --git a/Model/Data/SecondChance.php b/Model/Data/SecondChance.php new file mode 100644 index 0000000..58e0f60 --- /dev/null +++ b/Model/Data/SecondChance.php @@ -0,0 +1,70 @@ +_get(self::ENTITY_ID); + } + + /** + * Set secondChance_id + * + * @param string $secondChanceId + * @return \Buckaroo\Magento2SecondChance\Api\Data\SecondChanceInterface + */ + public function setSecondChanceId($secondChanceId) + { + return $this->setData(self::ENTITY_ID, $secondChanceId); + } + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Buckaroo\Magento2SecondChance\Api\Data\SecondChanceExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * Set an extension attributes object. + * + * @param \Buckaroo\Magento2SecondChance\Api\Data\SecondChanceExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Buckaroo\Magento2SecondChance\Api\Data\SecondChanceExtensionInterface $extensionAttributes + ) { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/Model/ResourceModel/SecondChance.php b/Model/ResourceModel/SecondChance.php new file mode 100644 index 0000000..d4f06e9 --- /dev/null +++ b/Model/ResourceModel/SecondChance.php @@ -0,0 +1,36 @@ +_init('buckaroo_magento2_second_chance', 'entity_id'); + } +} diff --git a/Model/ResourceModel/SecondChance/Collection.php b/Model/ResourceModel/SecondChance/Collection.php new file mode 100644 index 0000000..7a9e2e7 --- /dev/null +++ b/Model/ResourceModel/SecondChance/Collection.php @@ -0,0 +1,47 @@ +_init( + SecondChance::class, + SecondChanceResource::class + ); + } +} diff --git a/Model/SecondChance.php b/Model/SecondChance.php new file mode 100644 index 0000000..0c93fe3 --- /dev/null +++ b/Model/SecondChance.php @@ -0,0 +1,122 @@ +secondChanceDataFactory = $secondChanceDataFactory; + $this->dataObjectHelper = $dataObjectHelper; + parent::__construct($context, $registry, $resource, $resourceCollection, $data); + } + + /** + * Retrieve secondChance model with secondChance data + * + * @return SecondChanceInterface + */ + public function getDataModel() + { + $secondChanceData = $this->getData(); + + $secondChanceDataObject = $this->secondChanceDataFactory->create(); + $this->dataObjectHelper->populateWithArray( + $secondChanceDataObject, + $secondChanceData, + SecondChanceInterface::class + ); + + return $secondChanceDataObject; + } + + /** + * Get secondChance_id + * + * @return string|null + */ + public function getSecondChanceId() + { + return $this->_get(self::ENTITY_ID); + } + + /** + * Set secondChance_id + * + * @param string $secondChanceId + * @return \Buckaroo\Magento2SecondChance\Api\Data\SecondChanceInterface + */ + public function setSecondChanceId($secondChanceId) + { + return $this->setData(self::ENTITY_ID, $secondChanceId); + } + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Buckaroo\Magento2SecondChance\Api\Data\SecondChanceExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * Set an extension attributes object. + * + * @param \Buckaroo\Magento2SecondChance\Api\Data\SecondChanceExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Buckaroo\Magento2SecondChance\Api\Data\SecondChanceExtensionInterface $extensionAttributes + ) { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/Model/SecondChanceRepository.php b/Model/SecondChanceRepository.php new file mode 100644 index 0000000..afc3921 --- /dev/null +++ b/Model/SecondChanceRepository.php @@ -0,0 +1,656 @@ +resource = $resource; + $this->secondChanceFactory = $secondChanceFactory; + $this->secondChanceCollectionFactory = $secondChanceCollectionFactory; + $this->searchResultsFactory = $searchResultsFactory; + $this->dataObjectHelper = $dataObjectHelper; + $this->dataSecondChanceFactory = $dataSecondChanceFactory; + $this->dataObjectProcessor = $dataObjectProcessor; + $this->storeManager = $storeManager; + $this->collectionProcessor = $collectionProcessor; + $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; + $this->extensibleDataObjectConverter = $extensibleDataObjectConverter; + $this->logging = $logging; + $this->configProvider = $configProvider; + $this->checkoutSession = $checkoutSession; + $this->customerSession = $customerSession; + $this->mathRandom = $mathRandom; + $this->dateTime = $dateTime; + $this->orderFactory = $orderFactory; + $this->customerFactory = $customerFactory; + $this->orderIncrementIdChecker = $orderIncrementIdChecker; + $this->quoteFactory = $quoteFactory; + $this->addressFactory = $addressFactory; + $this->stockRegistry = $stockRegistry; + $this->inlineTranslation = $inlineTranslation; + $this->transportBuilder = $transportBuilder; + $this->addressRenderer = $addressRenderer; + $this->paymentHelper = $paymentHelper; + $this->identityContainer = $identityContainer; + $this->quoteRecreate = $quoteRecreate; + } + + /** + * {@inheritdoc} + */ + public function save( + SecondChanceInterface $secondChance + ) { + + $secondChanceData = $this->extensibleDataObjectConverter->toNestedArray( + $secondChance, + [], + SecondChanceInterface::class + ); + + $secondChanceModel = $this->secondChanceFactory->create()->setData($secondChanceData); + + try { + $this->resource->save($secondChanceModel); + } catch (\Exception $exception) { + throw new CouldNotSaveException( + __( + 'Could not save the secondChance: %1', + $exception->getMessage() + ) + ); + } + return $secondChanceModel->getDataModel(); + } + + /** + * {@inheritdoc} + */ + public function get($secondChanceId) + { + $secondChance = $this->secondChanceFactory->create(); + $this->resource->load($secondChance, $secondChanceId); + if (!$secondChance->getId()) { + throw new NoSuchEntityException(__('SecondChance with id "%1" does not exist.', $secondChanceId)); + } + return $secondChance->getDataModel(); + } + + public function getByOrderId(string $orderId): SecondChanceInterface + { + $this->logging->addDebug(__METHOD__ . '|orderId|' . $orderId); + /** + * @var SecondChanceInterface $secondChanceEntity + */ + $secondChanceEntity = $this->secondChanceFactory->create(); + $this->resource->load($secondChanceEntity, $orderId, SecondChanceInterface::ORDER_ID); + + if (!$secondChanceEntity->getId()) { + throw new NoSuchEntityException(__('SecondChance with orderId "%1" does not exist.', $orderId)); + } + + $this->logging->addDebug(__METHOD__ . '|secondChanceEntity->getId|' . $secondChanceEntity->getId()); + + return $secondChanceEntity; + } + + /** + * {@inheritdoc} + */ + public function getList( + \Magento\Framework\Api\SearchCriteriaInterface $criteria + ) { + $collection = $this->secondChanceCollectionFactory->create(); + + $this->extensionAttributesJoinProcessor->process( + $collection, + SecondChanceInterface::class + ); + + $this->collectionProcessor->process($criteria, $collection); + + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($criteria); + + $items = []; + foreach ($collection as $model) { + $items[] = $model->getDataModel(); + } + + $searchResults->setItems($items); + $searchResults->setTotalCount($collection->getSize()); + return $searchResults; + } + + /** + * {@inheritdoc} + */ + public function delete( + SecondChanceInterface $secondChance + ) { + try { + $secondChanceModel = $this->secondChanceFactory->create(); + $this->resource->load($secondChanceModel, $secondChance->getId()); + $this->resource->delete($secondChanceModel); + } catch (\Exception $exception) { + throw new CouldNotDeleteException( + __( + 'Could not delete the SecondChance: %1', + $exception->getMessage() + ) + ); + } + return true; + } + + /** + * {@inheritdoc} + */ + public function deleteById($secondChanceId) + { + return $this->delete($this->get($secondChanceId)); + } + + /** + * {@inheritdoc} + */ + public function deleteByOrderId($orderId) + { + $this->logging->addDebug(__METHOD__ . '|1|'); + $secondChance = $this->getByOrderId($orderId); + + return $this->delete($secondChance); + } + + /** + * {@inheritdoc} + */ + public function deleteOlderRecords($store) + { + $days = (int) $this->configProvider->getSecondChancePruneDays($store); + + if ($days <= 0) { + return false; + } + + $this->logging->addDebug(__METHOD__ . '|'. $store->getId(). '|$days|' . $days); + + $connection = $this->resource->getConnection(); + try { + $ageCondition = $connection->prepareSqlCondition( + 'created_at', + ['lt' => new \Zend_Db_Expr('NOW() - INTERVAL ? DAY')] + ); + $storeCondition = $connection->prepareSqlCondition('store_id', $store->getId()); + $connection->delete( + $this->resource->getMainTable(), + [$ageCondition => $days, $storeCondition] + ); + } catch (\Exception $exception) { + throw new CouldNotDeleteException(__($exception->getMessage())); + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function createSecondChance($order) + { + $this->logging->addDebug(__METHOD__ . '|1|' . $order->getIncrementId()); + if (!$this->customerSession->getSkipSecondChance()) { + $this->logging->addDebug(__METHOD__ . '|2|'); + $secondChance = $this->secondChanceFactory->create(); + $secondChance->setData( + [ + 'order_id' => $order->getIncrementId(), + 'token' => $this->mathRandom->getUniqueHash(), + 'store_id' => $order->getStoreId(), + 'created_at' => $this->dateTime->gmtDate(), + ] + ); + return $secondChance->save(); + } + $this->logging->addDebug(__METHOD__ . '|3|'); + $this->customerSession->setSkipSecondChance(false); + return false; + } + + /** + * {@inheritdoc} + */ + public function getSecondChanceByToken($token) + { + $secondChance = $this->secondChanceFactory->create(); + $collection = $secondChance->getCollection() + ->addFieldToFilter( + 'token', + ['eq' => $token] + ); + foreach ($collection as $item) { + $order = $this->orderFactory->create()->loadByIncrementId($item->getOrderId()); + + if (!$order->getCustomerId() && $customerEmail = $order->getCustomerEmail()) { + if ($customer = $this->customerFactory->create()->setWebsiteId( + $order->getStoreId() + )->loadByEmail($customerEmail) + ) { + if ($customer->getId()) { + $this->setCustomerAddress($customer, $order); + } + } + } + $this->customerSession->setSecondChanceRecreate($order->getQuoteId()); + $newOrderId = $this->setAvailableIncrementId($item->getOrderId(), $item, $order); + $this->customerSession->setSecondChanceNewIncrementId($newOrderId); + $this->quoteRecreate->recreateById($order->getQuoteId()); + } + } + + private function setAvailableIncrementId($orderId, $item, $order) + { + $this->logging->addDebug(__METHOD__ . '|setAvailableIncrementId|' . $orderId); + for ($i = 1; $i < 100; $i++) { + $newOrderId = $orderId . '-' . $i; + if (!$this->orderIncrementIdChecker->isIncrementIdUsed($newOrderId)) { + $this->logging->addDebug(__METHOD__ . '|setReservedOrderId|' . $newOrderId); + $this->checkoutSession->getQuote()->setReservedOrderId($newOrderId); + $this->checkoutSession->getQuote()->save(); + + $quote = $this->quoteFactory->create()->load($order->getQuoteId()); + $quote->setReservedOrderId($newOrderId)->save(); + + $this->customerSession->setSkipSecondChance($newOrderId); + + $item->setLastOrderId($newOrderId); + $item->save(); + return $newOrderId; + } + } + } + + public function getSecondChanceCollection($step, $store) + { + $final_status = $this->configProvider->getFinalStatus(); + + if ($step == 2) { + if (!$this->configProvider->isSecondChanceEmail2($store)) { + return false; + } + } else { + if (!$this->configProvider->isSecondChanceEmail($store)) { + return false; + } + } + + $timing = $this->configProvider->getSecondChanceTiming($store) + + ($step == 2 ? $this->configProvider->getSecondChanceTiming2($store) : 0); + + $this->logging->addDebug(__METHOD__ . '|secondChance timing|' . $timing); + + $secondChance = $this->secondChanceFactory->create(); + $collection = $secondChance->getCollection() + ->addFieldToFilter( + 'status', + ['eq' => ($step == 2 && $this->configProvider->isSecondChanceEmail($store)) ? 1 : ''] + ) + ->addFieldToFilter( + 'store_id', + ['eq' => $store->getId()] + ) + ->addFieldToFilter('created_at', ['lteq' => new \Zend_Db_Expr('NOW() - INTERVAL ' . $timing . ' HOUR')]) + ->addFieldToFilter('created_at', ['gteq' => new \Zend_Db_Expr('NOW() - INTERVAL 5 DAY')]) + ->setOrder('created_at', 'DESC'); + + $flag = $this->dateTime->gmtDate(); + + foreach ($collection as $item) { + $order = $this->orderFactory->create()->loadByIncrementId($item->getOrderId()); + + if (!$this->configProvider->isMultipleEmailsSend($store)) { + if ($this->checkForMultipleEmail($order, $flag)) { + $this->setFinalStatus($item, $final_status); + continue; + } + } + + //BP-896 skip Transfer method + $payment = $order->getPayment(); + if (in_array($payment->getMethod(), [Transfer::PAYMENT_METHOD_CODE, PayPerEmail::PAYMENT_METHOD_CODE])) { + $this->setFinalStatus($item, $final_status); + continue; + } + + if ($item->getLastOrderId() != null + && $last_order = $this->orderFactory->create()->loadByIncrementId($item->getLastOrderId()) + ) { + if ($last_order->hasInvoices()) { + $this->setFinalStatus($item, $final_status); + continue; + } + } + + if ($order->hasInvoices()) { + $this->setFinalStatus($item, $final_status); + } else { + if ($this->configProvider->getNoSendSecondChance($store)) { + $this->logging->addDebug(__METHOD__ . '|getNoSendSecondChance|'); + if ($this->checkOrderProductsIsInStock($order)) { + $this->logging->addDebug(__METHOD__ . '|checkOrderProductsIsInStock|'); + $this->sendMail($order, $item, $step); + } + } else { + $this->logging->addDebug(__METHOD__ . '|else getNoSendSecondChance|'); + $this->sendMail($order, $item, $step); + } + } + } + } + + public function sendMail($order, $secondChance, $step) + { + $this->logging->addDebug(__METHOD__ . '|sendMail start|'); + + $store = $order->getStore(); + $vars = [ + 'order' => $order, + 'billing' => $order->getBillingAddress(), + 'payment_html' => $this->getPaymentHtml($order), + 'store' => $store, + 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'secondChanceToken' => $secondChance->getToken(), + ]; + + $templateId = ($step == 1) ? + $this->configProvider->getSecondChanceTemplate($store) : + $this->configProvider->getSecondChanceTemplate2($store); + + $this->logging->addDebug(__METHOD__ . '|TemplateIdentifier|' . $templateId); + + $this->inlineTranslation->suspend(); + $this->transportBuilder->setTemplateIdentifier($templateId) + ->setTemplateOptions( + [ + 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, + 'store' => $store->getId(), + ] + )->setTemplateVars($vars) + ->setFrom( + [ + 'email' => $this->configProvider->getFromEmail($store), + 'name' => $this->configProvider->getFromName($store), + ] + )->addTo($order->getCustomerEmail()); + + if (!isset($transport)) { + $transport = $this->transportBuilder->getTransport(); + } + + try { + $transport->sendMessage(); + $this->inlineTranslation->resume(); + $secondChance->setStatus($step); + $secondChance->save(); + $this->logging->addDebug(__METHOD__ . '|secondChanceEmail is sended to|' . $order->getCustomerEmail()); + } catch (\Exception $exception) { + $this->logging->addDebug(__METHOD__ . '|log failed email send|' . $exception->getMessage()); + } + } + + /** + * Render shipping address into html. + * + * @param Order $order + * @return string|null + */ + protected function getFormattedShippingAddress($order) + { + return $order->getIsVirtual() + ? null + : $this->addressRenderer->format($order->getShippingAddress(), 'html'); + } + + /** + * Render billing address into html. + * + * @param Order $order + * @return string|null + */ + protected function getFormattedBillingAddress($order) + { + return $this->addressRenderer->format($order->getBillingAddress(), 'html'); + } + + /** + * Returns payment info block as HTML. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * + * @return string + * @throws \Exception + */ + private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) + { + return $this->paymentHelper->getInfoBlockHtml( + $order->getPayment(), + $this->identityContainer->getStore()->getStoreId() + ); + } + + private function checkOrderProductsIsInStock($order) + { + if ($allItems = $order->getAllItems()) { + foreach ($allItems as $orderItem) { + $product = $orderItem->getProduct(); + if ($sku = $product->getData('sku')) { + $stock = $this->stockRegistry->getStockItemBySku($sku); + + if ($orderItem->getProductType() == Type::TYPE_SIMPLE) { + //check is in stock flag and if there is enough qty + if ((!$stock->getIsInStock()) + || ((int) ($orderItem->getQtyOrdered()) > (int) ($stock->getQty())) + ) { + $this->logging->addDebug( + __METHOD__ . '|not getIsInStock|' . $orderItem->getProduct()->getId() + ); + return false; + } + } else { + //other product types - bundle / configurable, etc, check only flag + if (!$stock->getIsInStock()) { + $this->logging->addDebug( + __METHOD__ . '|not getIsInStock|' . $orderItem->getProduct()->getSku() + ); + return false; + } + } + + } + } + } + return true; + } + + private function setFinalStatus($item, $status) + { + $item->setStatus($status); + return $item->save(); + } + + private function setCustomerAddress($customer, $order) + { + $address = $this->addressFactory->create(); + $address->setData($order->getBillingAddress()->getData()); + $customerId = $customer->getId(); + + $address->setCustomerId($customerId) + ->setIsDefaultBilling('1') + ->setIsDefaultShipping('0') + ->setSaveInAddressBook('1'); + $address->save(); + + if (!$order->getIsVirtual()) { + $address = $this->addressFactory->create(); + $address->setData($order->getShippingAddress()->getData()); + + $address->setCustomerId($customerId) + ->setIsDefaultBilling('0') + ->setIsDefaultShipping('1') + ->setSaveInAddressBook('1'); + $address->save(); + } + } + + public function checkForMultipleEmail($order, $flag) + { + $multipleEmail = $this->checkoutSession->getMultipleEmail(); + if (!empty($multipleEmail[$flag][$order->getCustomerEmail()])) { + return true; + } + $multipleEmail[$flag][$order->getCustomerEmail()] = 1; + $this->checkoutSession->setMultipleEmail($multipleEmail); + return false; + } +} diff --git a/Observer/ProcessHandleFailed.php b/Observer/ProcessHandleFailed.php new file mode 100644 index 0000000..217d3ee --- /dev/null +++ b/Observer/ProcessHandleFailed.php @@ -0,0 +1,72 @@ +logging = $logging; + $this->configProvider = $configProvider; + $this->quoteRecreate = $quoteRecreate; + $this->customerSession = $customerSession; + $this->checkoutSession = $checkoutSession; + } + + /** + * @param \Magento\Framework\Event\Observer $observer + * + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $this->logging->addDebug(__METHOD__ . '|1|'); + /* @var $order \Magento\Sales\Model\Order */ + $order = $observer->getEvent()->getOrder(); + + if (!$order) { + $this->logging->addDebug(__METHOD__ . '|no observer order|'); + $order = $this->checkoutSession->getLastRealOrder(); + } + + if ($order && $this->configProvider->isSecondChanceEnabled($order->getStore())) { + $this->quoteRecreate->duplicate($order); + $this->customerSession->setSkipHandleFailedRecreate(1); + } + } +} diff --git a/Observer/ProcessRedirectSuccess.php b/Observer/ProcessRedirectSuccess.php new file mode 100644 index 0000000..4a1e492 --- /dev/null +++ b/Observer/ProcessRedirectSuccess.php @@ -0,0 +1,56 @@ +logging = $logging; + $this->configProvider = $configProvider; + $this->customerSession = $customerSession; + } + + /** + * @param \Magento\Framework\Event\Observer $observer + * + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + /* @var $order \Magento\Sales\Model\Order */ + $order = $observer->getEvent()->getOrder(); + if ($order && $this->configProvider->isSecondChanceEnabled($order->getStore())) { + $this->customerSession->setSkipSecondChance(false); + } + } +} diff --git a/Observer/SecondChance.php b/Observer/SecondChance.php new file mode 100644 index 0000000..8ebe643 --- /dev/null +++ b/Observer/SecondChance.php @@ -0,0 +1,69 @@ +secondChanceRepository = $secondChanceRepository; + $this->logging = $logging; + $this->configProvider = $configProvider; + } + + /** + * @param \Magento\Framework\Event\Observer $observer + * + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $this->logging->addDebug(__METHOD__ . '|1|'); + /* @var $order \Magento\Sales\Model\Order */ + $order = $observer->getEvent()->getOrder(); + // $order = $observer->getData('order'); + if ($order && $this->configProvider->isSecondChanceEnabled($order->getStore())) { + $this->logging->addDebug(__METHOD__ . '|2|'); + try { + $this->secondChanceRepository->createSecondChance($order); + } catch (\Exception $e) { + $this->logging->addError('Could not create SC:' . $order->getIncrementId() . $e->getMessage()); + } + } + $this->logging->addDebug(__METHOD__ . '|3|'); + } +} diff --git a/Observer/SecondChanceRestoreQuote.php b/Observer/SecondChanceRestoreQuote.php new file mode 100644 index 0000000..316f8b8 --- /dev/null +++ b/Observer/SecondChanceRestoreQuote.php @@ -0,0 +1,60 @@ +configProvider = $configProvider; + $this->customerSession = $customerSession; + $this->logging = $logging; + } + + /** + * @param \Magento\Framework\Event\Observer $observer + * + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + if ($quoteId = $this->customerSession->getSecondChanceRecreate()) { + try { + $this->customerSession->setSecondChanceRecreate(false); + } catch (\Exception $e) { + $this->logging->addError('Could not recreateById SC:' . $quoteId); + } + } + } +} diff --git a/Observer/SecondChanceSuccessOrder.php b/Observer/SecondChanceSuccessOrder.php new file mode 100644 index 0000000..3a6fdc4 --- /dev/null +++ b/Observer/SecondChanceSuccessOrder.php @@ -0,0 +1,66 @@ +secondChanceRepository = $secondChanceRepository; + $this->logging = $logging; + $this->configProvider = $configProvider; + } + + /** + * @param \Magento\Framework\Event\Observer $observer + * + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $this->logging->addDebug(__METHOD__ . '|1|'); + /* @var $order \Magento\Sales\Model\Order */ + $order = $observer->getEvent()->getOrder(); + if ($order && $this->configProvider->isSecondChanceEnabled($order->getStore())) { + try { + $this->secondChanceRepository->deleteByOrderId($order->getIncrementId()); + } catch (\Exception $e) { + $this->logging->addError('Could not find SC by order id:' . $order->getIncrementId()); + } + } + } +} diff --git a/Plugin/SecondChance.php b/Plugin/SecondChance.php new file mode 100644 index 0000000..97f416b --- /dev/null +++ b/Plugin/SecondChance.php @@ -0,0 +1,57 @@ +customerSession = $customerSession; + $this->logger = $logger; + $this->configProvider = $configProvider; + } + + public function aroundShouldSkipFurtherEventHandling() + { + return $this->customerSession->getSecondChanceRecreate(); + } + + public function aroundIsNeedRecreate(\Buckaroo\Magento2\Plugin\ShippingMethodManagement $subject, $proceed, $store) + { + return $this->configProvider->isSecondChanceEnabled($store); + } + + public function aroundGetSkipHandleFailedRecreate() + { + return $this->customerSession->getSkipHandleFailedRecreate(); + } + + public function aroundSetSkipHandleFailedRecreate($value) + { + return $this->customerSession->setSkipHandleFailedRecreate($value); + } +} diff --git a/README.md b/README.md index a4ecd41..e92e213 100644 --- a/README.md +++ b/README.md @@ -1 +1,45 @@ -# Magento2_SecondChance +
+ +
+ +# Buckaroo Magento 2 Second Chance extension + +## Installation +``` +composer require buckaroo/magento2secondchance +php bin/magento module:enable Buckaroo_Magento2SecondChance +php bin/magento setup:upgrade +php bin/magento setup:static-content:deploy +``` + +## Usage +### General information +The Second Chance module makes it possible to follow up unpaid orders with one or two reminder emails. This extension to the Buckaroo Payment module ensures a higher conversion rate.The Second Chance functionality is fully white-labelled, e-mails can be sent from your own corporate identity and mail servers. On top of that, the module can optionally also take into account whether or not stock is available. + +### Requirements +To use the plugin you must use: +- Magento Open Source version 2.3.x & 2.4.x +- Buckaroo Magento 2 Payment module 1.39.0 or greater + +### Configuration +In the module configuration, various settings are available to build an ideal Second Chance flow to suit everyone. The settings below can be adjusted manually. +* Switching on and off 1st and 2nd email. +* Select template for sending 1st and 2nd email. +* Determine timing for sending 1st and 2nd email. +* Don't send payment reminder when product is out of stock (on/off) +* Block multiple emails (on/off) + ++ +
+ +### Additional information +For more information on Second Chance please visit: +https://support.buckaroo.nl/categorieen/plugins/magento-2/magento-2-second-chance-extensie + +## Contribute +See [Contribution Guidelines](CONTRIBUTING.md) + +## Support: + +https://support.buckaroo.nl/contact diff --git a/Service/Sales/Quote/Recreate.php b/Service/Sales/Quote/Recreate.php new file mode 100644 index 0000000..dde9de3 --- /dev/null +++ b/Service/Sales/Quote/Recreate.php @@ -0,0 +1,233 @@ +cartRepository = $cartRepository; + $this->cart = $cart; + $this->checkoutSession = $checkoutSession; + $this->customerSession = $customerSession; + $this->quoteFactory = $quoteFactory; + $this->productFactory = $productFactory; + $this->cart = $cart; + $this->quoteRepository = $quoteRepository; + $this->messageManager = $messageManager; + $this->quoteManagement = $quoteManagement; + $this->quoteAddressResource = $quoteAddressResource; + $this->logger = $logger; + } + + /** + * @param Order $order + * + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function recreate($quote) + { + $this->logger->addDebug(__METHOD__ . '|1|'); + // @codingStandardsIgnoreStart + try { + if ($newIncrementId = $quote->getReservedOrderId()) { + $this->logger->addDebug(__METHOD__ . '|5|' . $newIncrementId); + } + $quote->setIsActive(true); + $quote->setTriggerRecollect('1'); + $quote->setReservedOrderId(null); + $quote->setBuckarooFee(null); + $quote->setBaseBuckarooFee(null); + $quote->setBuckarooFeeTaxAmount(null); + $quote->setBuckarooFeeBaseTaxAmount(null); + $quote->setBuckarooFeeInclTax(null); + $quote->setBaseBuckarooFeeInclTax(null); + $quote->save(); + $this->checkoutSession->replaceQuote($quote); + if ($newIncrementId) { + $quote->setReservedOrderId($newIncrementId); + } + $this->cart->setQuote($quote); + $this->cart->save(); + return $quote; + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //No such entity + $this->logger->addError($e->getMessage()); + } + // @codingStandardsIgnoreEnd + return false; + } + + public function recreateById($quoteId) + { + $this->logger->addDebug(__METHOD__ . '|1|' . $quoteId); + try { + $oldQuote = $this->quoteFactory->create()->load($quoteId); + } catch (\Exception $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } + + if ($oldQuote->getId()) { + $this->logger->addDebug(__METHOD__ . '|5|'); + try { + $quote = $this->quoteFactory->create(); + $quote->merge($oldQuote)->save(); + } catch (\Exception $e) { + $this->logger->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); + } + + $quote->setStoreId($oldQuote->getStoreId()); + $quote->getPayment()->setMethod($oldQuote->getPayment()->getMethod()); + $this->cart->setStoreId($oldQuote->getStoreId()); + $this->checkoutSession->setQuoteId($quote->getId()); + + if ($newIncrementId = $this->customerSession->getSecondChanceNewIncrementId()) { + $this->logger->addDebug(__METHOD__ . '|15|' . $newIncrementId); + $this->customerSession->setSecondChanceNewIncrementId(false); + $this->checkoutSession->getQuote()->setReservedOrderId($newIncrementId); + $this->checkoutSession->getQuote()->save(); + $quote->setReservedOrderId($newIncrementId)->save(); + } + + if ($email = $oldQuote->getBillingAddress()->getEmail()) { + $quote->setCustomerEmail($email); + } + + $quote->setCustomerIsGuest(true); + if ($customer = $this->customerSession->getCustomer()) { + $quote->setCustomerId($customer->getId()); + $quote->setCustomerGroupId($customer->getGroupId()); + $quote->setCustomerIsGuest(false); + } + + $this->logger->addDebug(__METHOD__ . '|30|'); + $quote = $this->recreate($quote); + + return $this->additionalMerge($oldQuote, $quote); + + } + } + + public function duplicate($order) + { + $quote = $this->quoteFactory->create(); + try { + $oldQuote = $this->quoteFactory->create()->load($order->getQuoteId()); + $quote->merge($oldQuote)->save(); + $oldQuote->setIsActive(true); + $oldQuote->save(); + } catch (\Exception $e) { + $this->logger->addError($e->getMessage()); + } + + $quote = $this->recreate($quote); + + return $this->additionalMerge($oldQuote, $quote); + } + + private function additionalMerge($oldQuote, $quote) + { + if (!$oldQuote->getCustomerIsGuest() && $oldQuote->getCustomerId()) { + $quote->setCustomerId($oldQuote->getCustomerId()); + } + + $quote->setCustomerEmail($oldQuote->getBillingAddress()->getEmail()); + $quote->setCustomerIsGuest($oldQuote->getCustomerIsGuest()); + + if ($customer = $this->customerSession->getCustomer()) { + $quote->setCustomerId($customer->getId()); + $quote->setCustomerEmail($customer->getEmail()); + $quote->setCustomerFirstname($customer->getFirstname()); + $quote->setCustomerLastname($customer->getLastname()); + $quote->setCustomerGroupId($customer->getGroupId()); + $quote->setCustomerIsGuest(false); + } + $quote->setBillingAddress( + $oldQuote->getBillingAddress()->setQuote($quote)->setId( + $quote->getBillingAddress()->getId() + ) + ); + $quote->setShippingAddress( + $oldQuote->getShippingAddress()->setQuote($quote)->setId( + $quote->getShippingAddress()->getId() + ) + ); + $quote->getShippingAddress()->setShippingMethod($oldQuote->getShippingAddress()->getShippingMethod()); + $this->quoteAddressResource->save($quote->getBillingAddress()); + $this->quoteAddressResource->save($quote->getShippingAddress()); + + $this->cart->setQuote($quote); + + try { + $this->cart->save(); + } catch (\Exception $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } + + return $quote; + } +} diff --git a/Setup/UpgradeData.php b/Setup/UpgradeData.php new file mode 100644 index 0000000..72fe1eb --- /dev/null +++ b/Setup/UpgradeData.php @@ -0,0 +1,52 @@ +startSetup(); + + $this->updateSecondChanceEmailTemplates($setup); + + $setup->endSetup(); + } + + protected function updateSecondChanceEmailTemplates(ModuleDataSetupInterface $setup) + { + $setup->getConnection()->update( + $setup->getTable('email_template'), + ['is_legacy' => 1], + $setup->getConnection()->quoteInto( + 'orig_template_code IN(?) ', + ['buckaroo_second_chance','buckaroo_second_chance2'] + ) + ); + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100644 index 0000000..3568d13 --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,129 @@ + + +
+ {{trans "%customer_name," customer_name=$order.getCustomerName()}} ++ {{trans 'You can finish your order from %store_name.' store_name=$store.getFrontendName() finish_url=$this.getUrl($store,'buckaroo_second_chance/checkout/secondchance/',[_query:[token:$secondChanceToken]]) |raw}} + ++ {{trans 'If you have questions about your order, you can email us at %store_email' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at %store_phone' store_phone=$store_phone |raw}}{{/depend}}. + {{depend store_hours}} + {{trans 'Our hours are %store_hours.' store_hours=$store_hours |raw}} + {{/depend}} + + |
+ ||||
+ {{trans 'Your Order #%increment_id' increment_id=$order.increment_id |raw}}+{{trans 'Placed on %created_at' created_at=$order.getCreatedAtFormatted(2) |raw}} + |
+ ||||
+ {{depend order.getEmailCustomerNote()}}
+
|
+
+ {{/depend}}
+
+ {{trans "%customer_name," customer_name=$order.getCustomerName()}} ++ {{trans 'You can finish your order from %store_name.' store_name=$store.getFrontendName() finish_url=$this.getUrl($store,'buckaroo_second_chance/checkout/secondchance/',[_query:[token:$secondChanceToken]]) |raw}} + ++ {{trans 'If you have questions about your order, you can email us at %store_email' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at %store_phone' store_phone=$store_phone |raw}}{{/depend}}. + {{depend store_hours}} + {{trans 'Our hours are %store_hours.' store_hours=$store_hours |raw}} + {{/depend}} + + |
+ ||||
+ {{trans 'Your Order #%increment_id' increment_id=$order.increment_id |raw}}+{{trans 'Placed on %created_at' created_at=$order.getCreatedAtFormatted(2) |raw}} + |
+ ||||
+ {{depend order.getEmailCustomerNote()}}
+
|
+
+ {{/depend}}
+