From c453bc9b967464919e3a65b7986aa74b111dd0a1 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 25 Aug 2025 14:09:49 +0200 Subject: [PATCH 1/7] Bugfix: Return the onetimepurchase option when relevant --- Service/Mollie/GetSelectedOption.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Service/Mollie/GetSelectedOption.php b/Service/Mollie/GetSelectedOption.php index 2ef6b1f..111a5d2 100644 --- a/Service/Mollie/GetSelectedOption.php +++ b/Service/Mollie/GetSelectedOption.php @@ -6,6 +6,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Mollie\Subscriptions\DTO\ProductSubscriptionOption; +use Mollie\Subscriptions\DTO\ProductSubscriptionOptionFactory; class GetSelectedOption { @@ -13,15 +14,31 @@ class GetSelectedOption * @var ParseSubscriptionOptions */ private $parseSubscriptionOptions; + /** + * @var ProductSubscriptionOptionFactory + */ + private $productSubscriptionOptionFactory; public function __construct( - ParseSubscriptionOptions $parseSubscriptionOptions + ParseSubscriptionOptions $parseSubscriptionOptions, + ProductSubscriptionOptionFactory $productSubscriptionOptionFactory ) { $this->parseSubscriptionOptions = $parseSubscriptionOptions; + $this->productSubscriptionOptionFactory = $productSubscriptionOptionFactory; } public function execute(ProductInterface $product, string $optionId): ProductSubscriptionOption { + if ($optionId == 'onetimepurchase') { + return $this->productSubscriptionOptionFactory->create([ + 'identifier' => 'onetimepurchase', + 'title' => __('One Time Purchase'), + 'interval_amount' => '', + 'interval_type' => '', + 'repetition_type' => 'onetime', + ]); + } + $options = $this->parseSubscriptionOptions->execute($product); foreach ($options as $option) { if ($option->getIdentifier() === $optionId) { From 17ddb1ea0ef157974f25fafea584a465266bca43 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Thu, 10 Jul 2025 10:21:42 +0200 Subject: [PATCH 2/7] Feature: Show price in add to cart button --- .../Product/View/SubscriptionOptions.php | 29 +++++++++++++++++++ Config.php | 6 ++++ .../ChangeAddToCartText.php | 9 +++--- Service/Mollie/ParseSubscriptionOptions.php | 4 +++ etc/adminhtml/system/general.xml | 6 ++++ .../product/view/subscription-options.phtml | 12 +++++++- 6 files changed, 60 insertions(+), 6 deletions(-) diff --git a/Block/Frontend/Product/View/SubscriptionOptions.php b/Block/Frontend/Product/View/SubscriptionOptions.php index c7893cb..b59d36a 100644 --- a/Block/Frontend/Product/View/SubscriptionOptions.php +++ b/Block/Frontend/Product/View/SubscriptionOptions.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Attribute\Source\Boolean; +use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Registry; use Magento\Framework\View\Element\Template; use Mollie\Subscriptions\Config; @@ -32,12 +33,17 @@ class SubscriptionOptions extends Template * @var ParseSubscriptionOptions */ private $parseSubscriptionOptions; + /** + * @var PriceCurrencyInterface + */ + private $priceCurrency; public function __construct( Template\Context $context, Registry $registry, Config $config, ParseSubscriptionOptions $parseSubscriptionOptions, + PriceCurrencyInterface $priceCurrency, array $data = [] ) { parent::__construct($context, $data); @@ -45,6 +51,7 @@ public function __construct( $this->registry = $registry; $this->config = $config; $this->parseSubscriptionOptions = $parseSubscriptionOptions; + $this->priceCurrency = $priceCurrency; } /** @@ -71,4 +78,26 @@ public function allowOneTimePurchase(): bool return (bool)$value; } + + public function showPriceInSubscriptionButton(): bool + { + return $this->config->showPriceInSubscriptionButton(); + } + + public function getProductPrice(): float + { + /** @var ProductInterface $product */ + $product = $this->registry->registry('current_product'); + + if ($product->getPrice() === null) { + return 0.0; + } + + return (float)$product->getPrice(); + } + + public function formatPrice(float $price, bool $includeContainer = false): string + { + return $this->priceCurrency->format($price, $includeContainer); + } } diff --git a/Config.php b/Config.php index 1706e71..5b4031a 100755 --- a/Config.php +++ b/Config.php @@ -47,6 +47,7 @@ class Config const XML_PATH_EMAILS_CUSTOMER_CANCEL_NOTIFICATION_TEMPLATE = 'mollie_subscriptions/emails/customer_cancel_notification_template'; const XML_PATH_DISABLE_NEW_ORDER_CONFIRMATION = 'mollie_subscriptions/emails/disable_new_order_confirmation'; const XML_PATH_ALLOW_ONE_TIME_PURCHASE = 'mollie_subscriptions/general/allow_one_time_purchases'; + const XML_PATH_SHOW_PRICE_IN_SUBSCRIPTION_BUTTON = 'mollie_subscriptions/general/show_price_in_subscription_button'; const MODULE_SUPPORT_LINK = 'https://www.magmodules.eu/help/%s'; /** @@ -269,6 +270,11 @@ public function allowOneTimePurchase($storeId = null, $scope = ScopeInterface::S return $this->getFlag(static::XML_PATH_ALLOW_ONE_TIME_PURCHASE, $storeId, $scope); } + public function showPriceInSubscriptionButton(?int $storeId = null, string $scope = ScopeInterface::SCOPE_STORE): bool + { + return $this->getFlag(static::XML_PATH_SHOW_PRICE_IN_SUBSCRIPTION_BUTTON, $storeId, $scope); + } + /** * @param int $storeId * @param string $scope diff --git a/Observer/ViewBlockAbstractToHtmlAfter/ChangeAddToCartText.php b/Observer/ViewBlockAbstractToHtmlAfter/ChangeAddToCartText.php index 79e4257..3000283 100644 --- a/Observer/ViewBlockAbstractToHtmlAfter/ChangeAddToCartText.php +++ b/Observer/ViewBlockAbstractToHtmlAfter/ChangeAddToCartText.php @@ -59,11 +59,13 @@ public function execute(Observer $observer) $html = $transport->getData('html'); $document = $this->domDocumentFactory->create(); + $document->encoding = 'UTF-8'; $document->preserveWhiteSpace = false; $document->formatOutput = true; try { libxml_use_internal_errors(true); + $html = mb_convert_encoding($html, 'UTF-8', mb_detect_encoding($html)); $load = $document->loadHTML($html, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED); libxml_clear_errors(); @@ -88,12 +90,9 @@ public function execute(Observer $observer) } $subscriptionOptionsBlock = $block->getLayout()->createBlock(SubscriptionOptions::class)->toHtml(); - $subscriptionOptionsBlock = str_replace('@', 'at-----', $subscriptionOptionsBlock); $newHtml = $this->domDocumentFactory->create(); - $newHtml->preserveWhiteSpace = false; - $newHtml->formatOutput = false; - $newHtml->loadHTML($subscriptionOptionsBlock, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED); + $newHtml->loadHTML(''); // Import our HTML before the regular "add to cart button". $imported = $document->importNode($newHtml->documentElement, true); @@ -103,7 +102,7 @@ public function execute(Observer $observer) $button->parentNode->removeChild($button); $saveHTML = $document->saveHTML(); - $saveHTML = str_replace('at-----', '@', $saveHTML); + $saveHTML = str_replace('', $subscriptionOptionsBlock, $saveHTML); $transport->setData('html', $saveHTML); } diff --git a/Service/Mollie/ParseSubscriptionOptions.php b/Service/Mollie/ParseSubscriptionOptions.php index c58acd3..02d7f4a 100644 --- a/Service/Mollie/ParseSubscriptionOptions.php +++ b/Service/Mollie/ParseSubscriptionOptions.php @@ -45,6 +45,10 @@ public function execute(ProductInterface $product): array $json = $this->serializer->unserialize($table); return array_map( function ($option) { + if (array_key_exists('price', $option) && !$option['price']) { + $option['price'] = 0; + } + return $this->productSubscriptionOptionFactory->create($option); }, $json); } diff --git a/etc/adminhtml/system/general.xml b/etc/adminhtml/system/general.xml index 3e031f3..7b590af 100755 --- a/etc/adminhtml/system/general.xml +++ b/etc/adminhtml/system/general.xml @@ -34,5 +34,11 @@ mollie_subscriptions/general/allow_one_time_purchases Note: This can be changed on a per-product level + + + Magento\Config\Model\Config\Source\Yesno + mollie_subscriptions/general/show_price_in_subscription_button + Renders the subscription interval price in the purchase button. Applies to one-time purchase as well when active. + diff --git a/view/frontend/templates/product/view/subscription-options.phtml b/view/frontend/templates/product/view/subscription-options.phtml index f437c13..340b9e4 100644 --- a/view/frontend/templates/product/view/subscription-options.phtml +++ b/view/frontend/templates/product/view/subscription-options.phtml @@ -21,7 +21,13 @@ class="action primary tocart product-addtocart-button select-subscription" value="onetimepurchase" title="" - > + > + + + showPriceInSubscriptionButton()): ?> + formatPrice($block->getProductPrice()) ?> + + getOptions() as $option): ?> @@ -33,6 +39,10 @@ title="escapeHtmlAttr($option->getTitle()) ?>" > escapeHtml($option->getTitle()); ?> + + showPriceInSubscriptionButton()): ?> + formatPrice($option->getPrice()) ?> + From f165f13373f3f358bd996e741997a148c793bfce Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Thu, 28 Aug 2025 13:05:03 +0200 Subject: [PATCH 3/7] Bugfix: Update the product price depending on the selected option --- Api/Data/SubscriptionToProductInterface.php | 14 ++ Block/Adminhtml/UpdatePrice/BackButton.php | 39 +++++ Block/Adminhtml/UpdatePrice/SaveButton.php | 26 +++ Config.php | 23 ++- .../Adminhtml/Subscriptions/SavePrice.php | 108 ++++++++++++ .../Adminhtml/Subscriptions/Transactions.php | 34 ++++ .../Adminhtml/Subscriptions/UpdatePrice.php | 50 ++++++ Controller/Index/Restart.php | 4 + Cron/UpdateSubscriptionsWithAPriceUpdate.php | 37 ++++- DTO/SubscriptionOption.php | 13 +- Model/Data/SubscriptionToProduct.php | 19 +++ Model/MollieSubscriptionsListing.php | 51 +++--- Model/MollieSubscriptionsTransactions.php | 155 ++++++++++++++++++ Model/UpdatePrice/DataProvider.php | 101 ++++++++++++ .../UpdateSubscriptionProduct.php | 45 ++++- .../CreateSubscriptions.php | 1 + .../Magento/CreateOrderFromSubscription.php | 6 +- .../Magento/GetPriceUpdateForSubscription.php | 73 +++++++++ .../Magento/SubscriptionAddProductToCart.php | 19 ++- Service/Mollie/ParseSubscriptionOptions.php | 30 +++- Service/Mollie/SubscriptionOptions.php | 19 ++- Service/Mollie/UpdateNextPaymentDate.php | 4 + Setup/Patch/Data/AddOptionIdToMetadata.php | 102 ++++++++++++ .../Controller/Api/WebhookTest.php | 10 +- .../CreateOrderFromSubscriptionTest.php | 16 +- .../Mollie/SubscriptionOptionsTest.php | 28 ++++ Ui/Component/Listing/Column/Actions.php | 54 ++++-- etc/adminhtml/system/general.xml | 18 +- etc/config.xml | 9 +- etc/db_schema.xml | 3 +- ...bscriptions_subscriptions_transactions.xml | 13 ++ ...ubscriptions_subscriptions_updateprice.xml | 15 ++ ...iptions_subscription_update_price_form.xml | 79 +++++++++ ...bscriptions_subscriptions_transactions.xml | 113 +++++++++++++ 34 files changed, 1250 insertions(+), 81 deletions(-) create mode 100644 Block/Adminhtml/UpdatePrice/BackButton.php create mode 100644 Block/Adminhtml/UpdatePrice/SaveButton.php create mode 100644 Controller/Adminhtml/Subscriptions/SavePrice.php create mode 100644 Controller/Adminhtml/Subscriptions/Transactions.php create mode 100644 Controller/Adminhtml/Subscriptions/UpdatePrice.php create mode 100644 Model/MollieSubscriptionsTransactions.php create mode 100644 Model/UpdatePrice/DataProvider.php create mode 100644 Service/Magento/GetPriceUpdateForSubscription.php create mode 100644 Setup/Patch/Data/AddOptionIdToMetadata.php create mode 100644 view/adminhtml/layout/mollie_subscriptions_subscriptions_transactions.xml create mode 100644 view/adminhtml/layout/mollie_subscriptions_subscriptions_updateprice.xml create mode 100644 view/adminhtml/ui_component/mollie_subscriptions_subscription_update_price_form.xml create mode 100644 view/adminhtml/ui_component/mollie_subscriptions_subscriptions_transactions.xml diff --git a/Api/Data/SubscriptionToProductInterface.php b/Api/Data/SubscriptionToProductInterface.php index 99383ab..3a55e79 100644 --- a/Api/Data/SubscriptionToProductInterface.php +++ b/Api/Data/SubscriptionToProductInterface.php @@ -15,6 +15,7 @@ interface SubscriptionToProductInterface extends ExtensibleDataInterface const CUSTOMER_ID = 'customer_id'; const SUBSCRIPTION_ID = 'subscription_id'; const PRODUCT_ID = 'product_id'; + const OPTION_ID = 'option_id'; const STORE_ID = 'store_id'; const HAS_PRICE_UPDATE = 'has_price_update'; const NEXT_PAYMENT_DATE = 'next_payment_date'; @@ -72,6 +73,19 @@ public function getProductId(): int; */ public function setProductId(int $productId); + /** + * Get option_id + * @return string|null + */ + public function getOptionId(): ?string; + + /** + * Set option_id + * @param string $optionId + * @return \Mollie\Subscriptions\Api\Data\SubscriptionToProductInterface + */ + public function setOptionId(string $optionId); + /** * Get store_id * @return int|null diff --git a/Block/Adminhtml/UpdatePrice/BackButton.php b/Block/Adminhtml/UpdatePrice/BackButton.php new file mode 100644 index 0000000..e22044e --- /dev/null +++ b/Block/Adminhtml/UpdatePrice/BackButton.php @@ -0,0 +1,39 @@ +context = $context; + } + + public function getButtonData(): array + { + return [ + 'label' => __('Back'), + 'on_click' => sprintf("location.href = '%s';", $this->getBackUrl()), + 'class' => 'back', + 'sort_order' => 10 + ]; + } + + private function getBackUrl(): string + { + return $this->context->getUrlBuilder()->getUrl('*/*/index'); + } +} diff --git a/Block/Adminhtml/UpdatePrice/SaveButton.php b/Block/Adminhtml/UpdatePrice/SaveButton.php new file mode 100644 index 0000000..13db0fc --- /dev/null +++ b/Block/Adminhtml/UpdatePrice/SaveButton.php @@ -0,0 +1,26 @@ + __('Update Price'), + 'class' => 'save primary', + 'data_attribute' => [ + 'mage-init' => ['button' => ['event' => 'save']], + 'form-role' => 'save', + ], + 'sort_order' => 90, + ]; + } +} diff --git a/Config.php b/Config.php index 1706e71..f547d3a 100755 --- a/Config.php +++ b/Config.php @@ -47,8 +47,14 @@ class Config const XML_PATH_EMAILS_CUSTOMER_CANCEL_NOTIFICATION_TEMPLATE = 'mollie_subscriptions/emails/customer_cancel_notification_template'; const XML_PATH_DISABLE_NEW_ORDER_CONFIRMATION = 'mollie_subscriptions/emails/disable_new_order_confirmation'; const XML_PATH_ALLOW_ONE_TIME_PURCHASE = 'mollie_subscriptions/general/allow_one_time_purchases'; + const XML_PATH_UPDATE_SUBSCRIPTION_WHEN_PRICE_CHANGES = 'mollie_subscriptions/general/update_subscription_when_price_changes'; const MODULE_SUPPORT_LINK = 'https://www.magmodules.eu/help/%s'; + /** + * @var \Mollie\Payment\Config + */ + private $config; + /** * @var StoreManagerInterface */ @@ -58,22 +64,32 @@ class Config * @var ScopeConfigInterface */ private $scopeConfig; - /** * @var ProductMetadataInterface */ private $metadata; public function __construct( + \Mollie\Payment\Config $config, StoreManagerInterface $storeManager, ScopeConfigInterface $scopeConfig, ProductMetadataInterface $metadata ) { + $this->config = $config; $this->storeManager = $storeManager; $this->scopeConfig = $scopeConfig; $this->metadata = $metadata; } + public function addToLog(string $type, $data): void + { + if (!$this->getFlag(static::XML_PATH_DEBUG)) { + return; + } + + $this->config->addToLog($type, $data); + } + /** * Get Configuration data * @@ -269,6 +285,11 @@ public function allowOneTimePurchase($storeId = null, $scope = ScopeInterface::S return $this->getFlag(static::XML_PATH_ALLOW_ONE_TIME_PURCHASE, $storeId, $scope); } + public function updateSubscriptionWhenPriceChanges(?int $storeId = null, string $scope = ScopeInterface::SCOPE_STORE): bool + { + return $this->getFlag(static::XML_PATH_UPDATE_SUBSCRIPTION_WHEN_PRICE_CHANGES, $storeId, $scope); + } + /** * @param int $storeId * @param string $scope diff --git a/Controller/Adminhtml/Subscriptions/SavePrice.php b/Controller/Adminhtml/Subscriptions/SavePrice.php new file mode 100644 index 0000000..cb75548 --- /dev/null +++ b/Controller/Adminhtml/Subscriptions/SavePrice.php @@ -0,0 +1,108 @@ +messageManager = $messageManager; + $this->request = $request; + $this->resultFactory = $resultFactory; + $this->dataPersistor = $dataPersistor; + $this->config = $config; + $this->mollieApi = $mollieApi; + } + + public function execute(): ResultInterface + { + $data = $this->request->getPostValue(); + $customerId = $data['customer_id'] ?? null; + $subscriptionId = $data['subscription_id'] ?? null; + $newPrice = $data['new_price'] ?? null; + + if (!$customerId || !$subscriptionId) { + throw new LocalizedException(__('Customer ID and subscription ID are required.')); + } + + try { + $mollieClient = $this->mollieApi->loadByStore(); + + $subscription = $mollieClient->subscriptions->getForId($customerId, $subscriptionId); + + $mollieClient->subscriptions->update($customerId, $subscriptionId, [ + 'amount' => [ + 'value' => number_format((float)$newPrice, 2, '.', ''), + 'currency' => $subscription->amount->currency, + ] + ]); + + $this->messageManager->addSuccessMessage(__('Subscription price has been updated successfully.')); + $this->dataPersistor->clear('mollie_subscription_update_price'); + + return $this->redirect('*/*/index'); + } catch (\Exception $exception) { + $this->config->addToLog('Error updating subscription ' . $subscriptionId, $exception); + $this->messageManager->addErrorMessage(__('Error updating subscription price: %1', $exception->getMessage())); + $this->dataPersistor->set('mollie_subscription_update_price', $data); + + return $this->redirect('*/*/*', [ + 'customer_id' => $customerId, + 'subscription_id' => $subscriptionId + ]); + } + } + + private function redirect(string $to, array $arguments = []): ResultInterface + { + $redirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + $redirect->setPath($to, $arguments); + + return $redirect; + } +} diff --git a/Controller/Adminhtml/Subscriptions/Transactions.php b/Controller/Adminhtml/Subscriptions/Transactions.php new file mode 100644 index 0000000..7207fd9 --- /dev/null +++ b/Controller/Adminhtml/Subscriptions/Transactions.php @@ -0,0 +1,34 @@ +pageFactory = $pageFactory; + } + + public function execute() + { + $page = $this->pageFactory->create(); + $page->setActiveMenu('Mollie_Subscriptions::view_subscription_payments'); + $page->getConfig()->getTitle()->prepend(__('Subscription Transactions')); + + return $page; + } +} diff --git a/Controller/Adminhtml/Subscriptions/UpdatePrice.php b/Controller/Adminhtml/Subscriptions/UpdatePrice.php new file mode 100644 index 0000000..31d1976 --- /dev/null +++ b/Controller/Adminhtml/Subscriptions/UpdatePrice.php @@ -0,0 +1,50 @@ +pageFactory = $pageFactory; + $this->request = $request; + } + + public function execute() + { + $customerId = $this->request->getParam('customer_id'); + $subscriptionId = $this->request->getParam('subscription_id'); + + if (!$customerId || !$subscriptionId) { + throw new LocalizedException(__('Customer ID and subscription ID are required.')); + } + + $page = $this->pageFactory->create(); + $page->setActiveMenu('Mollie_Subscriptions::view_subscriptions'); + $page->getConfig()->getTitle()->prepend(__('Update Subscription Price')); + + return $page; + } +} diff --git a/Controller/Index/Restart.php b/Controller/Index/Restart.php index 86d3574..1f2d8bb 100644 --- a/Controller/Index/Restart.php +++ b/Controller/Index/Restart.php @@ -188,6 +188,10 @@ private function saveSubscriptionResult(Subscription $subscription) $model->setStoreId($this->storeManager->getStore()->getId()); $model->setNextPaymentDate($subscription->nextPaymentDate); + if (property_exists($subscription->metadata, 'optionId')) { + $model->setOptionId($subscription->metadata->optionId); + } + $model = $this->subscriptionToProductRepository->save($model); $this->eventManager->dispatch('mollie_subscription_restarted', ['subscription' => $model]); diff --git a/Cron/UpdateSubscriptionsWithAPriceUpdate.php b/Cron/UpdateSubscriptionsWithAPriceUpdate.php index babb92e..e3d4de7 100644 --- a/Cron/UpdateSubscriptionsWithAPriceUpdate.php +++ b/Cron/UpdateSubscriptionsWithAPriceUpdate.php @@ -7,12 +7,14 @@ namespace Mollie\Subscriptions\Cron; +use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; -use Mollie\Payment\Config; use Mollie\Payment\Helper\General; use Mollie\Subscriptions\Api\Data\SubscriptionToProductInterface; use Mollie\Subscriptions\Api\SubscriptionToProductRepositoryInterface; +use Mollie\Subscriptions\Config; +use Mollie\Subscriptions\Service\Magento\GetPriceUpdateForSubscription; use Mollie\Subscriptions\Service\Mollie\MollieSubscriptionApi; class UpdateSubscriptionsWithAPriceUpdate @@ -48,6 +50,10 @@ class UpdateSubscriptionsWithAPriceUpdate * @var PriceCurrencyInterface */ private $priceCurrency; + /** + * @var GetPriceUpdateForSubscription + */ + private $getPriceUpdateForSubscription; public function __construct( Config $config, @@ -55,14 +61,17 @@ public function __construct( General $mollieHelper, SubscriptionToProductRepositoryInterface $subscriptionToProductRepository, ProductRepositoryInterface $productRepository, - PriceCurrencyInterface $priceCurrency - ) { + PriceCurrencyInterface $priceCurrency, + GetPriceUpdateForSubscription $getPriceUpdateForSubscription + ) + { $this->config = $config; $this->mollieSubscriptionApi = $mollieSubscriptionApi; $this->mollieHelper = $mollieHelper; $this->subscriptionToProductRepository = $subscriptionToProductRepository; $this->productRepository = $productRepository; $this->priceCurrency = $priceCurrency; + $this->getPriceUpdateForSubscription = $getPriceUpdateForSubscription; } public function execute() @@ -70,9 +79,13 @@ public function execute() $subscriptions = $this->subscriptionToProductRepository->getSubscriptionsWithAPriceUpdate(); foreach ($subscriptions->getItems() as $item) { + if (!$this->config->updateSubscriptionWhenPriceChanges($item->getStoreId())) { + continue; + } + try { $this->updateSubscription($item); - } catch (\Exception $exception) { + } catch (Exception $exception) { $this->config->addToLog('error', [ 'message' => __('Unable to change the price for subscription "%1"', $item->getEntityId()), 'exception' => $exception->getMessage(), @@ -96,7 +109,8 @@ private function getApiForStore(int $storeId) private function updateSubscription(SubscriptionToProductInterface $item): void { - $price = $this->productRepository->getById($item->getProductId())->getPrice(); + $product = $this->productRepository->getById($item->getProductId()); + $price = $this->getPriceUpdateForSubscription->execute($item, $product); $api = $this->getApiForStore($item->getStoreId()); $subscription = $api->subscriptions->getForId($item->getCustomerId(), $item->getSubscriptionId()); @@ -113,10 +127,21 @@ private function updateSubscription(SubscriptionToProductInterface $item): void return; } - $subscription->amount = $this->mollieHelper->getAmountArray( + $amount = $this->mollieHelper->getAmountArray( $subscription->amount->currency, $this->priceCurrency->convert($price, $item->getStoreId(), $subscription->amount->currency) ); + + if ($subscription->amount->currency == $amount['currency'] && + $subscription->amount->value == $amount['value'] + ) { + $item->setHasPriceUpdate(0); + $this->subscriptionToProductRepository->save($item); + + return; + } + + $subscription->amount = $amount; $subscription->update(); $this->config->addToLog('success', __( diff --git a/DTO/SubscriptionOption.php b/DTO/SubscriptionOption.php index cd1e1b1..bfd91ea 100644 --- a/DTO/SubscriptionOption.php +++ b/DTO/SubscriptionOption.php @@ -14,6 +14,11 @@ class SubscriptionOption */ private $productId; + /** + * @var string + */ + private $optionId; + /** * @var int */ @@ -48,7 +53,6 @@ class SubscriptionOption * @var \DateTimeImmutable */ private $startDate; - /** * @var int|null */ @@ -56,6 +60,7 @@ class SubscriptionOption public function __construct( int $productId, + string $optionId, int $storeId, array $amount, string $interval, @@ -66,6 +71,7 @@ public function __construct( ?int $times = null ) { $this->productId = $productId; + $this->optionId = $optionId; $this->storeId = $storeId; $this->amount = $amount; $this->interval = $interval; @@ -81,6 +87,11 @@ public function getProductId(): int return $this->productId; } + public function getOptionId(): string + { + return $this->optionId; + } + public function getStoreId(): int { return $this->storeId; diff --git a/Model/Data/SubscriptionToProduct.php b/Model/Data/SubscriptionToProduct.php index 85eebd3..286d01c 100644 --- a/Model/Data/SubscriptionToProduct.php +++ b/Model/Data/SubscriptionToProduct.php @@ -88,6 +88,25 @@ public function setProductId(int $productId) return $this->setData(self::PRODUCT_ID, $productId); } + /** + * Get option_id + * @return string|null + */ + public function getOptionId(): ?string + { + return $this->_get(self::OPTION_ID); + } + + /** + * Set option_id + * @param string $optionId + * @return \Mollie\Subscriptions\Api\Data\SubscriptionToProductInterface + */ + public function setOptionId(?string $optionId) + { + return $this->setData(self::OPTION_ID, $optionId); + } + /** * Get store_id * @return int|null diff --git a/Model/MollieSubscriptionsListing.php b/Model/MollieSubscriptionsListing.php index be9e26a..d07001f 100644 --- a/Model/MollieSubscriptionsListing.php +++ b/Model/MollieSubscriptionsListing.php @@ -6,6 +6,8 @@ namespace Mollie\Subscriptions\Model; +use DateInterval; +use DateTimeImmutable; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerInterfaceFactory; @@ -77,7 +79,8 @@ public function __construct( MollieCustomerRepositoryInterface $mollieCustomerRepository, array $components = [], array $data = [] - ) { + ) + { parent::__construct($context, $components, $data); $this->mollieSubscriptionApi = $mollieSubscriptionApi; $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; @@ -110,8 +113,8 @@ public function getDataSourceData() $items = array_map(function (Subscription $subscription) use ($daysBeforeReminder) { $prePaymentReminder = null; if ($subscription->nextPaymentDate) { - $prePaymentReminder = new \DateTimeImmutable($subscription->nextPaymentDate); - $prePaymentReminder = $prePaymentReminder->sub(new \DateInterval('P' . $daysBeforeReminder . 'D')); + $prePaymentReminder = new DateTimeImmutable($subscription->nextPaymentDate); + $prePaymentReminder = $prePaymentReminder->sub(new DateInterval('P' . $daysBeforeReminder . 'D')); } $response = new SubscriptionResponse( @@ -132,23 +135,6 @@ public function getDataSourceData() ]; } - private function preloadCustomers(array $result) - { - $mollieCustomerIds = array_column($result, 'customerId'); - - $searchCriteria = $this->searchCriteriaBuilderFactory->create(); - $searchCriteria->addFilter('mollie_customer_id', $mollieCustomerIds, 'in'); - $result = $this->mollieCustomerRepository->getList($searchCriteria->create()); - - $customerIds = array_map(function (MollieCustomerInterface $customerInfo) { - return $customerInfo->getCustomerId(); - }, $result->getItems()); - - $searchCriteria = $this->searchCriteriaBuilderFactory->create(); - $searchCriteria->addFilter('entity_id', $customerIds, 'in'); - $this->customers = $this->customerRepository->getList($searchCriteria->create())->getItems(); - } - private function getCustomerMollieCustomerById(string $customerId) { foreach ($this->customers as $customer) { @@ -160,6 +146,14 @@ private function getCustomerMollieCustomerById(string $customerId) return $this->customerFactory->create(); } + private function parseLink(string $link): string + { + $query = parse_url($link, PHP_URL_QUERY); + parse_str($query, $parts); + + return $parts['from']; + } + private function parsePreviousNext(SubscriptionCollection $result) { if ($result->hasNext()) { @@ -171,11 +165,20 @@ private function parsePreviousNext(SubscriptionCollection $result) } } - private function parseLink(string $link): string + private function preloadCustomers(array $result) { - $query = parse_url($link, PHP_URL_QUERY); - parse_str($query, $parts); + $mollieCustomerIds = array_column($result, 'customerId'); - return $parts['from']; + $searchCriteria = $this->searchCriteriaBuilderFactory->create(); + $searchCriteria->addFilter('mollie_customer_id', $mollieCustomerIds, 'in'); + $result = $this->mollieCustomerRepository->getList($searchCriteria->create()); + + $customerIds = array_map(function (MollieCustomerInterface $customerInfo) { + return $customerInfo->getCustomerId(); + }, $result->getItems()); + + $searchCriteria = $this->searchCriteriaBuilderFactory->create(); + $searchCriteria->addFilter('entity_id', $customerIds, 'in'); + $this->customers = $this->customerRepository->getList($searchCriteria->create())->getItems(); } } diff --git a/Model/MollieSubscriptionsTransactions.php b/Model/MollieSubscriptionsTransactions.php new file mode 100644 index 0000000..e4b7706 --- /dev/null +++ b/Model/MollieSubscriptionsTransactions.php @@ -0,0 +1,155 @@ +mollieSubscriptionApi = $mollieSubscriptionApi; + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + $this->customerFactory = $customerFactory; + $this->customerRepository = $customerRepository; + $this->mollieCustomerRepository = $mollieCustomerRepository; + $this->config = $config; + } + + public function getDataSourceData(): array + { + $storeId = $this->getContext()->getRequestParam('filters')['store_id'] ?? null; + $customerId = $this->getContext()->getRequestParam('customer_id'); + $subscriptionId = $this->getContext()->getRequestParam('subscription_id'); + + $api = $this->mollieSubscriptionApi->loadByStore($storeId); + + $paging = $this->getContext()->getRequestParam('paging'); + + $pageSize = $paging['pageSize'] ?? 20; + if ($pageSize > 250) { + $pageSize = 250; + } + + $result = $api->subscriptionPayments->pageForIds( + $customerId, + $subscriptionId, + $this->getContext()->getRequestParam('offsetID'), + $pageSize, + ); + + $this->parsePreviousNext($result); + + $items = array_map(function (Payment $payment) { + return [ + 'id' => $payment->id, + 'amount' => $payment->amount->value, + 'description' => $payment->description, + 'status' => $payment->status, + 'created_at' => $payment->createdAt, + 'paid_at' => $payment->paidAt, + 'canceled_at' => $payment->canceledAt, + 'expires_at' => $payment->expiresAt, + 'failed_at' => $payment->failedAt, + 'due_date' => $payment->dueDate, + ]; + }, (array)$result); + + return [ + 'data' => [ + 'items' => $items, + 'nextID' => $this->next, + 'previousID' => $this->previous, + ], + ]; + } + + private function parseLink(string $link): string + { + $query = parse_url($link, PHP_URL_QUERY); + parse_str($query, $parts); + + return $parts['from']; + } + + private function parsePreviousNext(PaymentCollection $result): void + { + if ($result->hasNext()) { + $this->next = $this->parseLink($result->_links->next->href); + } + + if ($result->hasPrevious()) { + $this->previous = $this->parseLink($result->_links->previous->href); + } + } +} diff --git a/Model/UpdatePrice/DataProvider.php b/Model/UpdatePrice/DataProvider.php new file mode 100644 index 0000000..7e38e7a --- /dev/null +++ b/Model/UpdatePrice/DataProvider.php @@ -0,0 +1,101 @@ +request = $request; + $this->dataPersistor = $dataPersistor; + $this->subscriptionRepository = $subscriptionRepository; + $this->mollieApi = $mollieApi; + $this->logger = $logger; + parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); + } + + public function getData() + { + $customerId = $this->request->getParam('customer_id'); + $subscriptionId = $this->request->getParam('subscription_id'); + + if (!$customerId || !$subscriptionId) { + return []; + } + + $data = $this->dataPersistor->get('mollie_subscription_update_price'); + if (!empty($data)) { + $this->dataPersistor->clear('mollie_subscription_update_price'); + return [$subscriptionId => $data]; + } + + try { + // Load subscription from local database + $subscription = $this->subscriptionRepository->getBySubscriptionId($subscriptionId); + + // Load current price from Mollie API + $mollieClient = $this->mollieApi->loadByStore(); + $mollieSubscription = $mollieClient->subscriptions->getForId($customerId, $subscriptionId); + + $formData = [ + 'subscription_id' => $subscriptionId, + 'customer_id' => $customerId, + 'current_price' => $mollieSubscription->amount->value, + 'new_price' => '' + ]; + + return [$subscriptionId => $formData]; + } catch (\Exception $e) { + $this->logger->error('Error loading subscription data: ' . $e->getMessage()); + return []; + } + } + + public function addFilter(\Magento\Framework\Api\Filter $filter) + { + return; + } +} diff --git a/Observer/CatalogProductSaveAfter/UpdateSubscriptionProduct.php b/Observer/CatalogProductSaveAfter/UpdateSubscriptionProduct.php index 6e6cffc..3d51b7e 100644 --- a/Observer/CatalogProductSaveAfter/UpdateSubscriptionProduct.php +++ b/Observer/CatalogProductSaveAfter/UpdateSubscriptionProduct.php @@ -5,20 +5,28 @@ */ namespace Mollie\Subscriptions\Observer\CatalogProductSaveAfter; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Serialize\SerializerInterface; use Mollie\Subscriptions\Api\SubscriptionToProductRepositoryInterface; class UpdateSubscriptionProduct implements ObserverInterface { + /** + * @var SerializerInterface + */ + private $serializer; /** * @var SubscriptionToProductRepositoryInterface */ private $subscriptionToProductRepository; public function __construct( + SerializerInterface $serializer, SubscriptionToProductRepositoryInterface $subscriptionToProductRepository ) { + $this->serializer = $serializer; $this->subscriptionToProductRepository = $subscriptionToProductRepository; } @@ -27,10 +35,41 @@ public function execute(Observer $observer) /** @var \Magento\Catalog\Model\Product $product */ $product = $observer->getData('product'); - if (!$product->dataHasChangedFor('price')) { - return; + if ($product->dataHasChangedFor('price')) { + $this->subscriptionToProductRepository->productHasPriceUpdate($product); + } + + if ($product->dataHasChangedFor('mollie_subscription_table') && + $this->subscriptionTableHasPriceUpdate($product) + ) { + $this->subscriptionToProductRepository->productHasPriceUpdate($product); + } + } + + private function subscriptionTableHasPriceUpdate(ProductInterface $product): bool + { + $old = $this->serializer->unserialize($product->getOrigData('mollie_subscription_table')); + $new = $this->serializer->unserialize($product->getData('mollie_subscription_table')); + + $oldMapping = $this->getIdentifierToPriceMapping($old); + $newMapping = $this->getIdentifierToPriceMapping($new); + + foreach ($oldMapping as $identifier => $price) { + if (array_key_exists($identifier, $newMapping) && $newMapping[$identifier] != $price) { + return true; + } + } + + return false; + } + + private function getIdentifierToPriceMapping(array $table): array + { + $output = []; + foreach ($table as $row) { + $output[$row['identifier']] = $row['price']; } - $this->subscriptionToProductRepository->productHasPriceUpdate($product); + return $output; } } diff --git a/Observer/MollieProcessTransactionEnd/CreateSubscriptions.php b/Observer/MollieProcessTransactionEnd/CreateSubscriptions.php index 76b141d..aa7e277 100644 --- a/Observer/MollieProcessTransactionEnd/CreateSubscriptions.php +++ b/Observer/MollieProcessTransactionEnd/CreateSubscriptions.php @@ -155,6 +155,7 @@ private function createSubscription(string $customerId, SubscriptionOption $subs $model->setProductId($subscriptionOptions->getProductId()); $model->setStoreId($subscriptionOptions->getStoreId()); $model->setNextPaymentDate($subscription->nextPaymentDate); + $model->setOptionId($subscriptionOptions->getOptionId()); $model = $this->subscriptionToProductRepository->save($model); diff --git a/Service/Magento/CreateOrderFromSubscription.php b/Service/Magento/CreateOrderFromSubscription.php index acb474e..c5b97a9 100644 --- a/Service/Magento/CreateOrderFromSubscription.php +++ b/Service/Magento/CreateOrderFromSubscription.php @@ -1,4 +1,8 @@ customer = $this->customerRepository->getById($mollieCustomer->getCustomerId()); $cart = $this->getCart(); - $this->product = $this->subscriptionAddToCart->execute($cart, $this->subscription->metadata); + $this->product = $this->subscriptionAddToCart->execute($cart, $this->subscription); $cart->setBillingAddress($this->formatAddress($this->getAddress('billing'))); diff --git a/Service/Magento/GetPriceUpdateForSubscription.php b/Service/Magento/GetPriceUpdateForSubscription.php new file mode 100644 index 0000000..da359be --- /dev/null +++ b/Service/Magento/GetPriceUpdateForSubscription.php @@ -0,0 +1,73 @@ +parseSubscriptionOptions = $parseSubscriptionOptions; + $this->catalogData = $catalogData; + } + + /** + * The issue: A subscription can have the price of a product, but it can also have a price that belongs to the + * chosen subscription. Example: Product price = 200, monthly price = 30. When the price is updated we need to + * calculate the new price for a specific subscription. + */ + public function execute(SubscriptionToProductInterface $item, ProductInterface $product): float + { + $price = $this->getPrice($product, $item); + + return $this->catalogData->getTaxPrice( + $product, + $price, + true, + null, + null, + null, + null, + null, + false + ); + } + + public function getPrice(ProductInterface $product, SubscriptionToProductInterface $item): float + { + $options = $this->parseSubscriptionOptions->execute($product); + foreach ($options as $option) { + if ($option->getIdentifier() !== $item->getOptionId()) { + continue; + } + + if ($option->getPrice() === null) { + return $product->getPrice(); + } + + return $option->getPrice(); + } + + return $product->getPrice(); + } +} diff --git a/Service/Magento/SubscriptionAddProductToCart.php b/Service/Magento/SubscriptionAddProductToCart.php index 9372fc8..7764915 100644 --- a/Service/Magento/SubscriptionAddProductToCart.php +++ b/Service/Magento/SubscriptionAddProductToCart.php @@ -1,4 +1,8 @@ productRepository = $productRepository; } - public function execute(CartInterface $cart, object $metadata): ProductInterface + public function execute(CartInterface $cart, Subscription $subscription): ProductInterface { + $metadata = $subscription->metadata; $sku = $metadata->sku; $parentSku = isset($metadata->parent_sku) ? $metadata->parent_sku : null; $quantity = isset($metadata->quantity) ? (float)$metadata->quantity : 1; @@ -32,7 +38,9 @@ public function execute(CartInterface $cart, object $metadata): ProductInterface $cart->setIsVirtual($product->getIsVirtual()); if (!$parentSku) { - $cart->addProduct($product, $quantity); + $item = $cart->addProduct($product, $quantity); + $item->setCustomPrice($subscription->amount->value); + $item->setOriginalCustomPrice($subscription->amount->value); return $product; } @@ -41,16 +49,19 @@ public function execute(CartInterface $cart, object $metadata): ProductInterface $productAttributeOptions = $product->getTypeInstance(true)->getConfigurableAttributesAsArray($product); $options = []; - foreach($productAttributeOptions as $option) { + foreach ($productAttributeOptions as $option) { $options[$option['attribute_id']] = $childProduct->getData($option['attribute_code']); } - $cart->addProduct($product, new DataObject([ + $item = $cart->addProduct($product, new DataObject([ 'product' => $product->getId(), 'qty' => $quantity, 'super_attribute' => $options, ])); + $item->setCustomPrice($subscription->amount->value); + $item->setOriginalCustomPrice($subscription->amount->value); + return $product; } } diff --git a/Service/Mollie/ParseSubscriptionOptions.php b/Service/Mollie/ParseSubscriptionOptions.php index c58acd3..89cfdb4 100644 --- a/Service/Mollie/ParseSubscriptionOptions.php +++ b/Service/Mollie/ParseSubscriptionOptions.php @@ -7,6 +7,7 @@ namespace Mollie\Subscriptions\Service\Mollie; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Helper\Data; use Magento\Framework\Serialize\SerializerInterface; use Mollie\Subscriptions\DTO\ProductSubscriptionOption; use Mollie\Subscriptions\DTO\ProductSubscriptionOptionFactory; @@ -22,13 +23,19 @@ class ParseSubscriptionOptions * @var ProductSubscriptionOptionFactory */ private $productSubscriptionOptionFactory; + /** + * @var Data + */ + private $catalogHelper; public function __construct( SerializerInterface $serializer, - ProductSubscriptionOptionFactory $productSubscriptionOptionFactory + ProductSubscriptionOptionFactory $productSubscriptionOptionFactory, + Data $catalogHelper ) { $this->serializer = $serializer; $this->productSubscriptionOptionFactory = $productSubscriptionOptionFactory; + $this->catalogHelper = $catalogHelper; } /** @@ -44,8 +51,27 @@ public function execute(ProductInterface $product): array $json = $this->serializer->unserialize($table); - return array_map( function ($option) { + return array_map(function ($option) use ($product) { + if (array_key_exists('price', $option)) { + $option['price'] = $this->addTaxToPrice($product, $option['price']); + } + return $this->productSubscriptionOptionFactory->create($option); }, $json); } + + private function addTaxToPrice(ProductInterface $product, float $price): float + { + return $this->catalogHelper->getTaxPrice( + $product, + $price, + true, + null, + null, + null, + null, + null, + false + ); + } } diff --git a/Service/Mollie/SubscriptionOptions.php b/Service/Mollie/SubscriptionOptions.php index eda1aaf..6d09fce 100644 --- a/Service/Mollie/SubscriptionOptions.php +++ b/Service/Mollie/SubscriptionOptions.php @@ -100,7 +100,7 @@ private function createSubscriptionFor(OrderItemInterface $orderItem): Subscript { $this->options = []; $this->orderItem = $orderItem; - $this->loadSubscriptionOption($orderItem); + $this->loadSubscriptionOption(); $this->addAmount(); $this->addShippingCost(); @@ -118,6 +118,7 @@ private function createSubscriptionFor(OrderItemInterface $orderItem): Subscript return new SubscriptionOption( $orderItem->getProductId(), + $this->currentOption->getIdentifier(), $this->order->getStoreId(), $amount, $this->options['interval'] ?? '', @@ -186,10 +187,13 @@ private function addDescription(): void private function addMetadata(): void { $product = $this->orderItem->getProduct(); + $optionId = $this->getOptionIdFromOrderItem(); + $metadata = [ 'sku' => $product->getSku(), 'quantity' => $this->orderItem->getQtyOrdered(), 'billingAddressId' => $this->order->getBillingAddressId(), + 'optionId' => $optionId, ]; if ($parent = $this->orderItem->getParentItem()) { @@ -288,9 +292,9 @@ private function getIntervalDescription(): string return ''; } - private function loadSubscriptionOption(OrderItemInterface $item): void + private function getOptionIdFromOrderItem(): string { - $mollieMetadata = $item->getBuyRequest()->getData('mollie_metadata'); + $mollieMetadata = $this->orderItem->getBuyRequest()->getData('mollie_metadata'); if ($mollieMetadata === null) { throw new \Exception('No Mollie Metadata present on order item'); } @@ -299,8 +303,13 @@ private function loadSubscriptionOption(OrderItemInterface $item): void throw new \Exception('No recurring metadata or option_id present on order item'); } - $optionId = $mollieMetadata['recurring_metadata']['option_id']; - $options = $this->parseSubscriptionOptions->execute($item->getProduct()); + return $mollieMetadata['recurring_metadata']['option_id']; + } + + private function loadSubscriptionOption(): void + { + $optionId = $this->getOptionIdFromOrderItem(); + $options = $this->parseSubscriptionOptions->execute($this->orderItem->getProduct()); foreach($options as $option) { if ($option->getIdentifier() == $optionId) { $this->currentOption = $option; diff --git a/Service/Mollie/UpdateNextPaymentDate.php b/Service/Mollie/UpdateNextPaymentDate.php index e855f73..2cdb310 100644 --- a/Service/Mollie/UpdateNextPaymentDate.php +++ b/Service/Mollie/UpdateNextPaymentDate.php @@ -26,6 +26,10 @@ public function __construct( public function execute(Subscription $subscription): void { + if ($subscription->nextPaymentDate === null) { + return; + } + $row = $this->subscriptionToProductRepository->getBySubscriptionId($subscription->id); $row->setNextPaymentDate($subscription->nextPaymentDate); $this->subscriptionToProductRepository->save($row); diff --git a/Setup/Patch/Data/AddOptionIdToMetadata.php b/Setup/Patch/Data/AddOptionIdToMetadata.php new file mode 100644 index 0000000..d6a3e31 --- /dev/null +++ b/Setup/Patch/Data/AddOptionIdToMetadata.php @@ -0,0 +1,102 @@ +repository = $repository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->mollieApiClient = $mollieApiClient; + $this->productRepository = $productRepository; + $this->parseSubscriptionOptions = $parseSubscriptionOptions; + } + + public function apply(): self + { + $searchCriteria = $this->searchCriteriaBuilder->create(); + + $result = $this->repository->getList($searchCriteria->create()); + foreach ($result->getItems() as $item) { + try { + $this->updateSubscription($item); + } catch (\Exception $e) {} + } + + return $this; + } + + public function getAliases(): array + { + return []; + } + + public static function getDependencies(): array + { + return []; + } + + private function updateSubscription(SubscriptionToProductInterface $item): void + { + $mollieApi = $this->mollieApiClient->loadByStore($item->getStoreId()); + $subscription = $mollieApi->subscriptions->getForId($item->getCustomerId(), $item->getSubscriptionId()); + + $interval = $subscription->interval; + $product = $this->productRepository->getById($item->getProductId()); + + $options = $this->parseSubscriptionOptions->execute($product); + + foreach ($options as $option) { + $optionInterval = (int)$option->getIntervalAmount() . ' ' . $option->getIntervalType(); + + // We send X months, Mollie returns 1 month + if ($optionInterval != $interval && $optionInterval != $interval . 's') { + continue; + } + + $item->setOptionId($option->getIdentifier()); + + $this->repository->save($item); + return; + } + } +} diff --git a/Test/Integration/Controller/Api/WebhookTest.php b/Test/Integration/Controller/Api/WebhookTest.php index 6bfb5c0..48a11e9 100644 --- a/Test/Integration/Controller/Api/WebhookTest.php +++ b/Test/Integration/Controller/Api/WebhookTest.php @@ -28,6 +28,7 @@ use Mollie\Subscriptions\Api\Data\SubscriptionToProductInterface; use Mollie\Subscriptions\Api\SubscriptionToProductRepositoryInterface; use Mollie\Subscriptions\Service\Mollie\MollieSubscriptionApi; +use stdClass; class WebhookTest extends ControllerTestCase { @@ -251,9 +252,12 @@ private function getSubscription(?callable $customize = null): Subscription { /** @var Subscription $subscription */ $subscription = $this->_objectManager->get(Subscription::class); + $subscription->amount = new stdClass(); + $subscription->amount->value = 100; + $subscription->amount->currency = 'EUR'; $subscription->id = 'sub_testsubscription'; $subscription->customerId = 'cst_testcustomer'; - $subscription->metadata = new \stdClass(); + $subscription->metadata = new stdClass(); $subscription->metadata->sku = 'simple'; $subscription->nextPaymentDate = '2019-11-19'; @@ -273,8 +277,8 @@ private function getPayment(string $transactionId): Payment $payment->id = $transactionId; $payment->customerId = 'cst_testcustomer'; $payment->subscriptionId = 'sub_testsubscription'; - $payment->_links = new \stdClass(); - $payment->_links->subscription = new \stdClass(); + $payment->_links = new stdClass(); + $payment->_links->subscription = new stdClass(); $payment->_links->subscription->href = 'https://example.com/mollie/subscriptions/sub_testsubscription'; return $payment; diff --git a/Test/Integration/Service/Magento/CreateOrderFromSubscriptionTest.php b/Test/Integration/Service/Magento/CreateOrderFromSubscriptionTest.php index 9b0d958..ed2f9ca 100644 --- a/Test/Integration/Service/Magento/CreateOrderFromSubscriptionTest.php +++ b/Test/Integration/Service/Magento/CreateOrderFromSubscriptionTest.php @@ -1,4 +1,8 @@ customerId = 'cst_testcustomer'; $subscription = $this->objectManager->get(Subscription::class); + $subscription->amount = new stdClass(); + $subscription->amount->value = 100; + $subscription->amount->currency = 'EUR'; $subscription->customerId = 'cst_testcustomer'; - $subscription->metadata = new \stdClass(); + $subscription->metadata = new stdClass(); $subscription->metadata->quantity = '1'; $subscription->metadata->sku = 'simple'; @@ -74,8 +81,11 @@ public function testHandlesVirtualProductsCorrect(): void $payment->customerId = 'cst_testcustomer'; $subscription = $this->objectManager->get(Subscription::class); + $subscription->amount = new stdClass(); + $subscription->amount->value = 100; + $subscription->amount->currency = 'EUR'; $subscription->customerId = 'cst_testcustomer'; - $subscription->metadata = new \stdClass(); + $subscription->metadata = new stdClass(); $subscription->metadata->quantity = '1'; $subscription->metadata->sku = 'simple'; diff --git a/Test/Integration/Service/Mollie/SubscriptionOptionsTest.php b/Test/Integration/Service/Mollie/SubscriptionOptionsTest.php index 8c2400c..d6d3e45 100644 --- a/Test/Integration/Service/Mollie/SubscriptionOptionsTest.php +++ b/Test/Integration/Service/Mollie/SubscriptionOptionsTest.php @@ -190,6 +190,34 @@ public function testAddsParentSku() $this->assertEquals('example-parent-sku', $subscription->toArray()['metadata']['parent_sku']); } + /** + * @magentoDataFixture Magento/Sales/_files/order.php + */ + public function testAddsTheOptionId(): void + { + $order = $this->loadOrder('100000001'); + $items = $order->getItems(); + + /** @var OrderItemInterface $orderItem */ + $orderItem = array_shift($items); + + $this->setOptionIdOnOrderItem($orderItem, 'weekly-finite'); + $this->setTheSubscriptionOnTheProduct($orderItem->getProduct()); + + $orderItem->getProduct()->setData('sku', 'example-sku'); + + /** @var SubscriptionOptions $instance */ + $instance = $this->objectManager->create(SubscriptionOptions::class); + $result = $instance->forOrder($order); + + $this->assertCount(1, $result); + $subscription = $result[0]; + $this->assertInstanceOf(SubscriptionOption::class, $subscription); + $this->assertArrayHasKey('metadata', $subscription->toArray()); + $this->assertArrayHasKey('optionId', $subscription->toArray()['metadata']); + $this->assertEquals('weekly-finite', $subscription->toArray()['metadata']['optionId']); + } + /** * @magentoDataFixture Magento/Sales/_files/order.php */ diff --git a/Ui/Component/Listing/Column/Actions.php b/Ui/Component/Listing/Column/Actions.php index 9230bc6..bcde708 100644 --- a/Ui/Component/Listing/Column/Actions.php +++ b/Ui/Component/Listing/Column/Actions.php @@ -38,29 +38,51 @@ public function prepareDataSource(array $dataSource) $storeId = $this->getContext()->getRequestParam('filters')['store_id'] ?? null; foreach ($dataSource['data']['items'] as &$item) { - if ($item['status'] != 'active') { - continue; - } + $output = []; + + $output['transactions'] = [ + 'href' => $this->urlBuilder->getUrl( + 'mollie_subscriptions/subscriptions/transactions', + [ + 'store_id' => $storeId, + 'customer_id' => $item['customer_id'], + 'subscription_id' => $item['id'], + ] + ), + 'label' => __('View transactions'), + ]; - $url = $this->urlBuilder->getUrl( - 'mollie_subscriptions/subscriptions/cancel', - [ - 'store_id' => $storeId, - 'customer_id' => $item['customer_id'], - 'subscription_id' => $item['id'], - ] - ); + if ($item['status'] == 'active') { + $output['update_price'] = [ + 'href' => $this->urlBuilder->getUrl( + 'mollie_subscriptions/subscriptions/updatePrice', + [ + 'store_id' => $storeId, + 'customer_id' => $item['customer_id'], + 'subscription_id' => $item['id'], + ] + ), + 'label' => __('Update price'), + ]; - $item[$this->getData('name')] = [ - 'view' => [ - 'href' => $url, + $output['cancel'] = [ + 'href' => $this->urlBuilder->getUrl( + 'mollie_subscriptions/subscriptions/cancel', + [ + 'store_id' => $storeId, + 'customer_id' => $item['customer_id'], + 'subscription_id' => $item['id'], + ] + ), 'label' => __('Cancel'), 'confirm' => [ 'title' => __('Delete'), 'message' => __('Are you sure you want to delete this record?'), ], - ] - ]; + ]; + } + + $item[$this->getData('name')] = $output; } return $dataSource; diff --git a/etc/adminhtml/system/general.xml b/etc/adminhtml/system/general.xml index 3e031f3..3c497c1 100755 --- a/etc/adminhtml/system/general.xml +++ b/etc/adminhtml/system/general.xml @@ -1,10 +1,8 @@ + ~ Copyright Magmodules.eu. All rights reserved. + ~ See COPYING.txt for license details. + --> mollie_subscriptions/general/shipping_method This method is used when recurring orders are created. - + Magento\Config\Model\Config\Source\Yesno mollie_subscriptions/general/allow_one_time_purchases Note: This can be changed on a per-product level + + + Magento\Config\Model\Config\Source\Yesno + mollie_subscriptions/general/update_subscription_when_price_changes + When the product price is updates, should we update the subscription as well? + diff --git a/etc/config.xml b/etc/config.xml index b575850..a455d1c 100755 --- a/etc/config.xml +++ b/etc/config.xml @@ -1,10 +1,8 @@ + ~ Copyright Magmodules.eu. All rights reserved. + ~ See COPYING.txt for license details. + --> @@ -13,6 +11,7 @@ v1.16.0 0 1 + 0 1 diff --git a/etc/db_schema.xml b/etc/db_schema.xml index 6679e29..33c0ac6 100755 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -1,6 +1,6 @@ + diff --git a/view/adminhtml/layout/mollie_subscriptions_subscriptions_transactions.xml b/view/adminhtml/layout/mollie_subscriptions_subscriptions_transactions.xml new file mode 100644 index 0000000..4458d8a --- /dev/null +++ b/view/adminhtml/layout/mollie_subscriptions_subscriptions_transactions.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/view/adminhtml/layout/mollie_subscriptions_subscriptions_updateprice.xml b/view/adminhtml/layout/mollie_subscriptions_subscriptions_updateprice.xml new file mode 100644 index 0000000..177e226 --- /dev/null +++ b/view/adminhtml/layout/mollie_subscriptions_subscriptions_updateprice.xml @@ -0,0 +1,15 @@ + + + + + Update Subscription Price + + + + + + + diff --git a/view/adminhtml/ui_component/mollie_subscriptions_subscription_update_price_form.xml b/view/adminhtml/ui_component/mollie_subscriptions_subscription_update_price_form.xml new file mode 100644 index 0000000..793ed8b --- /dev/null +++ b/view/adminhtml/ui_component/mollie_subscriptions_subscription_update_price_form.xml @@ -0,0 +1,79 @@ + + +
+ + + mollie_subscriptions_subscription_update_price_form.update_price_form_data_source + + Update Subscription Price + templates/form/collapsible + + + +