diff --git a/.github/workflows/end-2-end-test.yml b/.github/workflows/end-2-end-test.yml index 43455ac0f36..14ff4a960e1 100644 --- a/.github/workflows/end-2-end-test.yml +++ b/.github/workflows/end-2-end-test.yml @@ -76,10 +76,12 @@ jobs: MAGENTO_URL=$(docker exec magento-project-community-edition /bin/bash -c "curl -s ngrok:4040/api/tunnels |jq -r \".tunnels[0].public_url\"") echo "magento_url=$MAGENTO_URL" >> $GITHUB_ENV + # Note the `mollie-pwa.html` file, as it is copied to the pub folder. This is so that it can be accessed by Cypress. - name: Upload the code into the docker container run: | sed -i '/version/d' ./composer.json && \ docker cp $(pwd) magento-project-community-edition:/data/extensions/ && \ + docker cp $(pwd)/Test/End-2-end/cypress/fixtures/mollie-pwa.html magento-project-community-edition:/data/pub/opt/ && \ docker exec magento-project-community-edition ./install-composer-package mollie/magento2:@dev - name: Activate the extension diff --git a/.github/workflows/templates/e2e/Dockerfile b/.github/workflows/templates/e2e/Dockerfile index 2136acaf2c3..b9bf86b6530 100644 --- a/.github/workflows/templates/e2e/Dockerfile +++ b/.github/workflows/templates/e2e/Dockerfile @@ -2,6 +2,7 @@ FROM cypress/included:12.1.0 WORKDIR /e2e +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* RUN npm i @cypress/webpack-preprocessor cypress-mollie cypress-testrail --save-dev CMD tail -f /dev/null diff --git a/Config.php b/Config.php index 9fcb87b2724..0a2bf109633 100644 --- a/Config.php +++ b/Config.php @@ -58,6 +58,7 @@ class Config const PAYMENT_CREDITCARD_USE_COMPONENTS = 'payment/mollie_methods_creditcard/use_components'; const PAYMENT_CREDITCARD_ENABLE_CUSTOMERS_API = 'payment/mollie_methods_creditcard/enable_customers_api'; const PAYMENT_BANKTRANSFER_STATUS_PENDING = 'payment/mollie_methods_banktransfer/order_status_pending'; + const PAYMENT_METHOD_ISSUER_LIST_TYPE = 'payment/mollie_methods_%s/issuer_list_type'; const PAYMENT_METHOD_PAYMENT_ACTIVE = 'payment/mollie_methods_%s/active'; const PAYMENT_METHOD_PAYMENT_DESCRIPTION = 'payment/mollie_methods_%s/payment_description'; const PAYMENT_METHOD_PAYMENT_SURCHARGE_FIXED_AMOUNT = 'payment/mollie_methods_%s/payment_surcharge_fixed_amount'; @@ -73,6 +74,7 @@ class Config const PAYMENT_VOUCHER_CUSTOM_ATTRIBUTE = 'payment/mollie_methods_voucher/custom_attribute'; const CURRENCY_OPTIONS_DEFAULT = 'currency/options/default'; + /** * @var ScopeConfigInterface */ @@ -726,6 +728,20 @@ public function encryptPaymentDetails($storeId = null): bool return $this->isSetFlag(static::GENERAL_ENCRYPT_PAYMENT_DETAILS, $storeId); } + /** + * @param string $method + * @param null|int|string $storeId + * + * @return string + */ + public function getIssuerListType(string $method, $storeId = null): string + { + return $this->getPath( + $this->addMethodToPath(static::PAYMENT_METHOD_ISSUER_LIST_TYPE, $method), + $storeId + ) ?? 'none'; + } + /** * @param $method * @return string diff --git a/Controller/ApplePay/ShippingMethods.php b/Controller/ApplePay/ShippingMethods.php index 6d4377f3dfe..f65b04b9da7 100644 --- a/Controller/ApplePay/ShippingMethods.php +++ b/Controller/ApplePay/ShippingMethods.php @@ -11,13 +11,10 @@ use Magento\Framework\App\Action\Context; use Magento\Framework\Controller\ResultFactory; use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Quote\Api\Data\AddressInterfaceFactory; use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Api\Data\PaymentInterface; -use Magento\Quote\Api\Data\PaymentInterfaceFactory; use Magento\Quote\Api\GuestCartRepositoryInterface; -use Magento\Quote\Api\PaymentMethodManagementInterface; use Magento\Quote\Api\ShippingMethodManagementInterface; +use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Address\Total as AddressTotal; class ShippingMethods extends Action @@ -33,47 +30,25 @@ class ShippingMethods extends Action private $guestCartRepository; /** - * @var AddressInterfaceFactory - */ - private $addressFactory; - - /** - * @var PaymentMethodManagementInterface - */ - private $paymentMethodManagement; - - /** - * @var PaymentInterfaceFactory + * @var ShippingMethodManagementInterface */ - private $paymentInterfaceFactory; + private $shippingMethodManagement; /** * @var CheckoutSession */ private $checkoutSession; - /** - * @var ShippingMethodManagementInterface - */ - private $shippingMethodManagement; - public function __construct( Context $context, CartRepositoryInterface $cartRepository, - GuestCartRepositoryInterface $guestCartRepository, ShippingMethodManagementInterface $shippingMethodManagement, - AddressInterfaceFactory $addressFactory, - PaymentMethodManagementInterface $paymentMethodManagement, - PaymentInterfaceFactory $paymentInterfaceFactory, - CheckoutSession $checkoutSession + CheckoutSession $checkoutSession, + GuestCartRepositoryInterface $guestCartRepository ) { parent::__construct($context); - - $this->guestCartRepository = $guestCartRepository; $this->shippingMethodManagement = $shippingMethodManagement; - $this->addressFactory = $addressFactory; - $this->paymentMethodManagement = $paymentMethodManagement; - $this->paymentInterfaceFactory = $paymentInterfaceFactory; + $this->guestCartRepository = $guestCartRepository; $this->cartRepository = $cartRepository; $this->checkoutSession = $checkoutSession; } @@ -82,28 +57,25 @@ public function execute() { $cart = $this->getCart(); - $address = $this->addressFactory->create(); + /** + * @var Address $address + */ + $address = $cart->getShippingAddress(); + $address->setData(null); $address->setCountryId($this->getRequest()->getParam('countryCode')); $address->setPostcode($this->getRequest()->getParam('postalCode')); - $cart->setShippingAddress($address); - - $cart->collectTotals(); - $this->cartRepository->save($cart); - if ($this->getRequest()->getParam('shippingMethod')) { - $this->addShippingMethod($cart, $this->getRequest()->getParam('shippingMethod')['identifier']); + $address->setCollectShippingRates(true); + $address->setShippingMethod($this->getRequest()->getParam('shippingMethod')['identifier']); } - $methods = $this->shippingMethodManagement->getList($cart->getId()); - $this->setDefaultShippingMethod($cart, $methods); - - /** @var PaymentInterface $payment */ - $payment = $this->paymentInterfaceFactory->create(); - $payment->setMethod('mollie_methods_applepay'); - $this->paymentMethodManagement->set($cart->getId(), $payment); - $cart = $this->cartRepository->get($cart->getId()); + $cart->setPaymentMethod('mollie_methods_applepay'); + $cart->getPayment()->importData(['method' => 'mollie_methods_applepay']); + $this->cartRepository->save($cart); + $cart->collectTotals(); + $methods = $this->shippingMethodManagement->getList($cart->getId()); $response = $this->resultFactory->create(ResultFactory::TYPE_JSON); return $response->setData([ @@ -118,6 +90,7 @@ public function execute() 'totals' => array_map(function (AddressTotal $total) { return [ 'type' => 'final', + 'code' => $total->getCode(), 'label' => $total->getData('title'), 'amount' => number_format($total->getData('value'), 2, '.', ''), ]; @@ -125,36 +98,6 @@ public function execute() ]); } - /** - * @param CartInterface $cart - * @param \Magento\Quote\Api\Data\ShippingMethodInterface[] $methods - */ - private function setDefaultShippingMethod(CartInterface $cart, array $methods) - { - if ($cart->getShippingAddress()->getShippingMethod()) { - return; - } - - $method = array_shift($methods); - if (!$method) { - return; - } - - $this->addShippingMethod($cart, $method->getCarrierCode() . '_' . $method->getMethodCode()); - $this->cartRepository->save($cart); - } - - private function addShippingMethod(CartInterface $cart, string $identifier) - { - $address = $cart->getShippingAddress(); - - $address->setShippingMethod($identifier); - $address->setCollectShippingRates(true); - $address->save(); - - $address->collectShippingRates(); - } - /** * @throws \Magento\Framework\Exception\NoSuchEntityException * @return CartInterface diff --git a/Helper/General.php b/Helper/General.php index 06945c89d3b..36683d2556e 100755 --- a/Helper/General.php +++ b/Helper/General.php @@ -380,6 +380,8 @@ public function useImage($storeId = null) } /** + * @deprecated See \Mollie\Payment\Config::getIssuerListType instead + * * @param string $method * * @return mixed @@ -387,7 +389,7 @@ public function useImage($storeId = null) public function getIssuerListType(string $method): string { $methodXpath = str_replace('%method%', $method, self::XPATH_ISSUER_LIST_TYPE); - return $this->getStoreConfig($methodXpath); + return $this->getStoreConfig($methodXpath) ?? 'none'; } /** diff --git a/Model/MollieConfigProvider.php b/Model/MollieConfigProvider.php index bb9797195e6..3efe02cc6ad 100644 --- a/Model/MollieConfigProvider.php +++ b/Model/MollieConfigProvider.php @@ -263,7 +263,7 @@ public function getActiveMethods(MollieApiClient $mollieApi, CartInterface $cart */ private function getIssuers(MollieApiClient $mollieApi, string $code, array $config): array { - $issuerListType = $this->mollieHelper->getIssuerListType($code); + $issuerListType = $this->config->getIssuerListType($code, $this->storeManager->getStore()->getId()); $config['payment']['issuersListType'][$code] = $issuerListType; $config['payment']['issuers'][$code] = $this->getIssuers->execute($mollieApi, $code, $issuerListType); diff --git a/Model/PaymentFee/Quote/Address/Total/PaymentFeeTax.php b/Model/PaymentFee/Quote/Address/Total/PaymentFeeTax.php index 1e6f0d8afcb..b4772d7947c 100644 --- a/Model/PaymentFee/Quote/Address/Total/PaymentFeeTax.php +++ b/Model/PaymentFee/Quote/Address/Total/PaymentFeeTax.php @@ -6,25 +6,30 @@ namespace Mollie\Payment\Model\PaymentFee\Quote\Address\Total; +use Magento\Customer\Api\AccountManagementInterface as CustomerAccountManagement; +use Magento\Customer\Api\Data\AddressInterfaceFactory as CustomerAddressFactory; +use Magento\Customer\Api\Data\RegionInterfaceFactory as CustomerAddressRegionFactory; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address\Total; use Magento\Quote\Model\Quote\Address\Total\AbstractTotal; +use Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterfaceFactory; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; +use Magento\Tax\Api\Data\TaxClassKeyInterface; +use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; +use Magento\Tax\Api\TaxCalculationInterface; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Tax\Model\Config as TaxConfig; use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector; use Mollie\Payment\Config; use Mollie\Payment\Exceptions\UnknownPaymentFeeType; -use Mollie\Payment\Service\Config\PaymentFee as PaymentFeeConfig; use Mollie\Payment\Service\PaymentFee\Calculate; use Mollie\Payment\Service\PaymentFee\Result; -class PaymentFeeTax extends AbstractTotal +class PaymentFeeTax extends CommonTaxCollector { - /** - * @var PaymentFeeConfig - */ - private $paymentFeeConfig; - /** * @var PriceCurrencyInterface */ @@ -41,14 +46,55 @@ class PaymentFeeTax extends AbstractTotal private $config; public function __construct( - PaymentFeeConfig $paymentFeeConfig, - PriceCurrencyInterface $priceCurrency, + TaxConfig $taxConfig, + TaxCalculationInterface $taxCalculationService, + QuoteDetailsInterfaceFactory $quoteDetailsDataObjectFactory, + QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory, + TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory, + CustomerAddressFactory $customerAddressFactory, + CustomerAddressRegionFactory $customerAddressRegionFactory, Calculate $calculate, - Config $config + PriceCurrencyInterface $priceCurrency, + Config $config, + TaxHelper $taxHelper = null, + QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null, + ?CustomerAccountManagement $customerAccountManagement = null ) { - $this->paymentFeeConfig = $paymentFeeConfig; - $this->priceCurrency = $priceCurrency; + $parent = new \ReflectionClass(parent::class); + $parentConstructor = $parent->getConstructor(); + + // The parent call fails when running setup:di:compile in 2.4.3 and lower due to an extra parameter. + if ($parentConstructor->getNumberOfParameters() == 9) { + // @phpstan-ignore-next-line + parent::__construct( + $taxConfig, + $taxCalculationService, + $quoteDetailsDataObjectFactory, + $quoteDetailsItemDataObjectFactory, + $taxClassKeyDataObjectFactory, + $customerAddressFactory, + $customerAddressRegionFactory, + $taxHelper, + $quoteDetailsItemExtensionInterfaceFactory + ); + } else { + // @phpstan-ignore-next-line + parent::__construct( + $taxConfig, + $taxCalculationService, + $quoteDetailsDataObjectFactory, + $quoteDetailsItemDataObjectFactory, + $taxClassKeyDataObjectFactory, + $customerAddressFactory, + $customerAddressRegionFactory, + $taxHelper, + $quoteDetailsItemExtensionInterfaceFactory, + $customerAccountManagement + ); + } + $this->calculate = $calculate; + $this->priceCurrency = $priceCurrency; $this->config = $config; } @@ -74,10 +120,26 @@ public function collect(Quote $quote, ShippingAssignmentInterface $shippingAssig $this->addAssociatedTaxable($shippingAssignment, $result, $quote); + $feeDataObject = $this->quoteDetailsItemDataObjectFactory->create() + ->setType('mollie_payment_fee') + ->setCode('mollie_payment_fee') + ->setQuantity(1); + + $feeDataObject->setUnitPrice($result->getRoundedAmount()); + $feeDataObject->setTaxClassKey( + $this->taxClassKeyDataObjectFactory->create() + ->setType(TaxClassKeyInterface::TYPE_ID) + ->setValue(4) + ); + $feeDataObject->setIsTaxIncluded(true); + + $quoteDetails = $this->prepareQuoteDetails($shippingAssignment, [$feeDataObject]); + + $this->taxCalculationService->calculateTax($quoteDetails, $quote->getStoreId()); + parent::collect($quote, $shippingAssignment, $total); $extensionAttributes = $quote->getExtensionAttributes(); - if (!$extensionAttributes) { return $this; } diff --git a/Plugin/Tax/Helper/DataPlugin.php b/Plugin/Tax/Helper/DataPlugin.php new file mode 100644 index 00000000000..58b90e589d1 --- /dev/null +++ b/Plugin/Tax/Helper/DataPlugin.php @@ -0,0 +1,104 @@ +orderTaxManagement = $orderTaxManagement; + } + + public function afterGetCalculatedTaxes(object $callable, array $result, $source): array + { + if (!$source instanceof InvoiceInterface && + !$source instanceof CreditmemoInterface + ) { + return $result; + } + + $order = $source->getOrder(); + $orderTaxDetails = $this->orderTaxManagement->getOrderTaxDetails($order->getId()); + + $items = array_filter($orderTaxDetails->getItems(), function (Item $item) { + return $item->getType() == 'mollie_payment_fee_tax'; + }); + + if (count($items) === 0) { + return $result; + } + + foreach ($items as $item) { + $result = $this->aggregateTaxes($result, $item, 1); + } + + return $result; + } + + /** + * Copied from \Magento\Tax\Helper\Data::_aggregateTaxes + * + * @param $taxClassAmount + * @param OrderTaxDetailsItemInterface $itemTaxDetail + * @param $ratio + * @return array + */ + private function aggregateTaxes($taxClassAmount, OrderTaxDetailsItemInterface $itemTaxDetail, $ratio) + { + $itemAppliedTaxes = $itemTaxDetail->getAppliedTaxes(); + foreach ($itemAppliedTaxes as $itemAppliedTax) { + $taxAmount = $itemAppliedTax->getAmount() * $ratio; + $baseTaxAmount = $itemAppliedTax->getBaseAmount() * $ratio; + + if (0 == $taxAmount && 0 == $baseTaxAmount) { + continue; + } + $taxCode = $this->getKeyByName($taxClassAmount, $itemAppliedTax->getCode()); + if (!isset($taxClassAmount[$taxCode])) { + $taxClassAmount[$taxCode]['title'] = $itemAppliedTax->getTitle(); + $taxClassAmount[$taxCode]['percent'] = $itemAppliedTax->getPercent(); + $taxClassAmount[$taxCode]['tax_amount'] = $taxAmount; + $taxClassAmount[$taxCode]['base_tax_amount'] = $baseTaxAmount; + } else { + $taxClassAmount[$taxCode]['tax_amount'] += $taxAmount; + $taxClassAmount[$taxCode]['base_tax_amount'] += $baseTaxAmount; + } + } + + return $taxClassAmount; + } + + /** + * @param array $taxClassAmount + * @param string $name + * @return string|int + */ + private function getKeyByName(array $taxClassAmount, string $name) + { + foreach ($taxClassAmount as $key => $tax) { + if ($tax['title'] === $name) { + return $key; + } + } + + return $name; + } +} diff --git a/Service/Mollie/GetIssuers.php b/Service/Mollie/GetIssuers.php index 25a3f0986dc..d9259c5ff71 100644 --- a/Service/Mollie/GetIssuers.php +++ b/Service/Mollie/GetIssuers.php @@ -10,6 +10,7 @@ use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\Locale\Resolver; use Mollie\Api\MollieApiClient; +use Mollie\Payment\Config; use Mollie\Payment\Helper\General; use Mollie\Payment\Model\Mollie as MollieModel; @@ -36,24 +37,23 @@ class GetIssuers * @var Resolver */ private $resolver; - /** - * @var General + * @var Config */ - private $general; + private $config; public function __construct( CacheInterface $cache, SerializerInterface $serializer, MollieModel $mollieModel, Resolver $resolver, - General $general + Config $config ) { $this->cache = $cache; $this->serializer = $serializer; $this->mollieModel = $mollieModel; $this->resolver = $resolver; - $this->general = $general; + $this->config = $config; } /** @@ -106,7 +106,7 @@ public function getForGraphql($storeId, string $method): ?array $issuers = $this->execute( $mollieApi, $method, - $this->general->getIssuerListType($method) + $this->config->getIssuerListType($method) ); if (!$issuers) { diff --git a/Test/End-2-end/cypress/e2e/magento/backend/refunds.cy.js b/Test/End-2-end/cypress/e2e/magento/backend/refunds.cy.js new file mode 100644 index 00000000000..a44b2e4c6bb --- /dev/null +++ b/Test/End-2-end/cypress/e2e/magento/backend/refunds.cy.js @@ -0,0 +1,28 @@ +/* + * Copyright Magmodules.eu. All rights reserved. + * See COPYING.txt for license details. + */ + +import InvoiceOverviewPage from "Pages/backend/InvoiceOverviewPage"; +import InvoicePage from "Pages/backend/InvoicePage"; +import CreditMemoPage from "Pages/backend/CreditMemoPage"; +import PlaceOrderComposite from "CompositeActions/PlaceOrderComposite"; + +const invoiceOverviewPage = new InvoiceOverviewPage(); +const invoicePage = new InvoicePage(); +const creditMemoPage = new CreditMemoPage(); +const placeOrderComposite = new PlaceOrderComposite(); + +describe('Check that refunds behave as excepted', () => { + it('Can do a refund on an iDeal order', () => { + placeOrderComposite.placeOrder(); + + cy.get('@order-id').then(orderId => { + invoiceOverviewPage.openByOrderId(orderId); + }); + + invoicePage.creditMemo(); + + creditMemoPage.refund(); + }); +}); diff --git a/Test/End-2-end/cypress/e2e/magento/graphql/place-order.cy.js b/Test/End-2-end/cypress/e2e/magento/graphql/place-order.cy.js new file mode 100644 index 00000000000..b4251d1c255 --- /dev/null +++ b/Test/End-2-end/cypress/e2e/magento/graphql/place-order.cy.js @@ -0,0 +1,50 @@ +/* + * Copyright Magmodules.eu. All rights reserved. + * See COPYING.txt for license details. + */ + +import Cookies from "Services/Cookies"; +import MollieHostedPaymentPage from "Pages/mollie/MollieHostedPaymentPage"; +import CheckoutSuccessPage from "Pages/frontend/CheckoutSuccessPage"; +import OrdersPage from "Pages/backend/OrdersPage"; + +const cookies = new Cookies(); +const mollieHostedPaymentPage = new MollieHostedPaymentPage(); +const checkoutSuccessPage = new CheckoutSuccessPage(); +const ordersPage = new OrdersPage(); + +describe('Check that the headless endpoints work as expected', () => { + it('C1835263: Validate that an order can be placed through GraphQL ', () => { + cy.visit('opt/mollie-pwa.html'); + + cy.get('[data-key="start-checkout-process"]').click(); + + cy.get('[data-key="mollie_methods_ideal"]').click(); + + cy.get('[data-key="mollie_methods_ideal-issuer"]').first().click(); + + cy.get('[data-key="place-order-action"]').click(); + + cy.get('[data-key="increment-id"]').then((element) => { + cy.wrap(element.text()).as('increment-id'); + }); + + cookies.disableSameSiteCookieRestrictions(); + + cy.get('[data-key="redirect-url"]').then((element) => { + cy.visit(element.attr('href')); + }); + + mollieHostedPaymentPage.selectStatus('paid'); + + checkoutSuccessPage.assertThatOrderSuccessPageIsShown(); + + cy.backendLogin(false); + + cy.get('@increment-id').then((incrementId) => { + ordersPage.openByIncrementId(incrementId); + }); + + ordersPage.assertOrderStatusIs('Processing'); + }); +}) diff --git a/Test/End-2-end/cypress/fixtures/mollie-pwa.html b/Test/End-2-end/cypress/fixtures/mollie-pwa.html new file mode 100644 index 00000000000..fcea4e2c5e7 --- /dev/null +++ b/Test/End-2-end/cypress/fixtures/mollie-pwa.html @@ -0,0 +1,834 @@ + + + + +
payment/mollie_general/type
)
+ Order ID: {{ orderId }}
+1. Please open this url and select a payment status (or not):
+ {{ redirectUrl }}2. When you have opened the url, click here to get the payment status:
+ + +