diff --git a/inc/utils.php b/inc/utils.php index bdd68c75..c4f0e504 100644 --- a/inc/utils.php +++ b/inc/utils.php @@ -236,3 +236,13 @@ function transformPhoneToNLFormat($phone) } return $phone; } + +function isMollieBirthValid($billing_birthdate) +{ + $today = new DateTime(); + $birthdate = DateTime::createFromFormat('Y-m-d', $billing_birthdate); + if ($birthdate >= $today) { + return false; + } + return true; +} diff --git a/inc/woocommerce.php b/inc/woocommerce.php index 3c5a00fc..27fe5b02 100644 --- a/inc/woocommerce.php +++ b/inc/woocommerce.php @@ -36,6 +36,9 @@ function is_order_received_page() */ function untrailingslashit($string) { + if ($string === null) { + return ''; + } return rtrim($string, '/'); } } diff --git a/mollie-payments-for-woocommerce.php b/mollie-payments-for-woocommerce.php index 7e9d56bd..d129381e 100644 --- a/mollie-payments-for-woocommerce.php +++ b/mollie-payments-for-woocommerce.php @@ -3,16 +3,16 @@ * Plugin Name: Mollie Payments for WooCommerce * Plugin URI: https://www.mollie.com * Description: Accept payments in WooCommerce with the official Mollie plugin - * Version: 7.6.0 + * Version: 7.7.0 * Author: Mollie * Author URI: https://www.mollie.com * Requires at least: 5.0 - * Tested up to: 6.5 + * Tested up to: 6.6 * Text Domain: mollie-payments-for-woocommerce * Domain Path: /languages * License: GPLv2 or later * WC requires at least: 3.9 - * WC tested up to: 9.0 + * WC tested up to: 9.1 * Requires PHP: 7.2 * Requires Plugins: woocommerce */ diff --git a/public/images/payconiq.svg b/public/images/payconiq.svg new file mode 100644 index 00000000..ce31ca99 --- /dev/null +++ b/public/images/payconiq.svg @@ -0,0 +1 @@ + diff --git a/public/images/riverty.svg b/public/images/riverty.svg new file mode 100644 index 00000000..b9e2a97e --- /dev/null +++ b/public/images/riverty.svg @@ -0,0 +1,4 @@ + diff --git a/readme.txt b/readme.txt index e7247028..65478312 100644 --- a/readme.txt +++ b/readme.txt @@ -2,8 +2,8 @@ Contributors: daanvm, danielhuesken, davdebcom, dinamiko, syde, l.vangunst, ndijkstra, robin-mollie, wido, carmen222, inpsyde-maticluznar Tags: mollie, payments, payment gateway, woocommerce, credit card, apple pay, ideal, bancontact, klarna, sofort, giropay, woocommerce subscriptions Requires at least: 3.8 -Tested up to: 6.5 -Stable tag: 7.5.5 +Tested up to: 6.6 +Stable tag: 7.7.0 Requires PHP: 7.2 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -221,6 +221,20 @@ Automatic updates should work like a charm; as always though, ensure you backup == Changelog == += 7.7.0 - 12-08-2024 = + +* Added - Payconiq payment method +* Added - Riverty payment method +* Fix - Declaring compatibility in WP Editor +* Security - Enhanced object reference security + += 7.6.0 - 10-07-2024 = + +* Added - Trustly payment method +* Deprecated - Giropay payment method ([Giropay Depreciation FAQ](https://help.mollie.com/hc/en-gb/articles/19745480480786-Giropay-Depreciation-FAQ)) +* Fixed - Mollie hooks into unrelated orders +* Fixed - Notices and type errors after 7.5.5 update +* Fixed - Rounding issues with products including tax = 7.5.5 - 18-06-2024 = * Feature Flag - Enable Bancomat Pay & Alma feature flag by default (official launch 2024-07-01) diff --git a/resources/js/blocks/molliePaymentMethod.js b/resources/js/blocks/molliePaymentMethod.js index 50990ecc..f890ce92 100644 --- a/resources/js/blocks/molliePaymentMethod.js +++ b/resources/js/blocks/molliePaymentMethod.js @@ -197,17 +197,17 @@ const MollieComponent = (props) => { }, [activePaymentMethod, onCheckoutValidation, billing.billingData, shippingData.shippingAddress, item, phoneString, inputBirthdate, inputPhone]); onSubmitLocal = onSubmit - const updateIssuer = ( changeEvent ) => { - selectIssuer( changeEvent.target.value ) - }; - const updateCompany = ( changeEvent ) => { - selectCompany( changeEvent.target.value ) - }; - const updatePhone = ( changeEvent ) => { - selectPhone( changeEvent.target.value ) - } - const updateBirthdate = ( changeEvent ) => { - selectBirthdate( changeEvent.target.value ) + const updateIssuer = (e) => selectIssuer(e.target.value); + const updateCompany = (e) => selectCompany(e.target.value); + const updatePhone = (e) => selectPhone(e.target.value); + const updateBirthdate = (e) => selectBirthdate( e.target.value ); + + function fieldMarkup(id, fieldType, label, action, value, placeholder = null) { + const className = "wc-block-components-text-input wc-block-components-address-form__" + id; + return
{item.content}
{item.content}
{item.content}
{item.content}
+ + + +
+ customer->get_billing_country(); + $countryCodes = [ + 'BE' => '+32xxxxxxxxx', + 'NL' => '+316xxxxxxxx', + 'DE' => '+49xxxxxxxxx', + 'AT' => '+43xxxxxxxxx', + ]; + $placeholder = in_array($country, array_keys($countryCodes)) ? $countryCodes[$country] : $countryCodes['NL']; + ?> ++ + + + +
+ 'riverty', + 'defaultTitle' => __('Riverty', 'mollie-payments-for-woocommerce'), + 'settingsDescription' => __( + 'To accept payments via Riverty, all default WooCommerce checkout fields should be enabled and required.', + 'mollie-payments-for-woocommerce' + ), + 'defaultDescription' => '', + 'paymentFields' => true, + 'additionalFields' => ['birthdate', 'phone'], + 'instructions' => false, + 'supports' => [ + 'products', + 'refunds', + ], + 'filtersOnBuild' => true, + 'confirmationDelayed' => false, + 'SEPA' => false, + 'orderMandatory' => true, + 'phonePlaceholder' => __('Please enter your phone here. +316xxxxxxxx', 'mollie-payments-for-woocommerce'), + 'birthdatePlaceholder' => __('Please enter your birthdate here.', 'mollie-payments-for-woocommerce'), + ]; + } + + public function getFormFields($generalFormFields): array + { + return $generalFormFields; + } + + public function filtersOnBuild() + { + add_action( + 'woocommerce_checkout_posted_data', + [$this, 'switchFields'], + 11 + ); + add_action('woocommerce_rest_checkout_process_payment_with_context', [$this, 'addPhoneWhenRest'], 11); + add_action('woocommerce_rest_checkout_process_payment_with_context', [$this, 'addBirthdateWhenRest'], 11); + add_action('woocommerce_before_pay_action', [$this, 'fieldsMandatoryPayForOrder'], 11); + } + + /** + * @param $order + */ + public function fieldsMandatoryPayForOrder($order) + { + $paymentMethod = filter_input(INPUT_POST, 'payment_method', FILTER_SANITIZE_SPECIAL_CHARS) ?? false; + + if ($paymentMethod !== 'mollie_wc_gateway_riverty') { + return; + } + + $phoneValue = filter_input(INPUT_POST, 'billing_phone_riverty', FILTER_SANITIZE_SPECIAL_CHARS) ?? false; + $phoneValid = $phoneValue || null; + + if ($phoneValid) { + $order->set_billing_phone($phoneValue); + } + } + + public function switchFields($data) + { + if (isset($data['payment_method']) && $data['payment_method'] === 'mollie_wc_gateway_riverty') { + $fieldName = 'billing_phone_' . $this->getConfig()['id']; + $fieldPosted = filter_input(INPUT_POST, $fieldName, FILTER_SANITIZE_SPECIAL_CHARS) ?? false; + if (!empty($fieldPosted)) { + $data['billing_phone'] = $fieldPosted; + } + } + return $data; + } + + public function addPhoneWhenRest($arrayContext) + { + $context = $arrayContext; + $phoneMandatoryGateways = ['mollie_wc_gateway_riverty']; + $paymentMethod = $context->payment_data['payment_method'] ?? null; + if ($paymentMethod && in_array($paymentMethod, $phoneMandatoryGateways)) { + $billingPhone = $context->order->get_billing_phone(); + if (!empty($billingPhone)) { + return; + } + + $billingPhone = $context->payment_data['billing_phone'] ?? null; + if ($billingPhone) { + $context->order->set_billing_phone($billingPhone); + $context->order->save(); + } + } + } + + /** + * @throws RouteException + */ + public function addBirthdateWhenRest($context) + { + $paymentMethod = $context->payment_data['payment_method'] ?? null; + if ($paymentMethod) { + $billingBirthdate = $context->payment_data['billing_birthdate'] ?? null; + if ($billingBirthdate && $this->isBirthValid($billingBirthdate)) { + $context->order->update_meta_data('billing_birthdate', $billingBirthdate); + $context->order->save(); + } + } + } + + private function isBirthValid($billing_birthdate): bool + { + return isMollieBirthValid($billing_birthdate); + } +} diff --git a/src/Shared/SharedDataDictionary.php b/src/Shared/SharedDataDictionary.php index 3519fdde..10cf5c0e 100644 --- a/src/Shared/SharedDataDictionary.php +++ b/src/Shared/SharedDataDictionary.php @@ -36,6 +36,8 @@ class SharedDataDictionary 'Mollie_WC_Gateway_Bancomatpay', 'Mollie_WC_Gateway_Alma', 'Mollie_WC_Gateway_Trustly', + 'Mollie_WC_Gateway_Payconiq', + 'Mollie_WC_Gateway_Riverty', ]; public const MOLLIE_OPTIONS_NAMES = [ diff --git a/tests/php/Functional/ApplePayButton/DataToAppleButtonScriptsTest.php b/tests/php/Functional/ApplePayButton/DataToAppleButtonScriptsTest.php index 0638a28d..1d4c79ce 100644 --- a/tests/php/Functional/ApplePayButton/DataToAppleButtonScriptsTest.php +++ b/tests/php/Functional/ApplePayButton/DataToAppleButtonScriptsTest.php @@ -5,6 +5,7 @@ use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mollie\WooCommerce\Buttons\ApplePayButton\DataToAppleButtonScripts; use Mollie\WooCommerceTests\Stubs\postDTOTestsStubs; +use Mollie\WooCommerceTests\Stubs\WC_Product; use Mollie\WooCommerceTests\TestCase; use function Brain\Monkey\Functions\stubs; @@ -119,7 +120,7 @@ public function testApplePayScriptDataOnCart() private function wcProduct() { $item = $this->createConfiguredMock( - 'WC_Product', + WC_Product::class, [ 'get_price' => '1', 'get_type' => 'simple', diff --git a/tests/php/Functional/HelperMocks.php b/tests/php/Functional/HelperMocks.php index 61a80371..bb264c1d 100644 --- a/tests/php/Functional/HelperMocks.php +++ b/tests/php/Functional/HelperMocks.php @@ -10,6 +10,7 @@ use Mollie\WooCommerce\Payment\MollieOrder; use Mollie\WooCommerce\Payment\MollieOrderService; use Mollie\WooCommerce\Payment\OrderInstructionsService; +use Mollie\WooCommerce\Payment\OrderItemsRefunder; use Mollie\WooCommerce\Payment\OrderLines; use Mollie\WooCommerce\Payment\MollieObject; use Mollie\WooCommerce\Payment\PaymentFactory; @@ -100,6 +101,13 @@ public function mollieOrderService() ] ); } + + public function orderItemsRefunder() + { + return $this->getMockBuilder(OrderItemsRefunder::class) + ->disableOriginalConstructor() + ->getMock(); + } public function orderLines($apiClientMock){ return new OrderLines( $this->dataHelper($apiClientMock), diff --git a/tests/php/Functional/PayPalButton/DataToPayPalButtonScriptsTest.php b/tests/php/Functional/PayPalButton/DataToPayPalButtonScriptsTest.php index 44557a86..961c4c48 100644 --- a/tests/php/Functional/PayPalButton/DataToPayPalButtonScriptsTest.php +++ b/tests/php/Functional/PayPalButton/DataToPayPalButtonScriptsTest.php @@ -3,18 +3,13 @@ namespace Mollie\WooCommerceTests\Functional\PayPalButton; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; -use Mollie\Api\Endpoints\OrderEndpoint; -use Mollie\Api\Endpoints\WalletEndpoint; -use Mollie\Api\MollieApiClient; use Mollie\WooCommerce\Buttons\PayPalButton\DataToPayPal; use Mollie\WooCommerceTests\Stubs\postDTOTestsStubs; +use Mollie\WooCommerceTests\Stubs\WC_Product; use Mollie\WooCommerceTests\TestCase; use Mollie_WC_ApplePayButton_DataToAppleButtonScripts; -use Mollie_WC_Helper_Api; use Mollie_WC_ApplePayButton_DataObjectHttp; use Mollie_WC_Helper_ApplePayDirectHandler; -use Mollie_WC_Helper_Data; -use Mollie_WC_Payment_RefundLineItemsBuilder; use Mollie_WC_PayPalButton_DataToPayPalScripts; use PHPUnit_Framework_Exception; use PHPUnit_Framework_MockObject_MockObject; @@ -120,7 +115,7 @@ public function testApplePayScriptDataOnCart() private function wcProduct() { return $this->createConfiguredMock( - 'WC_Product', + WC_Product::class, [ 'get_price' => '1', 'get_type' => 'simple', diff --git a/tests/php/Functional/Payment/PaymentServiceTest.php b/tests/php/Functional/Payment/PaymentServiceTest.php index acf34319..f8107c8e 100644 --- a/tests/php/Functional/Payment/PaymentServiceTest.php +++ b/tests/php/Functional/Payment/PaymentServiceTest.php @@ -13,6 +13,7 @@ use Mollie\WooCommerceTests\Functional\HelperMocks; use Mollie\WooCommerceTests\Stubs\WC_Order_Item_Product; use Mollie\WooCommerceTests\Stubs\WC_Settings_API; +use Mollie\WooCommerceTests\Stubs\WC_Product; use Mollie\WooCommerceTests\TestCase; @@ -263,7 +264,7 @@ private function wcProduct() { $item = $this->createConfiguredMock( - 'WC_Product', + WC_Product::class, [ 'get_price' => '1', 'get_id'=>'1', diff --git a/tests/php/Functional/Payment/RequestObjectTest.php b/tests/php/Functional/Payment/RequestObjectTest.php new file mode 100644 index 00000000..221b1698 --- /dev/null +++ b/tests/php/Functional/Payment/RequestObjectTest.php @@ -0,0 +1,259 @@ + [ + 'id' => 1, + 'name' => 'product 1', + 'price' => '11.123' + ], + '2' => [ + 'id' => 2, + 'name' => 'product 2', + 'price' => '9.00' + ], + '3' => [ + 'id' => 3, + 'name' => 'product 3', + 'price' => '26.1234' + ], + '4' => [ + 'id' => 4, + 'name' => 'product 4', + 'price' => '30.01' + ] + ]; + + public function __construct($name = null, array $data = [], $dataName = '') + { + parent::__construct($name, $data, $dataName); + $this->helperMocks = new HelperMocks(); + } + + /** + * GIVEN A PAYMENT REQUEST + * WHEN THE TOTAL AMOUNT HAS DECIMALS + * THEN THE TOTAL AMOUNT HAS TO BE EQUAL TO THE SUM OF THE LINES + * + * + * @test + */ + public function processPayment_decimals_and_taxes_request_no_missmatch() + { + $testDataSet = $this->generateTestDataSet(); + + foreach ($testDataSet as $order) { + $this->executeTest($order); + } + } + + private function generateTestDataSet(): array + { + $products = $this->products; + return [ + $this->wcOrder('76.25', [ + $this->wcOrderItem($products['1']['id'], $products['1']['name'], $products['1']['price'], 1), + $this->wcOrderItem($products['2']['id'], $products['2']['name'], $products['2']['price'], 1), + $this->wcOrderItem($products['3']['id'], $products['3']['name'], $products['3']['price'], 1), + $this->wcOrderItem($products['4']['id'], $products['4']['name'], $products['4']['price'], 1) + ]), + $this->wcOrder('46.2464', [ + $this->wcOrderItem($products['1']['id'], $products['1']['name'], $products['1']['price'], 1), + $this->wcOrderItem($products['2']['id'], $products['2']['name'], $products['2']['price'], 1), + $this->wcOrderItem($products['3']['id'], $products['3']['name'], $products['3']['price'], 1) + ]), + $this->wcOrder('20.1234', [ + $this->wcOrderItem($products['1']['id'], $products['1']['name'], $products['1']['price'], 1), + $this->wcOrderItem($products['2']['id'], $products['2']['name'], $products['2']['price'], 1) + ]), + $this->wcOrder('11.123', [ + $this->wcOrderItem($products['1']['id'], $products['1']['name'], $products['1']['price'], 1) + ]) + ]; + } + + public function executeTest($order) + { + $customerId = 1; + $wrapperMock = $this->createMock(WC_Product::class); + + $callback = function ($productId) { + $price = $this->products[$productId]['price']; + $productMock = $this->wcProduct($productId, $price); + + return $productMock; + }; + $wrapperMock->method('getProduct')->willReturnCallback($callback); + + stubs([ + 'wc_get_payment_gateway_by_order' => $this->mollieGateway( + 'ideal', + $this->helperMocks->paymentService() + ), + 'add_query_arg' => 'https://webshop.example.org/wc-api/mollie_return?order_id=1&key=wc_order_hxZniP1zDcnM8', + 'WC' => $this->wooCommerce(), + 'get_option' => ['enabled' => false], + 'wc_get_product' => $wrapperMock, + 'wc_clean' => false + ]); + $apiClientMock = $this->createConfiguredMock( + MollieApiClient::class, + [] + ); + $orderLines = new OrderLines($this->helperMocks->dataHelper($apiClientMock), $this->helperMocks->pluginId()); + $testee = new MollieOrder( + $this->helperMocks->orderItemsRefunder(), + 'order', + $this->helperMocks->pluginId(), + $this->helperMocks->apiHelper($apiClientMock), + $this->helperMocks->settingsHelper(), + $this->helperMocks->dataHelper($apiClientMock), + $this->helperMocks->loggerMock(), + $orderLines + ); + + /* + * Execute Test + */ + + $arrayResult = $testee->getPaymentRequestData($order, $customerId); + $expectedResult = $this->noMissmatchError($arrayResult); + $this->assertTrue($expectedResult); + } + + + protected function setUp(): void + { + $_POST = []; + parent::setUp(); + + when('__')->returnArg(1); + } + + protected function mollieGateway($paymentMethodName, $testee, $isSepa = false, $isSubscription = false) + { + return $this->helperMocks->mollieGatewayBuilder($paymentMethodName, $isSepa, $isSubscription, [], $testee); + } + + + /** + * + * @param $total + * @param $items + * @return (object&MockObject)|MockObject|\WC_Order|(\WC_Order&object&MockObject)|(\WC_Order&MockObject) + */ + private function wcOrder($total, $items) + { + $item = $this->createConfiguredMock( + 'WC_Order', + [ + 'get_id' => 1, + 'get_order_key' => 'wc_order_hxZniP1zDcnM8', + 'get_total' => $total, + 'get_items' => $items, + 'get_billing_first_name' => 'billingggivenName', + 'get_billing_last_name' => 'billingfamilyName', + 'get_billing_email' => 'billingemail', + 'get_shipping_first_name' => 'shippinggivenName', + 'get_shipping_last_name' => 'shippingfamilyName', + 'get_billing_address_1' => 'shippingstreetAndNumber', + 'get_billing_address_2' => 'billingstreetAdditional', + 'get_billing_postcode' => 'billingpostalCode', + 'get_billing_city' => 'billingcity', + 'get_billing_state' => 'billingregion', + 'get_billing_country' => 'billingcountry', + 'get_billing_phone' => '+34345678900', + 'get_billing_company' => 'billingorganizationName', + 'get_shipping_address_1' => 'shippingstreetAndNumber', + 'get_shipping_address_2' => 'shippingstreetAdditional', + 'get_shipping_postcode' => 'shippingpostalCode', + 'get_shipping_city' => 'shippingcity', + 'get_shipping_state' => 'shippingregion', + 'get_shipping_country' => 'shippingcountry', + 'get_shipping_methods' => false, + 'get_order_number' => 1, + 'get_payment_method' => 'mollie_wc_gateway_ideal', + 'get_currency' => 'EUR', + ] + ); + + return $item; + } + + /** + * + * @return PHPUnit_Framework_MockObject_MockObject + * @throws PHPUnit_Framework_Exception + */ + public function wooCommerce() + { + $item = $this->createConfiguredMock( + 'WooCommerce', + [ + 'api_request_url' => 'https://webshop.example.org/wc-api/mollie_return' + ] + ); + + return $item; + } + + /** + * + * @return PHPUnit_Framework_MockObject_MockObject + * @throws PHPUnit_Framework_Exception + */ + private function wcOrderItem($id, $name, $price, $quantity) + { + $item = new \WC_Order_Item_Product(); + + $item['quantity'] = $quantity; + $item['variation_id'] = null; + $item['product_id'] = $id; + $item['line_subtotal_tax'] = 0; + $item['line_total'] = $price; + $item['line_subtotal'] = $price; + $item['line_tax'] = 0; + $item['tax_status'] = ''; + $item['total'] = $price; + $item['name'] = $name; + + return $item; + } + + private function noMissmatchError(array $arrayResult) + { + //array result total equals the sum of the lines + $total = ($arrayResult['amount']['value']) * 1000; + $lines = $arrayResult['lines']; + $sum = 0.0; + foreach ($lines as $line) { + $lineValue = ($line['totalAmount']['value']) * 1000; + $sum += $lineValue; + } + return $total == $sum; + } +} + + + diff --git a/tests/php/Stubs/WC_Product.php b/tests/php/Stubs/WC_Product.php new file mode 100644 index 00000000..c36dbde7 --- /dev/null +++ b/tests/php/Stubs/WC_Product.php @@ -0,0 +1,44 @@ +