arent_order->get_meta('_mollie_payment_mode', true); } if ($paymentMode === self::PAYMENT_TEST_MODE) { $result = true; break; } } return $result; } /** * @param WC_Order $renewal_order * @param $renewal_order_id * @param \Mollie\Api\Resources\Payment $payment * */ public function updateFirstPaymentMethodToRecurringPaymentMethod($renewal_order, $renewal_order_id, $payment) { // Update first payment method to actual recurring payment method used for renewal order, this is // for subscriptions where the first order used methods like iDEAL as first payment and // later renewal orders switch to SEPA Direct Debit. $methods_needing_update = [ 'mollie_wc_gateway_bancontact', 'mollie_wc_gateway_belfius', 'mollie_wc_gateway_eps', 'mollie_wc_gateway_giropay', 'mollie_wc_gateway_ideal', 'mollie_wc_gateway_kbc', 'mollie_wc_gateway_sofort', ]; $current_method = $renewal_order->get_meta('_payment_method', true); if (in_array($current_method, $methods_needing_update, true) && $payment->method === self::DIRECTDEBIT) { try { $renewal_order->set_payment_method('mollie_wc_gateway_directdebit'); $renewal_order->set_payment_method_title('SEPA Direct Debit'); $renewal_order->save(); } catch (\WC_Data_Exception $e) { $this->logger->debug('Updating payment method to SEPA Direct Debit failed for renewal order: ' . $renewal_order_id); } } } /** * @return mixed */ protected function getCurrentLocale() { return apply_filters('wpml_current_language', get_locale()); } /** * @param $order * @return mixed */ public function getOrderMollieCustomerId($order) { return $order->get_meta('_mollie_customer_id', true); } /** * @param $renewal_order * @param $initial_order_status * @param $payment */ protected function updateScheduledPaymentOrder($renewal_order, $initial_order_status, $payment) { $this->mollieOrderService->updateOrderStatus( $renewal_order, $initial_order_status, __('Awaiting payment confirmation.', 'mollie-payments-for-woocommerce') . "\n" ); $payment_method_title = $this->paymentMethod->getProperty('title'); $renewal_order->add_order_note(sprintf( /* translators: Placeholder 1: Payment method title, placeholder 2: payment ID */ __('%1$s payment started (%2$s).', 'mollie-payments-for-woocommerce'), $payment_method_title, $payment->id . ($payment->mode === 'test' ? (' - ' . __('test mode', 'mollie-payments-for-woocommerce')) : '') )); } /** * @param $resubscribe_order */ public function delete_resubscribe_meta($resubscribe_order) { $this->delete_renewal_meta($resubscribe_order); } /** * @param $renewal_order * @return mixed */ public function delete_renewal_meta($renewal_order) { $renewal_order->delete_meta_data('_mollie_payment_id'); $renewal_order->delete_meta_data('_mollie_cancelled_payment_id'); $renewal_order->save(); return $renewal_order; } /** * @param $payment_meta * @param $subscription * * @return mixed * @throws \Mollie\Api\Exceptions\ApiException */ public function add_subscription_payment_meta($payment_meta, $subscription) { $mollie_payment_id = $subscription->get_meta('_mollie_payment_id', true); $mollie_payment_mode = $subscription->get_meta('_mollie_payment_mode', true); $mollie_customer_id = $subscription->get_meta('_mollie_customer_id', true); $payment_meta[ $this->id ] = [ 'post_meta' => [ '_mollie_payment_id' => [ 'value' => $mollie_payment_id, 'label' => 'Mollie Payment ID', ], '_mollie_payment_mode' => [ 'value' => $mollie_payment_mode, 'label' => 'Mollie Payment Mode', ], '_mollie_customer_id' => [ 'value' => $mollie_customer_id, 'label' => 'Mollie Customer ID', ], ], ]; return $payment_meta; } /** * @param $payment_method_id * @param $payment_meta * @throws Exception */ public function validate_subscription_payment_meta($payment_method_id, $payment_meta) { if ($this->id === $payment_method_id) { // Check that a Mollie Customer ID is entered if (! isset($payment_meta['post_meta']['_mollie_customer_id']['value']) || empty($payment_meta['post_meta']['_mollie_customer_id']['value'])) { throw new Exception('A "_mollie_customer_id" value is required.'); } } } /** * @param $subscription * @param $renewal_order */ public function update_failing_payment_method($subscription, $renewal_order) { $subscription = wc_get_order($subscription->id); $subscription->update_meta_data('_mollie_customer_id', $renewal_order->mollie_customer_id); $subscription->update_meta_data('_mollie_payment_id', $renewal_order->mollie_payment_id); $subscription->save(); } /** * @param int $order_id * * @return array * @throws \Mollie\Api\Exceptions\ApiException * @throws InvalidApiKey */ public function process_payment($order_id) { $this->addWcSubscriptionsFiltersForPayment(); $isSubscription = $this->dataService->isSubscription($order_id); if ($isSubscription) { $this->paymentService->setGateway($this); $result = $this->process_subscription_payment($order_id); return $result; } return parent::process_payment($order_id); } protected function addWcSubscriptionsFiltersForPayment(): void { add_filter( $this->pluginId . '_is_subscription_payment', function ($isSubscription, $orderId) { if ($this->dataService->isWcSubscription($orderId)) { add_filter( $this->pluginId . '_is_automatic_payment_disabled', static function ($filteredOption) { if ( 'yes' == get_option( \WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments' ) ) { return true; } return $filteredOption; } ); return true; } return $isSubscription; }, 10, 2 ); } /** * @param $mollie_customer_id * @param $mollie_payment_id * @param $subscription * * @return string */ public function restore_mollie_customer_id_and_mandate($mollie_customer_id, $mollie_payment_id, $subscription) { try { // Get subscription ID $subscription_id = $subscription->get_id(); // Get full payment object from Mollie API $payment_object_resource = $this->paymentFactory->getPaymentObject($mollie_payment_id); // // If there is no known customer ID, try to get it from the API // if (empty($mollie_customer_id)) { $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: no valid customer ID found, trying to restore from Mollie API payment (' . $mollie_payment_id . ').'); // Try to get the customer ID from the payment object $mollie_customer_id = $payment_object_resource->getMollieCustomerIdFromPaymentObject($mollie_payment_id); if (empty($mollie_customer_id)) { $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: stopped processing, no customer ID found for this customer/payment combination.'); return $mollie_customer_id; } $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: customer ID (' . $mollie_customer_id . ') found, verifying status of customer and mandate(s).'); } // // Check for valid mandates // $apiKey = $this->dataService->getApiKey(); // Get the WooCommerce payment gateway for this subscription $gateway = wc_get_payment_gateway_by_order($subscription); if (! $gateway || ! ( $gateway instanceof MolliePaymentGateway )) { $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: stopped processing, not a Mollie payment gateway, could not restore customer ID.'); return $mollie_customer_id; } $mollie_method = $gateway->paymentMethod->getProperty('id'); // Check that first payment method is related to SEPA Direct Debit and update $methods_needing_update = [ 'bancontact', 'belfius', 'eps', 'giropay', 'ideal', 'kbc', 'sofort', ]; if (in_array($mollie_method, $methods_needing_update) != false) { $mollie_method = self::DIRECTDEBIT; } // Get all mandates for the customer $mandates = $this->apiHelper->getApiClient($apiKey)->customers->get($mollie_customer_id); // Check credit card payments and mandates if ($mollie_method === 'creditcard' && ! $mandates->hasValidMandateForMethod($mollie_method)) { $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: failed! No valid mandate for payment method ' . $mollie_method . ' found.'); return $mollie_customer_id; } // Get a Payment object from Mollie to check for paid status $payment_object = $payment_object_resource->getPaymentObject($mollie_payment_id); // Extra check that first payment was not sequenceType first $sequence_type = $payment_object_resource->getSequenceTypeFromPaymentObject($mollie_payment_id); // Check SEPA Direct Debit payments and mandates if ($mollie_method === self::DIRECTDEBIT && ! $mandates->hasValidMandateForMethod($mollie_method) && $payment_object->isPaid() && $sequence_type === 'oneoff') { $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: no valid mandate for payment method ' . $mollie_method . ' found, trying to create one.'); $options = $payment_object_resource->getMollieCustomerIbanDetailsFromPaymentObject($mollie_payment_id); // consumerName can be empty for Bancontact payments, in that case use the WooCommerce customer name if (empty($options['consumerName'])) { $billing_first_name = $subscription->get_billing_first_name(); $billing_last_name = $subscription->get_billing_last_name(); $options['consumerName'] = $billing_first_name . ' ' . $billing_last_name; } // Set method $options['method'] = $mollie_method; $customer = $this->apiHelper->getApiClient($apiKey)->customers->get($mollie_customer_id); $this->apiHelper->getApiClient($apiKey)->mandates->createFor($customer, $options); $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: mandate created successfully, customer restored.'); } else { $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: the subscription doesn\'t meet the conditions for a mandate restore.'); } return $mollie_customer_id; } catch (ApiException $e) { $this->logger->debug(__METHOD__ . ' - Subscription ' . $subscription_id . ' renewal payment: customer id and mandate restore failed. ' . $e->getMessage()); return $mollie_customer_id; } } /** * Check if the gateway is available in checkout * * @return bool */ public function is_available(): bool { if (!$this->checkEnabledNorDirectDebit()) { return false; } if (!$this->cartAmountAvailable()) { return true; } $status = parent::is_available(); // Do extra checks if WooCommerce Subscriptions is installed $orderTotal = $this->get_order_total(); return $this->subscriptionObject->isAvailableForSubscriptions($status, $this, $orderTotal); } /** * @param $subcriptionParentOrder * @return bool */ protected function initialPaymentUsedOrderAPI($subcriptionParentOrder): bool { if (!$subcriptionParentOrder) { return false; } $orderIdMeta = $subcriptionParentOrder->get_meta('_mollie_order_id'); $parentOrderMeta = $orderIdMeta ?: PaymentService::PAYMENT_METHOD_TYPE_PAYMENT; return strpos($parentOrderMeta, 'ord_') !== false; } /** * @param int $renewal_order_id * @param $customer_id * @param \Mollie\Api\MollieApiClient $mollieApiClient * @param $mandateId * @param bool $isRenewalMethodDirectDebit * @param $data * @param bool $validMandate * @return array * @throws ApiException */ protected function usePreviousMandate( int $renewal_order_id, $customer_id, \Mollie\Api\MollieApiClient $mollieApiClient, $mandateId, bool $isRenewalMethodDirectDebit, $data, bool $validMandate ): array { $this->logger->debug( $this->id . ': Found mandate ID for renewal order ' . $renewal_order_id . ' with customer ID ' . $customer_id ); $mandate = $mollieApiClient->customers->get($customer_id)->getMandate($mandateId); if ($mandate->status === 'valid') { $data['method'] = $mandate->method; $data['mandateId'] = $mandateId; $validMandate = true; } return [$mandate, $data, $validMandate]; } /** * @param int $renewal_order_id * @param $customer_id * @param \Mollie\Api\MollieApiClient $mollieApiClient * @param bool $validMandate * @param $data * @param $renewalOrderMethod * @return array * @throws ApiException */ protected function useAnyValidMandate( int $renewal_order_id, $customer_id, \Mollie\Api\MollieApiClient $mollieApiClient, bool $validMandate, $data, $renewalOrderMethod ): array { // Get all mandates for the customer ID $this->logger->debug( $this->id . ': Try to get all mandates for renewal order ' . $renewal_order_id . ' with customer ID ' . $customer_id ); $mandates = $mollieApiClient->customers->get($customer_id)->mandates(); foreach ($mandates as $mandate) { if ($mandate->status === 'valid') { $validMandate = true; $data['method'] = $mandate->method; if ($mandate->method === $renewalOrderMethod) { $data['method'] = $mandate->method; break; } } } return [$validMandate, $data]; } } woocommerce/product-meta', 'woocommerce/product-price', 'woocommerce/breadcrumbs' ); $found = false; foreach ( $parsed_blocks as $block ) { if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $single_product_template_blocks, true ) ) { $found = true; break; } $found = self::has_single_product_template_blocks( $block['innerBlocks'], $single_product_template_blocks ); if ( $found ) { break; } } return $found; } /** * Group blocks in this way: * B1 + TP1 + B2 + B3 + B4 + TP2 + B5 * (B = Block, TP = Template Part) * becomes: * [[B1], [TP1], [B2, B3, B4], [TP2], [B5]] * * @param array $parsed_blocks Array of parsed block objects. * @return array Array of blocks grouped by template part. */ private static function group_blocks( $parsed_blocks ) { return array_reduce( $parsed_blocks, function( $carry, $block ) { if ( 'core/template-part' === $block['blockName'] ) { $carry[] = array( $block ); return $carry; } $last_element_index = count( $carry ) - 1; if ( isset( $carry[ $last_element_index ][0]['blockName'] ) && 'core/template-part' !== $carry[ $last_element_index ][0]['blockName'] ) { $carry[ $last_element_index ][] = $block; return $carry; } $carry[] = array( $block ); return $carry; }, array() ); } /** * Inject the hooks after the div wrapper. * * @param string $block_content Block Content. * @param array $hooks Hooks to inject. * @return array */ private function inject_hooks_after_the_wrapper( $block_content, $hooks ) { $closing_tag_position = strpos( $block_content, '>' ); return substr_replace( $block_content, $this->get_hooks_buffer( $hooks, 'before' ), // Add 1 to the position to inject the content after the closing tag. $closing_tag_position + 1, 0 ); } /** * Plain custom HTML block is parsed as block with an empty blockName with a filled innerHTML. * * @param array $block Parse block. * @return bool */ private static function is_custom_html( $block ) { return empty( $block['blockName'] ) && ! empty( $block['innerHTML'] ); } /** * Serialize template. * * @param array $parsed_blocks Parsed blocks. * @return string */ private static function serialize_blocks( $parsed_blocks ) { return array_reduce( $parsed_blocks, function( $carry, $item ) { if ( is_array( $item ) ) { return $carry . serialize_blocks( $item ); } return $carry . serialize_block( $item ); }, '' ); } }