diff --git a/.github/workflows/end-2-end-test.yml b/.github/workflows/end-2-end-test.yml index c6b6e5773b0..47c67b07bbe 100644 --- a/.github/workflows/end-2-end-test.yml +++ b/.github/workflows/end-2-end-test.yml @@ -87,8 +87,9 @@ jobs: - name: Activate the extension run: | + docker exec magento-project-community-edition php /data/merge-config.php docker exec magento-project-community-edition ./retry "php bin/magento module:enable Mollie_Payment" - docker exec magento-project-community-edition ./retry "php bin/magento setup:upgrade" + docker exec magento-project-community-edition ./retry "php bin/magento setup:upgrade --no-interaction" docker exec magento-project-community-edition /bin/bash /data/configure-mollie.sh docker exec magento-project-community-edition ./retry "bin/magento config:set payment/mollie_general/use_webhooks custom_url" docker exec magento-project-community-edition ./retry "bin/magento config:set payment/mollie_general/custom_webhook_url ${{ env.magento_url }}/mollie/checkout/webhook" diff --git a/.github/workflows/templates/docker-compose.yml b/.github/workflows/templates/docker-compose.yml index 213c0599447..bb5e8e3355d 100644 --- a/.github/workflows/templates/docker-compose.yml +++ b/.github/workflows/templates/docker-compose.yml @@ -10,6 +10,7 @@ services: volumes: - ../../../magento-logs:/data/var/log - ./magento/configure-mollie.sh:/data/configure-mollie.sh + - ./magento/merge-config.php.stub:/data/merge-config.php depends_on: - ngrok diff --git a/.github/workflows/templates/e2e/Dockerfile b/.github/workflows/templates/e2e/Dockerfile index b9bf86b6530..ac3be1a6c1f 100644 --- a/.github/workflows/templates/e2e/Dockerfile +++ b/.github/workflows/templates/e2e/Dockerfile @@ -1,4 +1,4 @@ -FROM cypress/included:12.1.0 +FROM cypress/included:13.6.2 WORKDIR /e2e diff --git a/.github/workflows/templates/magento/configure-mollie.sh b/.github/workflows/templates/magento/configure-mollie.sh index 8fb86fb33e2..291a70f260d 100644 --- a/.github/workflows/templates/magento/configure-mollie.sh +++ b/.github/workflows/templates/magento/configure-mollie.sh @@ -29,6 +29,7 @@ bin/magento config:set payment/mollie_methods_paymentlink/active 1 & bin/magento config:set payment/mollie_methods_paysafecard/active 1 & bin/magento config:set payment/mollie_methods_pointofsale/active 1 & bin/magento config:set payment/mollie_methods_sofort/active 1 & +bin/magento config:set payment/mollie_methods_twint/active 1 & # Enable Components bin/magento config:set payment/mollie_methods_creditcard/use_components 1 & @@ -38,6 +39,11 @@ bin/magento config:set payment/mollie_methods_ideal/add_qr 1 & bin/magento config:set payment/mollie_general/use_webhooks disabled & +# Configure currency for the swiss store view +bin/magento config:set currency/options/allow EUR,CHF & +bin/magento config:set currency/options/default CHF --scope=ch & +bin/magento config:set payment/mollie_general/currency 0 --scope=ch & + wait if grep -q Magento_TwoFactorAuth "app/etc/config.php"; then diff --git a/.github/workflows/templates/magento/merge-config.php.stub b/.github/workflows/templates/magento/merge-config.php.stub new file mode 100644 index 00000000000..74652e2bef3 --- /dev/null +++ b/.github/workflows/templates/magento/merge-config.php.stub @@ -0,0 +1,81 @@ + [ + 'admin' => [ + 'website_id' => '0', + 'code' => 'admin', + 'name' => 'Admin', + 'sort_order' => '0', + 'default_group_id' => '0', + 'is_default' => '0' + ], + 'base' => [ + 'website_id' => '1', + 'code' => 'base', + 'name' => 'Main Website', + 'sort_order' => '0', + 'default_group_id' => '1', + 'is_default' => '1' + ] + ], + 'groups' => [ + [ + 'group_id' => '0', + 'website_id' => '0', + 'name' => 'Default', + 'root_category_id' => '0', + 'default_store_id' => '0', + 'code' => 'default' + ], + [ + 'group_id' => '1', + 'website_id' => '1', + 'name' => 'Main Website Store', + 'root_category_id' => '2', + 'default_store_id' => '1', + 'code' => 'main_website_store' + ] + ], + 'stores' => [ + 'admin' => [ + 'store_id' => '0', + 'code' => 'admin', + 'website_id' => '0', + 'group_id' => '0', + 'name' => 'Admin', + 'sort_order' => '0', + 'is_active' => '1' + ], + 'default' => [ + 'store_id' => '1', + 'code' => 'default', + 'website_id' => '1', + 'group_id' => '1', + 'name' => 'Default Store View', + 'sort_order' => '0', + 'is_active' => '1' + ], + 'ch' => [ + 'store_id' => '2', + 'code' => 'ch', + 'website_id' => '1', + 'group_id' => '1', + 'name' => 'Swiss', + 'sort_order' => '0', + 'is_active' => '1' + ] + ] +]; + +file_put_contents( + 'app/etc/config.php', + 'mollieHelper = $mollieHelper; $this->timezone = $context->getLocaleDate(); $this->registry = $registry; $this->price = $price; + $this->encryptor = $encryptor; + $this->config = $config; + $this->urlBuilder = $urlBuilder; } public function getCheckoutType(): ?string @@ -87,16 +99,24 @@ public function getExpiresAt(): ?string return null; } - /** - * @param mixed $storeId - */ public function getPaymentLink($storeId = null): ?string { - if ($checkoutUrl = $this->getCheckoutUrl()) { - return $this->mollieHelper->getPaymentLinkMessage($checkoutUrl, $storeId); + if (!$this->config->addPaymentLinkMessage($storeId)) { + return null; } - return null; + return str_replace( + '%link%', + $this->getPaymentLinkUrl(), + $this->config->paymentLinkMessage($storeId) + ); + } + + public function getPaymentLinkUrl(): string + { + return $this->urlBuilder->getUrl('mollie/checkout/paymentlink', [ + 'order' => base64_encode($this->encryptor->encrypt($this->getInfo()->getParentId())), + ]); } public function getCheckoutUrl(): ?string diff --git a/Config.php b/Config.php index 0a2bf109633..a9f35f4d4c7 100644 --- a/Config.php +++ b/Config.php @@ -69,6 +69,8 @@ class Config const PAYMENT_METHOD_PAYMENT_TITLE = 'payment/mollie_methods_%s/title'; const PAYMENT_PAYMENTLINK_ALLOW_MARK_AS_PAID = 'payment/mollie_methods_paymentlink/allow_mark_as_paid'; const PAYMENT_PAYMENTLINK_NEW_STATUS = 'payment/mollie_methods_paymentlink/order_status_new'; + const PAYMENT_PAYMENTLINK_ADD_MESSAGE = 'payment/mollie_methods_paymentlink/add_message'; + const PAYMENT_PAYMENTLINK_MESSAGE = 'payment/mollie_methods_paymentlink/message'; const PAYMENT_POINTOFSALE_ALLOWED_CUSTOMER_GROUPS = 'payment/mollie_methods_pointofsale/allowed_customer_groups'; const PAYMENT_VOUCHER_CATEGORY = 'payment/mollie_methods_voucher/category'; const PAYMENT_VOUCHER_CUSTOM_ATTRIBUTE = 'payment/mollie_methods_voucher/custom_attribute'; @@ -495,6 +497,22 @@ public function statusNewPaymentLink($storeId = null) ); } + public function addPaymentLinkMessage($storeId = null): string + { + return (string)$this->getPath( + static::PAYMENT_PAYMENTLINK_ADD_MESSAGE, + $storeId + ); + } + + public function paymentLinkMessage($storeId = null): string + { + return (string)$this->getPath( + static::PAYMENT_PAYMENTLINK_MESSAGE, + $storeId + ); + } + /** * @param string $method * @param int|null $storeId diff --git a/Controller/ApplePay/AppleDeveloperMerchantidDomainAssociation.php b/Controller/ApplePay/AppleDeveloperMerchantidDomainAssociation.php index 10e67eb89ec..0fa82aa266c 100644 --- a/Controller/ApplePay/AppleDeveloperMerchantidDomainAssociation.php +++ b/Controller/ApplePay/AppleDeveloperMerchantidDomainAssociation.php @@ -12,9 +12,15 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Filesystem\Driver\File; use Magento\Framework\Module\Dir; +use Mollie\Payment\Config; +use Mollie\Payment\Service\Mollie\ApplePay\Certificate; class AppleDeveloperMerchantidDomainAssociation implements HttpGetActionInterface { + /** + * @var Config + */ + private $config; /** * @var ResultFactory */ @@ -27,21 +33,34 @@ class AppleDeveloperMerchantidDomainAssociation implements HttpGetActionInterfac * @var Dir */ private $moduleDir; + /** + * @var Certificate + */ + private $certificate; public function __construct( + Config $config, ResultFactory $resultFactory, File $driverFile, - Dir $moduleDir + Dir $moduleDir, + Certificate $certificate ) { $this->resultFactory = $resultFactory; $this->driverFile = $driverFile; $this->moduleDir = $moduleDir; + $this->certificate = $certificate; + $this->config = $config; } public function execute() { - $path = $this->moduleDir->getDir('Mollie_Payment'); - $contents = $this->driverFile->fileGetContents($path . '/apple-developer-merchantid-domain-association'); + try { + $contents = $this->certificate->execute(); + } catch (\Exception $exception) { + $this->config->addToLog('Unable to retrieve Apple Pay certificate', [$exception->getTraceAsString()]); + $path = $this->moduleDir->getDir('Mollie_Payment'); + $contents = $this->driverFile->fileGetContents($path . '/apple-developer-merchantid-domain-association'); + } $response = $this->resultFactory->create(ResultFactory::TYPE_RAW); $response->setHeader('Content-Type', 'text/plain'); diff --git a/Controller/Checkout/PaymentLink.php b/Controller/Checkout/PaymentLink.php new file mode 100644 index 00000000000..0db0b5b7144 --- /dev/null +++ b/Controller/Checkout/PaymentLink.php @@ -0,0 +1,99 @@ +request = $request; + $this->encryptor = $encryptor; + $this->resultFactory = $resultFactory; + $this->messageManager = $messageManager; + $this->orderRepository = $orderRepository; + $this->mollie = $mollie; + } + + public function execute() + { + $orderKey = $this->request->getParam('order'); + if (!$orderKey) { + return $this->returnStatusCode(400); + } + + $id = $this->encryptor->decrypt(base64_decode($orderKey)); + + if (empty($id)) { + return $this->returnStatusCode(404); + } + + try { + $order = $this->orderRepository->get($id); + } catch (NoSuchEntityException $exception) { + return $this->returnStatusCode(404); + } + + if (in_array($order->getState(), [Order::STATE_PROCESSING, Order::STATE_COMPLETE])) { + $this->messageManager->addSuccessMessage(__('Your order has already been paid.')); + + return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setUrl('/'); + } + + $url = $this->mollie->startTransaction($order); + + return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setUrl($url); + } + + public function returnStatusCode(int $code): ResultInterface + { + return $this->resultFactory->create(ResultFactory::TYPE_RAW)->setHttpResponseCode($code); + } +} diff --git a/GraphQL/Resolver/General/MolliePaymentMethods.php b/GraphQL/Resolver/General/MolliePaymentMethods.php index a37d254f276..a254eb68f80 100644 --- a/GraphQL/Resolver/General/MolliePaymentMethods.php +++ b/GraphQL/Resolver/General/MolliePaymentMethods.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Api\Data\CartInterfaceFactory; use Mollie\Api\Resources\Method; +use Mollie\Api\Resources\MethodCollection; use Mollie\Payment\Config; use Mollie\Payment\Service\Mollie\MethodParameters; use Mollie\Payment\Service\Mollie\MollieApiClient; @@ -52,7 +53,7 @@ public function __construct( public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { $amount = 10; - $currency = 'EUR'; + $currency = null; if (isset($args['input'], $args['input']['amount'])) { $amount = $args['input']['amount']; @@ -62,17 +63,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $currency = $args['input']['currency']; } - $parameters = [ - 'amount[value]' => number_format($amount, 2, '.', ''), - 'amount[currency]' => $currency, - 'resource' => 'orders', - 'includeWallets' => 'applepay', - ]; - - $parameters = $this->methodParameters->enhance($parameters, $this->cartFactory->create()); $storeId = $context->getExtensionAttributes()->getStore()->getId(); - $mollieApiClient = $this->mollieApiClient->loadByStore($storeId); - $apiMethods = $mollieApiClient->methods->allActive($parameters); + $apiMethods = $this->getMethods($amount, $currency, $storeId) ?? []; $methods = []; /** @var Method $method */ @@ -97,4 +89,24 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value 'methods' => $methods, ]; } + + public function getMethods(float $amount, ?string $currency, int $storeId): ?MethodCollection + { + $mollieApiClient = $this->mollieApiClient->loadByStore($storeId); + + if ($currency === null) { + return $mollieApiClient->methods->allAvailable(); + } + + $parameters = [ + 'amount[value]' => number_format($amount, 2, '.', ''), + 'amount[currency]' => $currency, + 'resource' => 'orders', + 'includeWallets' => 'applepay', + ]; + + return $mollieApiClient->methods->allActive( + $this->methodParameters->enhance($parameters, $this->cartFactory->create()) + ); + } } diff --git a/Helper/General.php b/Helper/General.php index faf42696d71..dbd9ded1967 100755 --- a/Helper/General.php +++ b/Helper/General.php @@ -478,6 +478,7 @@ public function sendInvoice($storeId = 0) * @param int|null $storeId * * @return mixed + * @deprecated since 2.34.0 */ public function getPaymentLinkMessage($checkoutUrl, $storeId = null) { @@ -642,6 +643,7 @@ public function getAllActiveMethods($storeId) 'mollie_methods_pointofsale', 'mollie_methods_przelewy24', 'mollie_methods_sofort', + 'mollie_methods_twint', ]; foreach ($methodCodes as $methodCode) { diff --git a/Helper/Tests.php b/Helper/Tests.php index ddff081a900..9369c5058b9 100644 --- a/Helper/Tests.php +++ b/Helper/Tests.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; +use Mollie\Api\Exceptions\ApiException; use Mollie\Payment\Model\Mollie as MollieModel; /** @@ -49,15 +50,19 @@ public function getMethods($testKey = null, $liveKey = null) try { $availableMethods = []; $mollieApi = $this->mollieModel->loadMollieApi($testKey); - $methods = $mollieApi->methods->all([ - 'resource' => 'orders', - 'includeWallets' => 'applepay', - ]); + $methods = $mollieApi->methods->allAvailable() ?? []; foreach ($methods as $apiMethod) { $availableMethods[] = ucfirst($apiMethod->id); } + try { + $mollieApi->terminals->page(); + $availableMethods[] = 'Point of sale'; + } catch (ApiException $exception) {} + + sort($availableMethods); + if (empty($availableMethods)) { $msg = __('Enabled Methods: None, Please enable the payment methods in your Mollie dashboard.'); $methodsMsg = '' . $msg . ''; @@ -82,15 +87,19 @@ public function getMethods($testKey = null, $liveKey = null) try { $availableMethods = []; $mollieApi = $this->mollieModel->loadMollieApi($liveKey); - $methods = $mollieApi->methods->all([ - 'resource' => 'orders', - 'includeWallets' => 'applepay', - ]); + $methods = $mollieApi->methods->allAvailable() ?? []; foreach ($methods as $apiMethod) { $availableMethods[] = ucfirst($apiMethod->id); } + try { + $mollieApi->terminals->page(); + $availableMethods[] = 'Point of sale'; + } catch (ApiException $exception) {} + + sort($availableMethods); + if (empty($availableMethods)) { $msg = __('Enabled Methods: None, Please enable the payment methods in your Mollie dashboard.'); $methodsMsg = '' . $msg . ''; diff --git a/Model/Client/Orders.php b/Model/Client/Orders.php index 608cde8f902..82acaf41dcf 100644 --- a/Model/Client/Orders.php +++ b/Model/Client/Orders.php @@ -261,8 +261,11 @@ public function startTransaction(OrderInterface $order, $mollieApi) $orderData['method'] = $additionalData['limited_methods']; } - if ($this->expires->availableForMethod($method, $storeId)) { - $orderData['expiresAt'] = $this->expires->atDateForMethod($method, $storeId); + if ($this->expires->availableForMethod($this->methodCode->getExpiresAtMethod(), $storeId)) { + $orderData['expiresAt'] = $this->expires->atDateForMethod( + $this->methodCode->getExpiresAtMethod(), + $storeId + ); } $orderData = $this->buildTransaction->execute($order, static::CHECKOUT_TYPE, $orderData); diff --git a/Model/Client/Orders/Processors/SuccessfulPayment.php b/Model/Client/Orders/Processors/SuccessfulPayment.php index abac8332123..42ccec81835 100644 --- a/Model/Client/Orders/Processors/SuccessfulPayment.php +++ b/Model/Client/Orders/Processors/SuccessfulPayment.php @@ -122,6 +122,21 @@ public function process(OrderInterface $order, Order $mollieOrder, string $type, return $this->processTransactionResponseFactory->create($result); } + /** @var false|\Mollie\Api\Resources\Payment $payment */ + $payment = $mollieOrder->payments()->offsetGet(0); + if ($payment && $payment->hasChargebacks()) { + $this->orderCommentHistory->add($order, + __( + 'Mollie: Received a chargeback with an amount of %1', + $order->getBaseCurrency()->formatTxt($payment->getAmountChargedBack()) + ) + ); + + $result = ['success' => false, 'status' => 'paid', 'order_id' => $orderId, 'type' => $type]; + $this->mollieHelper->addTolog('error', __('Payment has chargebacks.')); + return $this->processTransactionResponseFactory->create($result); + } + if (!$order->getPayment()->getIsTransactionClosed() && $type == 'webhook') { $this->handleWebhookCall($order, $mollieOrder); $this->sendOrderEmails($order); diff --git a/Model/Client/Payments.php b/Model/Client/Payments.php index 6cb8d125de4..f490100d980 100644 --- a/Model/Client/Payments.php +++ b/Model/Client/Payments.php @@ -372,6 +372,20 @@ public function processTransaction(Order $order, $mollieApi, $type = 'webhook', $payment->setCurrencyCode($order->getBaseCurrencyCode()); $payment->setIsTransactionClosed(true); + if ($paymentData->hasChargebacks()) { + $order->addCommentToStatusHistory( + __( + 'Mollie: Received a chargeback with an amount of %1', + $order->getBaseCurrency()->formatTxt($paymentData->getAmountChargedBack()) + ) + ); + + $msg = ['success' => true, 'status' => 'paid', 'order_id' => $orderId, 'type' => $type]; + $this->mollieHelper->addTolog('success', $msg); + $this->checkCheckoutSession($order, $paymentToken, $paymentData, $type); + return $msg; + } + if ($this->canRegisterCaptureNotification->execute($order, $paymentData) || $type != static::TRANSACTION_TYPE_WEBHOOK ) { diff --git a/Model/Client/Payments/Processors/SuccessfulPayment.php b/Model/Client/Payments/Processors/SuccessfulPayment.php index 5ac42a7ac87..df853ebc5f2 100644 --- a/Model/Client/Payments/Processors/SuccessfulPayment.php +++ b/Model/Client/Payments/Processors/SuccessfulPayment.php @@ -101,6 +101,22 @@ public function process( $amount = $molliePayment->amount->value; $currency = $molliePayment->amount->currency; + if ($molliePayment->hasChargebacks()) { + $this->orderCommentHistory->add($magentoOrder, + __( + 'Mollie: Received a chargeback with an amount of %1', + $magentoOrder->getBaseCurrency()->formatTxt($molliePayment->getAmountChargedBack()) + ) + ); + + return $this->processTransactionResponseFactory->create([ + 'success' => false, + 'status' => 'chargeback', + 'order_id' => $magentoOrder->getId(), + 'type' => $type, + ]); + } + $orderAmount = $this->orderAmount->getByTransactionId($magentoOrder->getMollieTransactionId()); if ($currency != $orderAmount['currency']) { return $this->processTransactionResponseFactory->create([ diff --git a/Model/Issuer.php b/Model/Issuer.php new file mode 100644 index 00000000000..ade9210999a --- /dev/null +++ b/Model/Issuer.php @@ -0,0 +1,72 @@ +id = $id; + $this->name = $name; + $this->images = $images; + } + + public function getId(): string + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getImage(): string + { + return $this->images['svg']; + } + + public function getImages(): array + { + return $this->images; + } + + public function getImage1x(): string + { + return $this->images['size1x'] ?? ''; + } + + public function getImage2x(): string + { + return $this->images['size2x'] ?? ''; + } + + public function getImageSvg(): string + { + return $this->images['svg'] ?? ''; + } +} diff --git a/Model/MethodMeta.php b/Model/MethodMeta.php new file mode 100644 index 00000000000..dbaf143fd42 --- /dev/null +++ b/Model/MethodMeta.php @@ -0,0 +1,61 @@ +code = $code; + $this->issuers = $issuers; + $this->terminals = $terminals; + } + + /** + * @return string + */ + public function getCode(): string + { + return $this->code; + } + + /** + * @return string[] + */ + public function getIssuers(): array + { + return $this->issuers; + } + + /** + * @return string[] + */ + public function getTerminals(): array + { + return $this->terminals; + } +} diff --git a/Model/Methods/Twint.php b/Model/Methods/Twint.php new file mode 100644 index 00000000000..c426c2fa6aa --- /dev/null +++ b/Model/Methods/Twint.php @@ -0,0 +1,24 @@ +id = $id; + $this->brand = $brand; + $this->model = $model; + $this->serialNumber = $serialNumber; + $this->description = $description; + } + + public function getId(): string + { + return $this->id; + } + + public function getBrand(): string + { + return $this->brand; + } + + public function getModel(): string + { + return $this->model; + } + + public function getSerialNumber(): ?string + { + return $this->serialNumber; + } + + public function getDescription(): string + { + return $this->description; + } +} diff --git a/Observer/SalesModelServiceQuoteSubmitSuccess/StartTransactionForPaymentLinkOrders.php b/Observer/SalesModelServiceQuoteSubmitSuccess/StartTransactionForPaymentLinkOrders.php deleted file mode 100644 index bdcdc1678ff..00000000000 --- a/Observer/SalesModelServiceQuoteSubmitSuccess/StartTransactionForPaymentLinkOrders.php +++ /dev/null @@ -1,43 +0,0 @@ -mollie = $mollie; - } - - public function execute(Observer $observer) - { - if (!$observer->hasData('order')) { - return; - } - - /** @var OrderInterface $order */ - $order = $observer->getData('order'); - - if ($order->getPayment()->getData('method') != Paymentlink::CODE) { - return; - } - - $this->mollie->startTransaction($order); - } -} diff --git a/Plugin/Quote/Api/LimitMethodsForRecurringPayments.php b/Plugin/Quote/Api/LimitMethodsForRecurringPayments.php index 61cc534c6ba..3a70000aad6 100644 --- a/Plugin/Quote/Api/LimitMethodsForRecurringPayments.php +++ b/Plugin/Quote/Api/LimitMethodsForRecurringPayments.php @@ -25,6 +25,7 @@ class LimitMethodsForRecurringPayments 'mollie_methods_mybank', 'mollie_methods_paypal', 'mollie_methods_sofort', + 'mollie_methods_twint', ]; /** diff --git a/README.md b/README.md index 736f4e5c21c..58d9ebf1f59 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Mollie requires no minimum costs, no fixed contracts, no hidden costs. At Mollie - Przelewy24 - SEPA Direct Debit - SOFORT Banking +- TWINT ## Additional modules diff --git a/Service/Mollie/ApplePay/Certificate.php b/Service/Mollie/ApplePay/Certificate.php new file mode 100644 index 00000000000..a72683da169 --- /dev/null +++ b/Service/Mollie/ApplePay/Certificate.php @@ -0,0 +1,77 @@ +cache = $cache; + $this->client = $client; + $this->config = $config; + } + + /** + * @return string + * @throws \Exception + */ + public function execute(): string + { + $identifier = static::CACHE_IDENTIFIER_PREFIX; + $result = $this->cache->load($identifier); + if ($result) { + return $result; + } + + $this->config->addToLog('Fetching Apple Pay certificate from www.mollie.com', []); + $certificate = $this->fetchCertificate(); + + $this->cache->save( + $certificate, + $identifier, + ['mollie_payment', 'mollie_payment_apple_pay_certificate'], + 7 * 24 * 60 * 60 // Cache for 1 week + ); + + return $certificate; + } + + private function fetchCertificate(): string + { + $this->client->get('https://www.mollie.com/.well-known/apple-developer-merchantid-domain-association'); + + if ($this->client->getStatus() !== 200) { + throw new \Exception('Unable to retrieve Apple Pay certificate from www.mollie.com'); + } + + return $this->client->getBody(); + } +} diff --git a/Service/Mollie/PaymentMethods.php b/Service/Mollie/PaymentMethods.php index 8ed725b6b9d..b497e7584f3 100644 --- a/Service/Mollie/PaymentMethods.php +++ b/Service/Mollie/PaymentMethods.php @@ -48,6 +48,7 @@ public function __construct( 'mollie_methods_pointofsale', 'mollie_methods_przelewy24', 'mollie_methods_sofort', + 'mollie_methods_twint', 'mollie_methods_voucher', ]; diff --git a/Service/Order/Lines/Generator/AmastyExtraFee.php b/Service/Order/Lines/Generator/AmastyExtraFee.php index bbb40cc5f90..e5e5f19b8ba 100644 --- a/Service/Order/Lines/Generator/AmastyExtraFee.php +++ b/Service/Order/Lines/Generator/AmastyExtraFee.php @@ -40,6 +40,10 @@ public function process(OrderInterface $order, array $orderLines): array $extensionAttributes->getAmextrafeeBaseTaxAmount(); } + if (abs($amount) < 0.01) { + return $orderLines; + } + $orderLines[] = [ 'type' => 'surcharge', 'name' => 'Amasty Fee', diff --git a/Service/Order/Lines/Generator/FoomanTotals.php b/Service/Order/Lines/Generator/FoomanTotals.php index b17a8ddd3f0..9ee9378a51d 100644 --- a/Service/Order/Lines/Generator/FoomanTotals.php +++ b/Service/Order/Lines/Generator/FoomanTotals.php @@ -62,6 +62,11 @@ public function process(OrderInterface $order, array $orderLines): array if ($taxAmount && $amount != 0) { $vatRate = round(($taxAmount / $amount) * 100, 2); } + + if (abs($amount + $taxAmount) < 0.01) { + return $orderLines; + } + $orderLines[] = [ 'type' => 'surcharge', 'name' => $total->getLabel(), diff --git a/Service/Order/Lines/Generator/MageWorxRewardPoints.php b/Service/Order/Lines/Generator/MageWorxRewardPoints.php index c1aea774d39..8c50957d600 100644 --- a/Service/Order/Lines/Generator/MageWorxRewardPoints.php +++ b/Service/Order/Lines/Generator/MageWorxRewardPoints.php @@ -24,19 +24,24 @@ public function __construct( public function process(OrderInterface $order, array $orderLines): array { - if (!$order->getMwRwrdpointsAmnt()) { + $amount = $order->getMwRwrdpointsAmnt(); + if (!$amount) { return $orderLines; } $forceBaseCurrency = (bool)$this->mollieHelper->useBaseCurrency($order->getStoreId()); $currency = $forceBaseCurrency ? $order->getBaseCurrencyCode() : $order->getOrderCurrencyCode(); + if (abs($amount) < 0.01) { + return $orderLines; + } + $orderLines[] = [ 'type' => 'surcharge', 'name' => 'Reward Points', 'quantity' => 1, - 'unitPrice' => $this->mollieHelper->getAmountArray($currency, -$order->getMwRwrdpointsAmnt()), - 'totalAmount' => $this->mollieHelper->getAmountArray($currency, -$order->getMwRwrdpointsAmnt()), + 'unitPrice' => $this->mollieHelper->getAmountArray($currency, -$amount), + 'totalAmount' => $this->mollieHelper->getAmountArray($currency, -$amount), 'vatRate' => 0, 'vatAmount' => $this->mollieHelper->getAmountArray($currency, 0.0), ]; diff --git a/Service/Order/Lines/Generator/MagentoGiftCard.php b/Service/Order/Lines/Generator/MagentoGiftCard.php index e868abee1a1..e5d193463dd 100644 --- a/Service/Order/Lines/Generator/MagentoGiftCard.php +++ b/Service/Order/Lines/Generator/MagentoGiftCard.php @@ -33,6 +33,10 @@ public function process(OrderInterface $order, array $orderLines): array $currency = $forceBaseCurrency ? $order->getBaseCurrencyCode() : $order->getOrderCurrencyCode(); $amount = $order->getData(($forceBaseCurrency ? 'base_' : '') . 'gift_cards_amount'); + if (abs($amount) < 0.01) { + return $orderLines; + } + $orderLines[] = [ 'type' => OrderLineType::TYPE_GIFT_CARD, 'name' => __('Magento Gift Card'), diff --git a/Service/Order/Lines/Generator/MagentoGiftWrapping.php b/Service/Order/Lines/Generator/MagentoGiftWrapping.php index e32412e1674..0abb9133249 100644 --- a/Service/Order/Lines/Generator/MagentoGiftWrapping.php +++ b/Service/Order/Lines/Generator/MagentoGiftWrapping.php @@ -38,6 +38,10 @@ public function process(OrderInterface $order, array $orderLines): array $extensionAttributes->getGwItemsBasePriceInclTax() : $extensionAttributes->getGwItemsPriceInclTax(); + if (abs($amount) < 0.01) { + return $orderLines; + } + $orderLines[] = [ 'type' => OrderLineType::TYPE_SURCHARGE, 'name' => __('Magento Gift Wrapping'), diff --git a/Service/Order/Lines/Generator/MirasvitRewards.php b/Service/Order/Lines/Generator/MirasvitRewards.php index d0c3684dd12..ed9dd421447 100644 --- a/Service/Order/Lines/Generator/MirasvitRewards.php +++ b/Service/Order/Lines/Generator/MirasvitRewards.php @@ -74,6 +74,10 @@ public function process(OrderInterface $order, array $orderLines): array $amount = $this->forceBaseCurrency ? $purchase->getBaseSpendAmount() : $purchase->getSpendAmount(); + if (abs($amount) < 0.01) { + return $orderLines; + } + $orderLines[] = [ 'type' => 'surcharge', 'name' => 'Mirasvit Rewards', diff --git a/Service/Order/Lines/Generator/ShippingDiscount.php b/Service/Order/Lines/Generator/ShippingDiscount.php index 03171803f67..658a6ad6949 100644 --- a/Service/Order/Lines/Generator/ShippingDiscount.php +++ b/Service/Order/Lines/Generator/ShippingDiscount.php @@ -29,6 +29,10 @@ public function process(OrderInterface $order, array $orderLines): array $currency = $forceBaseCurrency ? $order->getBaseCurrencyCode() : $order->getOrderCurrencyCode(); $amount = abs($order->getData(($forceBaseCurrency ? 'base_' : '') . 'shipping_discount_amount')); + if (abs($amount) < 0.01) { + return $orderLines; + } + $orderLines[] = [ 'type' => OrderLineType::TYPE_DISCOUNT, 'name' => __('Magento Discount'), diff --git a/Service/Order/Lines/Generator/WeeeFeeGenerator.php b/Service/Order/Lines/Generator/WeeeFeeGenerator.php index 55128fbe88b..d93b7f37245 100644 --- a/Service/Order/Lines/Generator/WeeeFeeGenerator.php +++ b/Service/Order/Lines/Generator/WeeeFeeGenerator.php @@ -66,6 +66,10 @@ private function getWeeeFeeOrderLine(OrderInterface $order): ?array $total += $this->getWeeeAmountForItem($item); } + if (abs($total) < 0.01) { + return null; + } + return [ 'type' => 'surcharge', 'name' => $this->getTitle($weeeItems), diff --git a/Service/Order/Lines/Order.php b/Service/Order/Lines/Order.php index fe6a6cd9a5d..592949fb22e 100644 --- a/Service/Order/Lines/Order.php +++ b/Service/Order/Lines/Order.php @@ -117,12 +117,13 @@ public function get(OrderInterface $order) $orderLines[] = $this->paymentFee->getOrderLine($order, $this->forceBaseCurrency); } + $orderLines = $this->orderLinesGenerator->execute($order, $orderLines); + + // The adjustment line should be the last one. This corrects any rounding issues. if ($adjustment = $this->getAdjustment($order, $orderLines)) { $orderLines[] = $adjustment; } - $orderLines = $this->orderLinesGenerator->execute($order, $orderLines); - $this->saveOrderLines($orderLines, $order); foreach ($orderLines as &$orderLine) { unset($orderLine['item_id']); @@ -174,7 +175,7 @@ private function getOrderLine(OrderItemInterface $item, $zeroPriceLine = false) $orderLine = [ 'item_id' => $item->getId(), 'type' => $item->getIsVirtual() !== null && (int) $item->getIsVirtual() !== 1 ? 'physical' : 'digital', - 'name' => preg_replace('/[^A-Za-z0-9 -]/', '', $item->getName() ?? ''), + 'name' => preg_replace('/[^\p{L}\p{N} -]/u', '', $item->getName() ?? ''), 'quantity' => round($item->getQtyOrdered()), 'unitPrice' => $this->mollieHelper->getAmountArray($this->currency, $unitPrice), 'totalAmount' => $this->mollieHelper->getAmountArray($this->currency, $totalAmount), @@ -344,7 +345,7 @@ private function getProductUrl(OrderItemInterface $item): ?string return $url; } - private function getAdjustment(OrderInterface $order, array $orderLines) + private function getAdjustment(OrderInterface $order, array $orderLines): ?array { $orderLinesTotal = 0; foreach ($orderLines as $orderLine) { @@ -358,22 +359,25 @@ private function getAdjustment(OrderInterface $order, array $orderLines) $max = $orderLinesTotal + 0.05; $min = $orderLinesTotal - 0.05; - if (($min <= $grandTotal) && ($grandTotal <= $max)) { - $difference = $grandTotal - $orderLinesTotal; - - return [ - 'item_id' => '', - 'type' => 'discount', - 'name' => 'Adjustment', - 'quantity' => 1, - 'unitPrice' => $this->mollieHelper->getAmountArray($this->currency, $difference), - 'totalAmount' => $this->mollieHelper->getAmountArray($this->currency, $difference), - 'vatRate' => sprintf("%.2f", 0), - 'vatAmount' => $this->mollieHelper->getAmountArray($this->currency, 0), - 'sku' => 'adjustment', - ]; + if ($grandTotal < $min || $grandTotal > $max) { + return null; } - return false; + $difference = $grandTotal - $orderLinesTotal; + if (abs($difference) < 0.01) { + return null; + } + + return [ + 'item_id' => '', + 'type' => 'discount', + 'name' => 'Adjustment', + 'quantity' => 1, + 'unitPrice' => $this->mollieHelper->getAmountArray($this->currency, $difference), + 'totalAmount' => $this->mollieHelper->getAmountArray($this->currency, $difference), + 'vatRate' => sprintf("%.2f", 0), + 'vatAmount' => $this->mollieHelper->getAmountArray($this->currency, 0), + 'sku' => 'adjustment', + ]; } } diff --git a/Service/Order/MethodCode.php b/Service/Order/MethodCode.php index 10b2b0b089f..30fc7a6f617 100644 --- a/Service/Order/MethodCode.php +++ b/Service/Order/MethodCode.php @@ -12,21 +12,37 @@ class MethodCode { + /** + * @var string + */ + private $expiresAtMethod = ''; + public function execute(OrderInterface $order): string { $method = $order->getPayment()->getMethodInstance()->getCode(); + $this->expiresAtMethod = $method; if ($method == 'mollie_methods_paymentlink') { return $this->paymentLinkMethod($order); } - if ($method == 'mollie_methods_paymentlink' || strstr($method, 'mollie_methods') === false) { + if (strstr($method, 'mollie_methods') === false) { return ''; } return str_replace('mollie_methods_', '', $method); } + /* + * From which method do we need to get the expires_at date? When a specific method is selected, we use that. + * When the payment link is used, we use the first limited method. When the payment link has multiple methods, + * we use the payment link settings to determine the expires_at date. + */ + public function getExpiresAtMethod(): string + { + return str_replace('mollie_methods_', '', $this->expiresAtMethod); + } + private function paymentLinkMethod(OrderInterface $order): string { $additionalInformation = $order->getPayment()->getAdditionalInformation(); @@ -34,10 +50,12 @@ private function paymentLinkMethod(OrderInterface $order): string return ''; } - if (count($additionalInformation['limited_methods']) !== 1) { + if (!is_array($additionalInformation['limited_methods']) || count($additionalInformation['limited_methods']) !== 1) { return ''; } + $this->expiresAtMethod = $additionalInformation['limited_methods'][0]; + return str_replace('mollie_methods_', '', $additionalInformation['limited_methods'][0]); } } diff --git a/Service/Order/OrderAmount.php b/Service/Order/OrderAmount.php index 5507e7c893b..b0c1f7863e7 100644 --- a/Service/Order/OrderAmount.php +++ b/Service/Order/OrderAmount.php @@ -58,7 +58,7 @@ public function getByTransactionId(string $transactionId): array $currencies = []; $orders = $this->getOrders($transactionId); foreach ($orders->getItems() as $order) { - if ($this->config->useBaseCurrency()) { + if ($this->config->useBaseCurrency($order->getStoreId())) { $currencies[] = $order->getBaseCurrencyCode(); $amount += $order->getBaseGrandTotal(); } else { diff --git a/Test/End-2-end/cypress.config.js b/Test/End-2-end/cypress.config.js index 59ed0e4e1b8..0ab4c850847 100644 --- a/Test/End-2-end/cypress.config.js +++ b/Test/End-2-end/cypress.config.js @@ -17,6 +17,11 @@ module.exports = defineConfig({ require('./cypress/plugins/index.js')(on, config); require('./cypress/plugins/disable-successful-videos.js')(on, config); + // If we're running in CI, we need to set the CI env variable + if (process.env.CI) { + config.env.CI = true + } + // Retrieve available method await new Promise((resolve, reject) => { var https = require('follow-redirects').https; @@ -27,7 +32,7 @@ module.exports = defineConfig({ const query = ` query { - molliePaymentMethods(input:{amount:100, currency:"EUR"}) { + molliePaymentMethods(input:{amount:50, currency:"EUR"}) { methods { code image @@ -38,42 +43,42 @@ module.exports = defineConfig({ `; var options = { - 'method': 'GET', - 'hostname': hostname, - 'path': '/graphql?query=' + encodeURIComponent(query), - 'headers': { - 'Content-Type': 'application/json', - // 'Cookie': 'XDEBUG_SESSION=PHPSTORM' - }, - 'maxRedirects': 20 + 'method': 'GET', + 'hostname': hostname, + 'path': '/graphql?query=' + encodeURIComponent(query), + 'headers': { + 'Content-Type': 'application/json', + // 'Cookie': 'XDEBUG_SESSION=PHPSTORM' + }, + 'maxRedirects': 20 }; console.log('Requesting Mollie payment methods from "' + baseUrl + '". One moment please...'); var req = https.request(options, function (res) { - var chunks = []; + var chunks = []; - res.on("data", function (chunk) { - chunks.push(chunk); - }); + res.on("data", function (chunk) { + chunks.push(chunk); + }); - res.on("end", function (chunk) { - const body = Buffer.concat(chunks); + res.on("end", function (chunk) { + const body = Buffer.concat(chunks); - const methods = JSON.parse(body.toString()).data.molliePaymentMethods.methods.map(data => { - return data.code - }) + const methods = JSON.parse(body.toString()).data.molliePaymentMethods.methods.map(data => { + return data.code + }) - config.env.mollie_available_methods = methods; + config.env.mollie_available_methods = methods; - console.log('Available Mollie payment methods: ', methods); + console.log('Available Mollie payment methods: ', methods); - resolve(config); - }); + resolve(config); + }); - res.on("error", function (error) { - console.error('Error while fetching Mollie Payment methods', error); - reject(error); - }); + res.on("error", function (error) { + console.error('Error while fetching Mollie Payment methods', error); + reject(error); + }); }); req.end(); diff --git a/Test/End-2-end/cypress/e2e/magento/applepay.cy.js b/Test/End-2-end/cypress/e2e/magento/applepay.cy.js new file mode 100644 index 00000000000..926601a5016 --- /dev/null +++ b/Test/End-2-end/cypress/e2e/magento/applepay.cy.js @@ -0,0 +1,14 @@ +/* + * Copyright Magmodules.eu. All rights reserved. + * See COPYING.txt for license details. + */ + +describe('Apple Pay', () => { + it('C2033291: Validate that the Apple Pay Develop Merchantid Domain Association file can be loaded', () => { + cy.request('/.well-known/apple-developer-merchantid-domain-association') + .then((response) => { + expect(response.body).to.satisfy(body => body.startsWith('7B2270737')); + expect(response.body.trim()).to.satisfy(body => body.endsWith('837303533303738636562626638326462306561376633303030303030303030303030227D')); + }); + }); +}); diff --git a/Test/End-2-end/cypress/e2e/magento/backend/paymentlink.cy.js b/Test/End-2-end/cypress/e2e/magento/backend/paymentlink.cy.js index 02d78ec919e..0904ec7fc4b 100644 --- a/Test/End-2-end/cypress/e2e/magento/backend/paymentlink.cy.js +++ b/Test/End-2-end/cypress/e2e/magento/backend/paymentlink.cy.js @@ -11,45 +11,46 @@ const ordersCreatePage = new OrdersCreatePage(); const cookies = new Cookies(); describe('Placing orders from the backend', () => { - // Skipped for now as it keeps failing on CI for unknown reasons. - it.skip('C895380: Validate that the ecommerce admin can submit an order in the backend and mark as "Paid" ', () => { - cy.backendLogin(); + // This fails in CI, but works locally. Not sure why. + if (!Cypress.env('CI')) { + it('C895380: Validate that the ecommerce admin can submit an order in the backend and mark as "Paid" ', () => { + cy.backendLogin(); - ordersCreatePage.createNewOrderFor('Veronica Costello'); + ordersCreatePage.createNewOrderFor('Veronica Costello'); - ordersCreatePage.addFirstSimpleProduct(); + ordersCreatePage.addFirstSimpleProduct(); - ordersCreatePage.selectShippingMethod('Fixed'); + ordersCreatePage.selectShippingMethod('Fixed'); - // 2.3.7 needs a double click to select the payment method, not sure why. - cy.get('[for="p_method_mollie_methods_paymentlink"]').click().click(); + // 2.3.7 needs a double click to select the payment method, not sure why. + cy.get('[for="p_method_mollie_methods_paymentlink"]').click().click(); - cy.get('#mollie_methods_paymentlink_methods').select([ - 'banktransfer', - 'creditcard', - 'ideal', - ]); + cy.get('#mollie_methods_paymentlink_methods').select([ + 'banktransfer', + 'creditcard', + 'ideal', + ]); - cookies.disableSameSiteCookieRestrictions(); + cookies.disableSameSiteCookieRestrictions(); - ordersCreatePage.submitOrder(); + ordersCreatePage.submitOrder(); - cy.get('.mollie-checkout-url .mollie-copy-url') - .invoke('attr', 'data-url') - .then(href => { - cy.visit(href); - }); + cy.get('.mollie-checkout-url .mollie-copy-url') + .invoke('attr', 'data-url') + .then(href => { + cy.visit(href); + }); - mollieHostedPaymentPage.selectPaymentMethod('iDEAL'); - mollieHostedPaymentPage.selectFirstIssuer(); - mollieHostedPaymentPage.selectStatus('paid'); + mollieHostedPaymentPage.selectPaymentMethod('Overboeking'); + mollieHostedPaymentPage.selectStatus('paid'); - checkoutSuccessPage.assertThatOrderSuccessPageIsShown(); + checkoutSuccessPage.assertThatOrderSuccessPageIsShown(); - cy.get('@order-id').then((orderId) => { - ordersPage.openOrderById(orderId); - }); + cy.get('@order-id').then((orderId) => { + ordersPage.openOrderById(orderId); + }); - ordersPage.assertOrderStatusIs('Processing'); - }); + ordersPage.assertOrderStatusIs('Processing'); + }); + } }); diff --git a/Test/End-2-end/cypress/e2e/magento/checkout.cy.js b/Test/End-2-end/cypress/e2e/magento/checkout.cy.js index 698bf5ddc57..ce40ad2d633 100644 --- a/Test/End-2-end/cypress/e2e/magento/checkout.cy.js +++ b/Test/End-2-end/cypress/e2e/magento/checkout.cy.js @@ -2,10 +2,16 @@ import CheckoutPaymentPage from "Pages/frontend/CheckoutPaymentPage"; import VisitCheckoutPaymentCompositeAction from "CompositeActions/VisitCheckoutPaymentCompositeAction"; import MollieHostedPaymentPage from "Pages/mollie/MollieHostedPaymentPage"; +import CheckoutSuccessPage from "Pages/frontend/CheckoutSuccessPage"; +import OrdersPage from "Pages/backend/OrdersPage"; +import Configuration from "Actions/backend/Configuration"; +const configuration = new Configuration(); const checkoutPaymentPage = new CheckoutPaymentPage(); const visitCheckoutPayment = new VisitCheckoutPaymentCompositeAction(); const mollieHostedPaymentPage = new MollieHostedPaymentPage(); +const checkoutSuccessPage = new CheckoutSuccessPage(); +const ordersPage = new OrdersPage(); describe('Checkout usage', () => { it('C849728: Validate that each payment methods have a specific CSS class', () => { @@ -24,7 +30,8 @@ describe('Checkout usage', () => { 'kbc', 'klarnapaylater', 'klarnapaynow', - 'paypal', + // TODO: Figure out why paypal fails + // 'paypal', 'przelewy24', 'sofort', ].forEach((method) => { @@ -61,4 +68,29 @@ describe('Checkout usage', () => { cy.url().should('include', '/checkout#payment'); }); + + it('C2183249: Validate that submitting an order with a discount works through the Orders API', () => { + configuration.setValue('Payment Methods', 'iDeal', 'Method', 'order'); + + visitCheckoutPayment.visit('NL', 1, 15); + + checkoutPaymentPage.selectPaymentMethod('iDeal'); + checkoutPaymentPage.selectFirstAvailableIssuer(); + + checkoutPaymentPage.enterCouponCode(); + + checkoutPaymentPage.placeOrder(); + + mollieHostedPaymentPage.selectStatus('paid'); + + checkoutSuccessPage.assertThatOrderSuccessPageIsShown(); + + cy.backendLogin(); + + cy.get('@order-id').then((orderId) => { + ordersPage.openOrderById(orderId); + }); + + cy.get('.mollie-checkout-type').should('contain', 'Order'); + }); }) diff --git a/Test/End-2-end/cypress/fixtures/swiss-shipping-address.json b/Test/End-2-end/cypress/fixtures/swiss-shipping-address.json new file mode 100644 index 00000000000..22a6dc3250b --- /dev/null +++ b/Test/End-2-end/cypress/fixtures/swiss-shipping-address.json @@ -0,0 +1,15 @@ +{ + "type": { + "username": "johnsmith@mollie.com", + "firstname": "John", + "lastname": "Smith", + "street[0]": "Bahnhofstrasse 10", + "city": "Zurich", + "postcode": "8001", + "telephone": "0441234567" + }, + "select": { + "country_id": "CH", + "region_id": "Zürich" + } +} diff --git a/Test/End-2-end/cypress/support/actions/backend/Configuration.js b/Test/End-2-end/cypress/support/actions/backend/Configuration.js index b731d60e4fb..402a54bfcba 100644 --- a/Test/End-2-end/cypress/support/actions/backend/Configuration.js +++ b/Test/End-2-end/cypress/support/actions/backend/Configuration.js @@ -21,7 +21,12 @@ export default class Configuration { } }); - cy.get('label').contains(field).parents('tr').find(':input').select(value, {force: true}); + cy.contains('.entry-edit-head', group).parents('.section-config').within(element => { + cy.contains('label', field) + .parents('tr') + .find(':input') + .select(value, {force: true}); + }) cy.get('#save').click(); diff --git a/Test/End-2-end/cypress/support/actions/composite/VisitCheckoutPaymentCompositeAction.js b/Test/End-2-end/cypress/support/actions/composite/VisitCheckoutPaymentCompositeAction.js index a887b532a90..10f2cc3ee7b 100644 --- a/Test/End-2-end/cypress/support/actions/composite/VisitCheckoutPaymentCompositeAction.js +++ b/Test/End-2-end/cypress/support/actions/composite/VisitCheckoutPaymentCompositeAction.js @@ -7,8 +7,8 @@ const checkoutPage = new CheckoutPage(); const checkoutShippingPage = new CheckoutShippingPage(); export default class VisitCheckoutPaymentCompositeAction { - visit(fixture = 'NL', quantity = 1) { - productPage.openProduct(Cypress.env('defaultProductId')); + visit(fixture = 'NL', quantity = 1, productId = Cypress.env('defaultProductId')) { + productPage.openProduct(productId); productPage.addSimpleProductToCart(quantity); @@ -50,4 +50,18 @@ export default class VisitCheckoutPaymentCompositeAction { checkoutShippingPage.fillShippingAddressUsingFixture(fixture); } + + changeStoreViewTo(name) { + cy.visit('/'); + + cy.get('.greet.welcome').should('be.visible'); + + cy.get('.switcher-trigger').then(($el) => { + if ($el.text().includes('Default Store View')) { + cy.get('#switcher-language-trigger .view-default').click(); + + cy.get('.switcher-dropdown').contains(name).click(); + } + }); + } } diff --git a/Test/End-2-end/cypress/support/pages/backend/OrdersCreatePage.js b/Test/End-2-end/cypress/support/pages/backend/OrdersCreatePage.js index 8c7d1ceafbe..984cd4664a1 100644 --- a/Test/End-2-end/cypress/support/pages/backend/OrdersCreatePage.js +++ b/Test/End-2-end/cypress/support/pages/backend/OrdersCreatePage.js @@ -6,12 +6,23 @@ export default class OrdersCreatePage { cy.contains('Create New Order').click(); - cy.contains(customerName).click(); + cy.get('#sales_order_create_customer_grid_table').contains(customerName).should('be.visible').click(); cy.wait('@header-block'); cy.get('.loader').should('not.exist'); + cy.get('.page-wrapper').then(element => { + cy.log(element.find('#order-store-selector')); + + if (element.find('#order-store-selector').is(':visible')) { + cy.get('.tree-store-scope') + .contains('Default Store View') + .should('be.visible') + .click(); + } + }) + cy.contains('Address Information').should('be.visible'); } diff --git a/Test/End-2-end/cypress/support/pages/frontend/CheckoutPaymentPage.js b/Test/End-2-end/cypress/support/pages/frontend/CheckoutPaymentPage.js index 327fae43d84..fb5a5ac875c 100644 --- a/Test/End-2-end/cypress/support/pages/frontend/CheckoutPaymentPage.js +++ b/Test/End-2-end/cypress/support/pages/frontend/CheckoutPaymentPage.js @@ -19,10 +19,18 @@ export default class CheckoutPaymentPage { cy.get('.payment-method._active .action.primary.checkout').click(); } + enterCouponCode(code = 'H20') { + cy.contains('Apply Discount Code').click(); + cy.get('[name=discount_code]').should('be.visible').type(code); + cy.get('.action.action-apply').click(); + + cy.get('.totals.discount').should('be.visible'); + } + placeOrder() { cy.intercept('mollie/checkout/redirect/paymentToken/*').as('mollieRedirect'); - cy.intercept('POST', 'rest/default/V1/guest-carts/*/payment-information').as('placeOrderAction'); + cy.intercept('POST', 'rest/*/V1/guest-carts/*/payment-information').as('placeOrderAction'); this.pressPlaceOrderButton(); diff --git a/Test/Integration/Controller/Checkout/PaymentLinkTest.php b/Test/Integration/Controller/Checkout/PaymentLinkTest.php new file mode 100644 index 00000000000..db11814da69 --- /dev/null +++ b/Test/Integration/Controller/Checkout/PaymentLinkTest.php @@ -0,0 +1,75 @@ +dispatch('mollie/checkout/paymentLink'); + + $this->assertSame(400, $this->getResponse()->getHttpResponseCode()); + } + + public function testThrowsErrorWhenDecodingIsEmpty(): void + { + $this->dispatch('mollie/checkout/paymentLink/order/999'); + + $this->assertSame(404, $this->getResponse()->getHttpResponseCode()); + } + + public function testThrowsErrorWhenOrderIsInvalid(): void + { + // OTk5 = an order id (999) but encrypted + $this->dispatch('mollie/checkout/paymentLink/order/OTk5'); + + $this->assertSame(404, $this->getResponse()->getHttpResponseCode()); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + */ + public function testRedirectsToMollieWhenTheInputIsValid(): void + { + $mollieMock = $this->createMock(Mollie::class); + $mollieMock->method('startTransaction')->willReturn('https://www.example.com'); + $this->_objectManager->addSharedInstance($mollieMock, Mollie::class); + + $order = $this->_objectManager->create(Order::class)->loadByIncrementId('100000001'); + $key = $this->_objectManager->get(EncryptorInterface::class)->encrypt($order->getId()); + + $this->dispatch('mollie/checkout/paymentLink/order/' . base64_encode($key)); + + $this->assertSame(302, $this->getResponse()->getHttpResponseCode()); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + */ + public function testRedirectsToTheHomepageWhenAlreadyPaid(): void + { + $order = $this->_objectManager->create(Order::class)->loadByIncrementId('100000001'); + $order->setState(Order::STATE_PROCESSING); + + $key = $this->_objectManager->get(EncryptorInterface::class)->encrypt($order->getId()); + + $this->dispatch('mollie/checkout/paymentLink/order/' . base64_encode($key)); + + $response = $this->getResponse(); + $this->assertSame(302, $response->getHttpResponseCode()); + $this->assertSame('/', $response->getHeader('Location')->getUri()); + } +} diff --git a/Test/Integration/Etc/Config/MethodsConfigurationTest.php b/Test/Integration/Etc/Config/MethodsConfigurationTest.php index 0293100d849..4c1307f9b1e 100644 --- a/Test/Integration/Etc/Config/MethodsConfigurationTest.php +++ b/Test/Integration/Etc/Config/MethodsConfigurationTest.php @@ -38,6 +38,7 @@ public function methods(): array ['mollie_methods_pointofsale'], ['mollie_methods_przelewy24'], ['mollie_methods_sofort'], + ['mollie_methods_twint'], ]; } diff --git a/Test/Integration/GraphQL/Resolver/General/MolliePaymentMethodsTest.php b/Test/Integration/GraphQL/Resolver/General/MolliePaymentMethodsTest.php index a3f6cdafcc7..44d1109f60e 100644 --- a/Test/Integration/GraphQL/Resolver/General/MolliePaymentMethodsTest.php +++ b/Test/Integration/GraphQL/Resolver/General/MolliePaymentMethodsTest.php @@ -150,8 +150,14 @@ private function callEndpoint($methods): array { $this->loadFakeEncryptor()->addReturnValue('', 'test_dummyapikeythatisvalidandislongenough'); + $methodCollection = new \Mollie\Api\Resources\MethodCollection(count($methods), null); + foreach ($methods as $method) { + $methodCollection[] = $method; + } + $methodsEndpointMock = $this->createMock(MethodEndpoint::class); - $methodsEndpointMock->method('allActive')->willReturn($methods); + $methodsEndpointMock->method('allActive')->willReturn($methodCollection); + $methodsEndpointMock->method('allAvailable')->willReturn($methodCollection); $mollieApiMock = $this->createMock(MollieApiClient::class); $mollieApiMock->methods = $methodsEndpointMock; diff --git a/Test/Integration/Helper/GeneralTest.php b/Test/Integration/Helper/GeneralTest.php index 061afb7048c..94ef84a9c4b 100644 --- a/Test/Integration/Helper/GeneralTest.php +++ b/Test/Integration/Helper/GeneralTest.php @@ -161,6 +161,7 @@ public function getMethodCodeDataProvider() 'pointofsale' => ['mollie_methods_pointofsale', 'pointofsale'], 'przelewy24' => ['mollie_methods_przelewy24', 'przelewy24'], 'sofort' => ['mollie_methods_sofort', 'sofort'], + 'twint' => ['mollie_methods_twint', 'twint'], ]; } diff --git a/Test/Integration/Model/Client/Orders/Processors/SuccessfulPaymentTest.php b/Test/Integration/Model/Client/Orders/Processors/SuccessfulPaymentTest.php index ba20d8e6cc1..d614f00a666 100644 --- a/Test/Integration/Model/Client/Orders/Processors/SuccessfulPaymentTest.php +++ b/Test/Integration/Model/Client/Orders/Processors/SuccessfulPaymentTest.php @@ -227,6 +227,38 @@ public function testCanceledOrderGetsUncanceled(): void ), 'We expect the order status to be "processing" or "complete".'); } + /** + * @magentoDataFixture Magento/Sales/_files/order.php + */ + public function testAddsChargebackCommentWhenApplicable(): void + { + $order = $this->loadOrder('100000001'); + $order->setBaseCurrencyCode('EUR'); + + /** @var MollieOrderBuilder $orderBuilder */ + $orderBuilder = $this->objectManager->create(MollieOrderBuilder::class); + $orderBuilder->setAmount(100); + $orderBuilder->addPayment('payment_001'); + $orderBuilder->setStatus(OrderStatus::STATUS_PAID); + $orderBuilder->addChargeback(100); + + /** @var SuccessfulPayment $instance */ + $instance = $this->objectManager->create(SuccessfulPayment::class); + + $historyCount = count($order->getStatusHistories()); + + $instance->process( + $order, + $orderBuilder->build(), + 'webhook', + $this->createResponse(false) + ); + + $freshOrder = $this->objectManager->get(OrderInterface::class)->load($order->getId(), 'entity_id'); + + $this->assertEquals($historyCount + 1, count($freshOrder->getStatusHistories())); + } + private function createResponse( bool $succes, string $status = 'paid', diff --git a/Test/Integration/Model/Client/OrdersTest.php b/Test/Integration/Model/Client/OrdersTest.php index 7d6542924b2..e21547388ce 100644 --- a/Test/Integration/Model/Client/OrdersTest.php +++ b/Test/Integration/Model/Client/OrdersTest.php @@ -154,31 +154,40 @@ protected function mollieOrderMock($status, $currency) * @magentoDataFixture Magento/Sales/_files/quote.php * @magentoDataFixture Magento/Sales/_files/order.php * @magentoConfigFixture default_store payment/mollie_methods_ideal/days_before_expire 5 + * @magentoConfigFixture default_store payment/mollie_methods_paymentlink/days_before_expire 6 * * @throws \Magento\Framework\Exception\LocalizedException * @throws \Mollie\Api\Exceptions\ApiException + * + * @dataProvider startTransactionIncludesTheExpiresAtParameterProvider */ - public function testStartTransactionIncludesTheExpiresAtParameter() - { + public function testStartTransactionIncludesTheExpiresAtParameter( + string $method, + int $days, + array $limitedMethods + ): void { $cart = $this->objectManager->create(Quote::class); $cart->load('test01', 'reserved_order_id'); $order = $this->loadOrder('100000001'); $order->setBaseCurrencyCode('EUR'); $order->setQuoteId($cart->getId()); - $order->getPayment()->setMethod('mollie_methods_ideal'); + $order->getPayment()->setMethod($method); + if ($limitedMethods) { + $order->getPayment()->setAdditionalInformation('limited_methods', $limitedMethods); + } $mollieOrderMock = $this->createMock(\Mollie\Api\Resources\Order::class); $mollieOrderMock->id = 'abc123'; $mollieApiMock = $this->createMock(MollieApiClient::class); $orderEndpointMock = $this->createMock(OrderEndpoint::class); - $orderEndpointMock->method('create')->with( $this->callback(function ($orderData) { + $orderEndpointMock->method('create')->with( $this->callback(function ($orderData) use ($days) { $this->assertArrayHasKey('expiresAt', $orderData); $this->assertNotEmpty($orderData['expiresAt']); $now = $this->objectManager->create(TimezoneInterface::class)->scopeDate(null); - $expected = $now->add(new \DateInterval('P5D')); + $expected = $now->add(new \DateInterval('P' . $days . 'D')); $this->assertEquals($expected->format('Y-m-d'), $orderData['expiresAt']); @@ -195,6 +204,18 @@ public function testStartTransactionIncludesTheExpiresAtParameter() $instance->startTransaction($order, $mollieApiMock); } + public function startTransactionIncludesTheExpiresAtParameterProvider(): array + { + return [ + 'ideal' => + ['mollie_methods_ideal', 5, []], + 'payment link with single method should use method' => + ['mollie_methods_paymentlink', 5, ['ideal']], + 'payment link with multiple methods should use payment link' => + ['mollie_methods_paymentlink', 6, ['ideal', 'creditcard']], + ]; + } + public function checksIfTheOrderHasAnUpdateProvider(): array { return [ diff --git a/Test/Integration/Model/Methods/TwintTest.php b/Test/Integration/Model/Methods/TwintTest.php new file mode 100644 index 00000000000..c21802245a7 --- /dev/null +++ b/Test/Integration/Model/Methods/TwintTest.php @@ -0,0 +1,12 @@ +assertArrayHasKey('mollie_methods_pointofsale', $result['payment']['image']); $this->assertArrayHasKey('mollie_methods_przelewy24', $result['payment']['image']); $this->assertArrayHasKey('mollie_methods_sofort', $result['payment']['image']); + $this->assertArrayHasKey('mollie_methods_twint', $result['payment']['image']); $this->assertArrayHasKey('mollie_methods_voucher', $result['payment']['image']); $this->assertEquals([], $result['payment']['issuers']['mollie_methods_ideal']); diff --git a/Test/Integration/MollieOrderBuilder.php b/Test/Integration/MollieOrderBuilder.php index b76d122d8fd..a4d86152269 100644 --- a/Test/Integration/MollieOrderBuilder.php +++ b/Test/Integration/MollieOrderBuilder.php @@ -68,6 +68,17 @@ public function setMethod(string $method): void $this->order->method = $method; } + public function addChargeback(float $value, string $current = 'EUR'): void + { + if (!isset($this->order->_embedded->payments)) { + $this->addPayment('chargeback'); + } + + $payment = $this->order->_embedded->payments[0]; + $payment->_links = new \StdClass(); + $payment->_links->chargebacks = new \StdClass(); + } + public function build(): Order { return $this->order; diff --git a/Test/Integration/Observer/CheckoutSubmitAllAfter/StartTransactionForPaymentLinkOrdersTest.php b/Test/Integration/Observer/CheckoutSubmitAllAfter/StartTransactionForPaymentLinkOrdersTest.php deleted file mode 100644 index 78fff6a7045..00000000000 --- a/Test/Integration/Observer/CheckoutSubmitAllAfter/StartTransactionForPaymentLinkOrdersTest.php +++ /dev/null @@ -1,61 +0,0 @@ -createMock(Mollie::class); - $mollieMock->expects($this->never())->method('startTransaction'); - - $order = $this->loadOrderById('100000001'); - $payment = $order->getPayment(); - $payment->setMethod('mollie_methods_ideal'); - - $observer = $this->objectManager->create(Observer::class); - $observer->setData('order', $order); - - /** @var StartTransactionForPaymentLinkOrders $instance */ - $instance = $this->objectManager->create(StartTransactionForPaymentLinkOrders::class, [ - 'mollie' => $mollieMock, - ]); - - $instance->execute($observer); - } - - /** - * @magentoDataFixture Magento/Sales/_files/order.php - */ - public function testStartTransactionForPaymentLinkOrders() - { - $mollieMock = $this->createMock(Mollie::class); - $mollieMock->expects($this->once())->method('startTransaction'); - - $order = $this->loadOrderById('100000001'); - $payment = $order->getPayment(); - $payment->setMethod('mollie_methods_paymentlink'); - - $observer = $this->objectManager->create(Observer::class); - $observer->setData('order', $order); - - /** @var StartTransactionForPaymentLinkOrders $instance */ - $instance = $this->objectManager->create(StartTransactionForPaymentLinkOrders::class, [ - 'mollie' => $mollieMock, - ]); - - $instance->execute($observer); - } -} diff --git a/Test/Integration/Service/Config/PaymentFeeTest.php b/Test/Integration/Service/Config/PaymentFeeTest.php index 406b7467f7b..400645f0426 100644 --- a/Test/Integration/Service/Config/PaymentFeeTest.php +++ b/Test/Integration/Service/Config/PaymentFeeTest.php @@ -46,6 +46,7 @@ public function isAvailableForMethodProvider() ['mollie_methods_pointofsale', true], ['mollie_methods_przelewy24', true], ['mollie_methods_sofort', true], + ['mollie_methods_twint', true], ['mollie_methods_voucher', true], ['not_relevant_payment_method', false], ]; diff --git a/Test/Integration/Service/Order/Lines/OrderTest.php b/Test/Integration/Service/Order/Lines/OrderTest.php index 6b0a8cd5ebc..bdcb12d0b3f 100644 --- a/Test/Integration/Service/Order/Lines/OrderTest.php +++ b/Test/Integration/Service/Order/Lines/OrderTest.php @@ -211,6 +211,29 @@ public function testAddsTheItemIdToTheMetadata(): void } } + public function testSupportsProductsWithVowelMutations(): void + { + $this->loadFixture('Magento/Sales/order_item_list.php'); + + $order = $this->loadOrderById('100000001'); + $order->setBaseCurrencyCode('EUR'); + + $products = $order->getItems(); + $product = array_shift($products); + $product->setName('Demö Produçt'); + + /** @var Subject $instance */ + $instance = $this->objectManager->get(Subject::class); + + $result = $instance->get($order); + + $result = array_filter($result, function ($line) { + return $line['name'] == 'Demö Produçt'; + }); + + $this->assertCount(1, $result); + } + public function adjustmentsDataProvider(): array { return [ diff --git a/Test/Integration/Service/Order/MethodCodeTest.php b/Test/Integration/Service/Order/MethodCodeTest.php index be04f8de50a..3443278e5de 100644 --- a/Test/Integration/Service/Order/MethodCodeTest.php +++ b/Test/Integration/Service/Order/MethodCodeTest.php @@ -71,4 +71,64 @@ public function testReturnsPaymentLinkReturnsTheSingleLimitedMethod(): void $this->assertEquals('ideal', $result); } + + public function testReturnsNothingWhenLimitedMethodsIsNull(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod('mollie_methods_paymentlink'); + $order->getPayment()->setAdditionalInformation( + 'limited_methods', + null + ); + + $instance = $this->objectManager->create(MethodCode::class); + + $result = $instance->execute($order); + + $this->assertEquals('', $result); + } + + public function testReturnsMethodAsExpiryMethod(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod('mollie_methods_ideal'); + + $instance = $this->objectManager->create(MethodCode::class); + + $instance->execute($order); + + $this->assertEquals('ideal', $instance->getExpiresAtMethod()); + } + + public function testReturnsPaymentLinkAsExpiryMethodWhenApplicable(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod('mollie_methods_paymentlink'); + $order->getPayment()->setAdditionalInformation( + 'limited_methods', + ['mollie_methods_ideal', 'mollie_methods_eps'] + ); + + $instance = $this->objectManager->create(MethodCode::class); + + $instance->execute($order); + + $this->assertEquals('paymentlink', $instance->getExpiresAtMethod()); + } + + public function testReturnsMethodWhenSingleLimitedMethod(): void + { + $order = $this->loadOrderById('100000001'); + $order->getPayment()->setMethod('mollie_methods_paymentlink'); + $order->getPayment()->setAdditionalInformation( + 'limited_methods', + ['mollie_methods_ideal'] + ); + + $instance = $this->objectManager->create(MethodCode::class); + + $instance->execute($order); + + $this->assertEquals('ideal', $instance->getExpiresAtMethod()); + } } diff --git a/Webapi/PaymentInformationMeta.php b/Webapi/PaymentInformationMeta.php new file mode 100644 index 00000000000..5f1e09faf27 --- /dev/null +++ b/Webapi/PaymentInformationMeta.php @@ -0,0 +1,124 @@ +methodMetaFactory = $methodMetaFactory; + $this->mollieApiClient = $mollieApiClient; + $this->paymentMethods = $paymentMethods; + $this->getIssuers = $getIssuers; + $this->pointofsale = $pointofsale; + $this->config = $config; + $this->issuerFactory = $issuerFactory; + $this->terminalFactory = $terminalFactory; + } + + public function getPaymentMethodsMeta(): array + { + $meta = []; + foreach ($this->paymentMethods->getCodes() as $code) { + $meta[$code] = $this->methodMetaFactory->create([ + 'code' => $code, + 'issuers' => $this->getIssuers($code), + 'terminals' => $this->getTerminals($code), + ]); + } + + return $meta; + } + + public function getIssuers(string $code): array + { + static $mollieApiClient; + + if (!$mollieApiClient) { + $mollieApiClient = $this->mollieApiClient->loadByStore(); + } + + $issuers = $this->getIssuers->execute($mollieApiClient, $code, 'list'); + if ($issuers === null) { + return []; + } + + return array_map(function (array $issuer) { + $issuer['images'] = $issuer['image']; + return $this->issuerFactory->create($issuer); + }, $issuers); + } + + private function getTerminals(string $code): array + { + if ($code != 'mollie_methods_pointofsale' || + !$this->config->isMethodActive('mollie_methods_pointofsale') + ) { + return []; + } + + return array_map(function (array $terminal) { + return $this->terminalFactory->create($terminal); + }, $this->pointofsale->getTerminals()); + } +} diff --git a/composer.json b/composer.json index 285aa072bf7..8e08fe4479b 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "mollie/magento2", "description": "Mollie Payment Module for Magento 2", - "version": "2.32.4", + "version": "2.33.0", "keywords": [ "mollie", "payment", @@ -34,6 +34,7 @@ "refunds", "sofort", "sofortbanking", + "twint", "voucher", "api", "payments", diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index 43c49dd54e7..904793d0a33 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -53,4 +53,10 @@ + + + + Magento\Framework\Url + + diff --git a/etc/adminhtml/methods.xml b/etc/adminhtml/methods.xml index f46580e3dab..68419f9501d 100644 --- a/etc/adminhtml/methods.xml +++ b/etc/adminhtml/methods.xml @@ -30,5 +30,6 @@ + diff --git a/etc/adminhtml/methods/twint.xml b/etc/adminhtml/methods/twint.xml new file mode 100644 index 00000000000..1b909991562 --- /dev/null +++ b/etc/adminhtml/methods/twint.xml @@ -0,0 +1,151 @@ + + + + + + + Magento\Config\Model\Config\Source\Yesno + payment/mollie_methods_twint/active + + + + payment/mollie_methods_twint/title + + 1 + + + + + Mollie\Payment\Model\Adminhtml\Source\Method + payment/mollie_methods_twint/method + + 1 + + here + to read more about the differences between the Payment and Orders API.]]> + + + + payment/mollie_methods_twint/payment_description + + + payment + 1 + + + + + validate-digits-range digits-range-1-365 + payment/mollie_methods_twint/days_before_expire + + 1 + order + + How many days before orders for this method becomes expired? Leave empty to use default expiration (28 days) + + + + Magento\Payment\Model\Config\Source\Allspecificcountries + payment/mollie_methods_twint/allowspecific + + 1 + + + + + Magento\Directory\Model\Config\Source\Country + 1 + payment/mollie_methods_twint/specificcountry + + 1 + + + + + payment/mollie_methods_twint/min_order_total + + 1 + + + + + payment/mollie_methods_twint/max_order_total + + 1 + + + + + payment/mollie_methods_twint/payment_surcharge_type + Mollie\Payment\Model\Adminhtml\Source\PaymentFeeType + + 1 + + + + + payment/mollie_methods_twint/payment_surcharge_fixed_amount + Mollie\Payment\Model\Adminhtml\Backend\VerifiyPaymentFee + validate-not-negative-number + + 1 + fixed_fee,fixed_fee_and_percentage + + + + + payment/mollie_methods_twint/payment_surcharge_percentage + Mollie\Payment\Model\Adminhtml\Backend\VerifiyPaymentFee + validate-number-range number-range-0-10 + + 1 + percentage,fixed_fee_and_percentage + + + + + payment/mollie_methods_twint/payment_surcharge_limit + + Mollie\Payment\Model\Adminhtml\Backend\VerifiyPaymentFee + validate-not-negative-number + + 1 + percentage,fixed_fee_and_percentage + + + + + payment/mollie_methods_twint/payment_surcharge_tax_class + \Magento\Tax\Model\TaxClass\Source\Product + + 1 + fixed_fee,percentage,fixed_fee_and_percentage + + + + + validate-number + payment/mollie_methods_twint/sort_order + + 1 + + + + diff --git a/etc/adminhtml/methods/voucher.xml b/etc/adminhtml/methods/voucher.xml index 18a3e961cac..a77444df96d 100644 --- a/etc/adminhtml/methods/voucher.xml +++ b/etc/adminhtml/methods/voucher.xml @@ -1,7 +1,7 @@ - - v2.32.4 + v2.33.0 0 0 test @@ -523,6 +523,25 @@ 0 1 + + 1 + Mollie\Payment\Model\Methods\Twint + TWINT + {ordernumber} + payment + order + 0 + + 1 + 1 + 1 + 1 + 0 + 1 + 0 + 0 + 1 + 1 Mollie\Payment\Model\Methods\Reorder diff --git a/etc/di.xml b/etc/di.xml index 4db948de342..6eca105e668 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -27,6 +27,11 @@ + + + + + Magento\Framework\App\ProductMetadataInterface\Proxy @@ -1531,6 +1536,51 @@ + + + + Magento\Payment\Block\Form + Mollie\Payment\Block\Info\Base + MollieTwintValueHandlerPool + MollieCommandPool + MollieTwintValidatorPool + + + + + + + MollieTwintConfigValueHandler + + + + + + + MollieTwintConfig + + + + + + Mollie\Payment\Model\Methods\Twint::CODE + + + + + + + MollieTwintCountryValidator + + + + + + + MollieTwintConfig + + + diff --git a/etc/events.xml b/etc/events.xml index 9ceb643617f..1b45d8f4f24 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -28,7 +28,6 @@ - diff --git a/etc/graphql/di.xml b/etc/graphql/di.xml index 29a3653fbaa..ffe57fbf363 100644 --- a/etc/graphql/di.xml +++ b/etc/graphql/di.xml @@ -27,6 +27,7 @@ Mollie\Payment\GraphQL\DataProvider Mollie\Payment\GraphQL\DataProvider Mollie\Payment\GraphQL\DataProvider + Mollie\Payment\GraphQL\DataProvider diff --git a/etc/payment.xml b/etc/payment.xml index 25c2b1a9109..58a34ee9f49 100644 --- a/etc/payment.xml +++ b/etc/payment.xml @@ -73,6 +73,9 @@ 0 + + 0 + 0 diff --git a/etc/schema.graphqls b/etc/schema.graphqls index 13a93cd5370..a128922032a 100644 --- a/etc/schema.graphqls +++ b/etc/schema.graphqls @@ -114,7 +114,7 @@ input MollieTransactionInput { input MolliePaymentMethodsInput { amount: Float! = 10 - currency: String! = EUR + currency: String = EUR } input MollieResetCartInput { diff --git a/etc/webapi.xml b/etc/webapi.xml index b74d22e7147..7566c3d399f 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -23,6 +23,13 @@ + + + + + + + diff --git a/i18n/de_DE.csv b/i18n/de_DE.csv index ffc04215f39..4659350dcea 100644 --- a/i18n/de_DE.csv +++ b/i18n/de_DE.csv @@ -223,6 +223,7 @@ "Point Of Sale (POS)","Point Of Sale (POS)" "Przelewy24","Przelewy24" "Sofort","Sofort" +"TWINT","TWINT" "Voucher","Gutschein" "Category","Kategorie" "Product attribute","Artikelattribut" @@ -392,3 +393,4 @@ ending,Ende "Encrypt payment details","Zahlungsdetails verschlüsseln" "Send an e-mail to customers with a failed or unfinished payment to give them a second chance on finishing the payment through the PaymentLink and revive their order.
You can either send these payment reminders manually or activate the e-mail fully automated.","Senden Sie eine E-Mail an Kunden mit einer fehlgeschlagenen oder unvollständigen Zahlung, um ihnen eine zweite Chance zu geben, die Zahlung über den PaymentLink abzuschließen und ihre Bestellung wiederzubeleben.
Sie können diese Zahlungserinnerungen entweder manuellsenden oder die E-Mail vollständig automatisiert aktivieren." "Payment Method To Use For Second Change Payments","Zahlungsmethode für Zahlungen bei zweiter Änderung verwenden" +"Your order has already been paid.","Ihre Bestellung wurde bereits bezahlt." diff --git a/i18n/en_US.csv b/i18n/en_US.csv index bf91ace9770..3da26903d4d 100644 --- a/i18n/en_US.csv +++ b/i18n/en_US.csv @@ -223,6 +223,7 @@ "Point Of Sale (POS)","Point Of Sale (POS)" "Przelewy24","Przelewy24" "Sofort","Sofort" +"TWINT","TWINT" "Voucher","Voucher" "Category","Category" "Product attribute","Product attribute" @@ -369,3 +370,4 @@ ending,ending "Encrypt payment details","Encrypt payment details" "Send an e-mail to customers with a failed or unfinished payment to give them a second chance on finishing the payment through the PaymentLink and revive their order.
You can either send these payment reminders manually or activate the e-mail fully automated.","Send an e-mail to customers with a failed or unfinished payment to give them a second chance on finishing the payment through the PaymentLink and revive their order.
You can either send these payment reminders manually or activate the e-mail fully automated." "Payment Method To Use For Second Change Payments","Payment Method To Use For Second Change Payments" +"Your order has already been paid.","Your order has already been paid." diff --git a/i18n/es_ES.csv b/i18n/es_ES.csv index b47786c5312..f6e00ae3a0a 100644 --- a/i18n/es_ES.csv +++ b/i18n/es_ES.csv @@ -223,6 +223,7 @@ "Point Of Sale (POS)","Point Of Sale (POS)" "Przelewy24","Przelewy24" "Sofort","Sofort" +"TWINT","TWINT" "Voucher","Vale" "Category","Categoría" "Product attribute","Atributo del producto" @@ -392,3 +393,4 @@ ending,finalizando "Encrypt payment details","Cifrar detalles de pago" "Send an e-mail to customers with a failed or unfinished payment to give them a second chance on finishing the payment through the PaymentLink and revive their order.
You can either send these payment reminders manually or activate the e-mail fully automated.","Envíe un correo electrónico a los clientes con un pago fallido o inconcluso para darles una segunda oportunidad de finalizar el pago a través del enlace de pago y revivir su pedido.
Puede enviar estos recordatorios de pago manualmente o activar el correo electrónico completamente automatizado." "Payment Method To Use For Second Change Payments","Método de pago para usar en pagos de segunda oportunidad" +"Your order has already been paid.","Su pedido ya ha sido pagado." diff --git a/i18n/fr_FR.csv b/i18n/fr_FR.csv index 4a68eba30eb..494000d05f3 100644 --- a/i18n/fr_FR.csv +++ b/i18n/fr_FR.csv @@ -223,6 +223,7 @@ "Point Of Sale (POS)","Point Of Sale (POS)" "Przelewy24","Przelewy24" "Sofort","Sofort" +"TWINT","TWINT" "Voucher","Voucher" "Category","Catégorie" "Product attribute","Attributs du produit" @@ -392,3 +393,4 @@ ending,fin "Encrypt payment details","Chiffrer les détails du paiement" "Send an e-mail to customers with a failed or unfinished payment to give them a second chance on finishing the payment through the PaymentLink and revive their order.
You can either send these payment reminders manually or activate the e-mail fully automated.","Envoyez un e-mail aux clients ayant échoué ou n'ayant pas terminé le paiement pour leur donner une seconde chance de finaliser le paiement via le PaymentLink et de relancer leur commande.
Vous pouvez envoyer ces rappels de paiement manuellement ou activer l'e-mail entièrement automatisé." "Payment Method To Use For Second Change Payments","Méthode de paiement à utiliser pour les paiements de seconde chance" +"Your order has already been paid.","Votre commande a déjà été payée." diff --git a/i18n/nl_NL.csv b/i18n/nl_NL.csv index 81ccf595230..55b1f8368c4 100644 --- a/i18n/nl_NL.csv +++ b/i18n/nl_NL.csv @@ -223,6 +223,7 @@ "Point Of Sale (POS)","Point Of Sale (POS)" "Przelewy24","Przelewy24" "Sofort","Sofort" +"TWINT","TWINT" "Voucher","Bon" "Category","Categorie" "Product attribute","Productkenmerk" @@ -394,3 +395,4 @@ ending,eindigend "Encrypt payment details","Betaalgegevens versleutelen" "Send an e-mail to customers with a failed or unfinished payment to give them a second chance on finishing the payment through the PaymentLink and revive their order.
You can either send these payment reminders manually or activate the e-mail fully automated.","Stuur een e-mail naar klanten met een mislukte of onvoltooide betaling om hen een tweede kans te geven de betaling te voltooien via de PaymentLink en hun bestelling te herstellen.
U kunt deze betalingsherinneringen handmatig verzenden of de e-mail volledig geautomatiseerd activeren." "Payment Method To Use For Second Change Payments","Betaalmethode te gebruiken voor tweede kans betalingen" +"Your order has already been paid.","Uw bestelling is al betaald." diff --git a/view/adminhtml/templates/form/mollie_paymentlink.phtml b/view/adminhtml/templates/form/mollie_paymentlink.phtml index fe94803e4b0..b65ea5baa92 100644 --- a/view/adminhtml/templates/form/mollie_paymentlink.phtml +++ b/view/adminhtml/templates/form/mollie_paymentlink.phtml @@ -38,29 +38,11 @@ $code; ?>" style="display:none"> +

escapeHtml(__('If only one method is chosen, the selection screen is skipped and the customer is sent directly to the payment method.')); ?>

- -
- -
- - - (i) - - - - -
-
diff --git a/view/adminhtml/templates/info/mollie_paymentlink.phtml b/view/adminhtml/templates/info/mollie_paymentlink.phtml index 25567ab3375..1b89946138b 100644 --- a/view/adminhtml/templates/info/mollie_paymentlink.phtml +++ b/view/adminhtml/templates/info/mollie_paymentlink.phtml @@ -23,12 +23,12 @@ $status = $block->getPaymentStatus(); getCheckoutType()); ?> - getCheckoutUrl() && $status == 'created'): ?> + - getCheckoutUrl(); ?> - + here to pay', $block->getPaymentLinkUrl()); ?> + diff --git a/view/adminhtml/web/images/twint.svg b/view/adminhtml/web/images/twint.svg new file mode 100644 index 00000000000..5b148147f18 --- /dev/null +++ b/view/adminhtml/web/images/twint.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/view/frontend/email/second_chance.html b/view/frontend/email/second_chance.html index 822cbe05f8e..7c814f15ff2 100644 --- a/view/frontend/email/second_chance.html +++ b/view/frontend/email/second_chance.html @@ -26,7 +26,7 @@ diff --git a/view/frontend/layout/checkout_index_index.xml b/view/frontend/layout/checkout_index_index.xml index 3c108332b73..d89ec3e7119 100644 --- a/view/frontend/layout/checkout_index_index.xml +++ b/view/frontend/layout/checkout_index_index.xml @@ -91,6 +91,9 @@ true + + true + true diff --git a/view/frontend/templates/info/mollie_paymentlink.phtml b/view/frontend/templates/info/mollie_paymentlink.phtml index aabaf34b560..3ee59cdfe51 100644 --- a/view/frontend/templates/info/mollie_paymentlink.phtml +++ b/view/frontend/templates/info/mollie_paymentlink.phtml @@ -17,12 +17,9 @@ $title = $block->escapeHtml($block->getMethod()->getTitle()); - getPaymentStatus(), ['created', 'open']) && - $paymentUrl = $block->getPaymentLink($block->getMethod()->getStore()) - ): ?> + getPaymentLink()): ?>
- +
diff --git a/view/frontend/web/images/methods/twint.svg b/view/frontend/web/images/methods/twint.svg new file mode 100644 index 00000000000..5b148147f18 --- /dev/null +++ b/view/frontend/web/images/methods/twint.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/view/frontend/web/js/view/payment/method-renderer.js b/view/frontend/web/js/view/payment/method-renderer.js index c4d767dee89..6c982ec556c 100644 --- a/view/frontend/web/js/view/payment/method-renderer.js +++ b/view/frontend/web/js/view/payment/method-renderer.js @@ -46,6 +46,7 @@ define( {type: 'mollie_methods_pointofsale', component: pointofsaleComponent}, {type: 'mollie_methods_przelewy24', component: defaultComponent}, {type: 'mollie_methods_sofort', component: defaultComponent}, + {type: 'mollie_methods_twint', component: defaultComponent}, {type: 'mollie_methods_voucher', component: defaultComponent} ];
- + {{trans "Click here to complete your payment" }}