id = 'oney_x3_with_fees'; $this->method_title = _x('PayPlug Oney 3x', 'Gateway method title', 'payplug'); $this->method_description = __('Enable PayPlug Oney 3x for your customers.', 'payplug'); $this->title = __('Pay by card in 3x with Oney', 'payplug'); $this->has_fields = true; add_action('woocommerce_order_item_add_action_buttons', [$this, 'oney_refund_text']); add_action('woocommerce_after_checkout_validation', [$this, 'validate_checkout'], 10); self::set_oney_configuration(); // Unlike the other sub-gateways (Bancontact, AmericanExpress, PPRO methods...), Oney never // synced $this->enabled with the plugin's own "active" toggle, so disabling Oney from the // Payplug config left it shown as enabled on WooCommerce's native Payments settings page. if (!$this->checkGateway()) { $this->enabled = 'no'; } if (is_checkout()) { if ($this->check_oney_is_available() === self::ONEY_DISALBE_CHECKOUT_OPTIONS) { $this->enabled = 'no'; } } } public function validate_checkout(): void { $posted_data = $this->get_post_data(); // Was a single malformed string (missing quotes around each id) instead of a 4-element // array, so this guard never matched and the whole method was a silent no-op (PRE-3457 // review / PRE-3457 follow-up fix). $oney_gateway_ids = ['oney_x3_with_fees', 'oney_x4_with_fees', 'oney_x3_without_fees', 'oney_x4_without_fees']; if (in_array($posted_data['payment_method'] ?? '', $oney_gateway_ids, true)) { if ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED) { throw new \Exception(__('Unavailable for the specified country.')); } elseif ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH) { throw new \Exception(sprintf(__('The payment with Oney is unavailable because you have more than %s items in your cart.', 'payplug'), self::ONEY_PRODUCT_QUANTITY_MAXIMUM)); } elseif (!$this->check_oney_is_available()) { throw new \Exception(sprintf(__('The total amount of your order should be between %s€ and %s€ to pay with Oney.', 'payplug'), $this->oney_thresholds_min, $this->oney_thresholds_max)); } } } /** * Set oney settings * * @return void */ private function set_oney_configuration(): void { $account = PayplugWoocommerceHelper::get_account_data_from_options(); if ($account) { $oney_configuration = $account['configuration']['oney']; $this->min_oney_price = $oney_configuration['min_amounts']['EUR'] / 100; $this->max_oney_price = $oney_configuration['max_amounts']['EUR'] / 100; $this->allowed_country_codes = $oney_configuration['allowed_countries']; } } /** * Get payment icons. * * @return string */ public function get_icon() { $disable = $this->check_oney_is_available() === true ? '' : 'disable-checkout-icons'; $available_img = 'x3_with_fees.svg'; $icons = apply_filters('payplug_payment_icons', [ 'payplug' => sprintf('Oney 3x', esc_url(PAYPLUG_GATEWAY_PLUGIN_URL . '/assets/images/checkout/' . $available_img)), ]); $icons_str = ''; foreach ($icons as $icon) { $icons_str .= $icon; } return $icons_str; } /** * Render the placeholder + config the official Oney widget uses to display the inline * payment-schedule section for this specific gateway (checkout obligation). */ public function payment_fields(): void { if ($this->check_oney_is_available() !== true) { // check_oney_is_available() sets $this->description with the reason Oney can't be used. $description = $this->get_description(); if (!empty($description)) { echo wpautop(wptexturize($description)); } return; } $account = PayplugWoocommerceHelper::get_account_data_from_options(); $oney_settings = $this->get_configuration()->get_option('payment_methods.configuration.oney'); $country = PayplugWoocommerceHelper::getISOCountryCode(); $mapper = OneyAccountMapper::map((array) $account, $oney_settings, $country); $code = $mapper->business_transaction_code_for_gateway($this->id); if (!$mapper->is_enabled() || !$code) { return; } printf( '
', esc_attr($this->id), wp_json_encode([ 'merchant_guid' => $mapper->get_merchant_guid(), 'business_transaction_code' => $code, 'country' => $country, 'language' => strtoupper(substr(get_locale(), 0, 2)), 'payment_amount' => floatval(WC()->cart->total), ]) ); } /** * Check if Oney is available * * @return bool|int true, false, or an ONEY_* status code */ public function check_oney_is_available() { static $is_running = false; if ($is_running) { return false; } $is_running = true; try { $result = $this->do_check_oney_is_available(); } finally { $is_running = false; } return $result; } private function do_check_oney_is_available() { $cart = WC()->cart; //for backend orders if (!empty(get_query_var('order-pay'))) { $order = wc_get_order(get_query_var('order-pay')); //wc_get_order @return bool|WC_Order|WC_Order_Refund if ($order === false) { return false; } $items = $order->get_items(); $country_code_shipping = $order->get_shipping_country(); $country_code_billing = $order->get_billing_country(); // Skip cart population for subscription renewals: WC Subscriptions manages the cart // itself and adding items here would cause a double entry in the order summary. $is_renewal = function_exists('wcs_order_contains_renewal') && wcs_order_contains_renewal($order); if (!$is_renewal) { $this->order_items_to_cart($cart, $items); } } $oney_cfg = $this->get_configuration()->get_option('payment_methods.configuration.oney'); $account = PayplugWoocommerceHelper::get_account_data_from_options(); $oney_account_cfg = ((array) $account)['configuration']['oney'] ?? []; // Same clamping OneyAccountMapper::map() uses for the badge/widget display, so // checkout validation and display never silently drift apart (PRE-3457 review). [$min_cents, $max_cents] = OneyAccountMapper::effective_bounds($oney_account_cfg, $oney_cfg); $oney_amount = [ 'min' => $min_cents / 100, 'max' => $max_cents / 100, ]; // validate_checkout() reads these to build its exception message - populate them // unconditionally (before any early return below) so they're never stale/unset there. $this->oney_thresholds_min = $oney_amount['min']; $this->oney_thresholds_max = $oney_amount['max']; if (empty($cart->total)) { return false; } if (empty($cart->cart_contents_count)) { return false; } $total_price = floatval($cart->total); $products_qty = (int) $cart->cart_contents_count; // Min and max if ($total_price < $oney_amount['min'] || $total_price > $oney_amount['max']) { $this->description = '
' . sprintf(__('The total amount of your order should be between %s€ and %s€ to pay with Oney.', 'payplug'), $oney_amount['min'], $oney_amount['max']) . '
'; return false; } // Cart check if ($products_qty >= self::ONEY_PRODUCT_QUANTITY_MAXIMUM) { $this->description = '
' . sprintf(__('The payment with Oney is unavailable because you have more than %s items in your cart.', 'payplug'), self::ONEY_PRODUCT_QUANTITY_MAXIMUM) . '
'; return self::ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH; } // Country check if (empty($country_code_shipping) || empty($country_code_shipping)) { $country_code_shipping = WC()->customer->get_shipping_country(); $country_code_billing = WC()->customer->get_billing_country(); } //WOOC-663 exception for the description to be visible //billing allowed? if ($this->allowed_country($country_code_billing, $this->allowed_country_codes)) { //if shipping is different from billing and billing is accepted if (!$this->validate_shipping_billing_country($country_code_shipping, $country_code_billing)) { $this->description = '
' . __('Unavailable for the specified country.', 'payplug') . '
'; return self::ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED; } } else { return self::ONEY_DISALBE_CHECKOUT_OPTIONS; } return true; } /** * Check the order amount to ensure it's on the allowed range. * * @param int $amount * * @return int|\WP_Error */ public function validate_order_amount($amount) { if ($amount / 100 < $this->min_oney_price || $amount / 100 > $this->max_oney_price) { return new \WP_Error( 'invalid order amount', sprintf(__('The total amount of your order should be between %s€ and %s€ to pay with Oney.', 'payplug'), $this->min_oney_price, $this->max_oney_price) ); } return $amount; } /** * @param \WC_Order $order * @param int $amount * @param int $customer_id * * @throws \Exception * * @return array */ public function process_standard_payment($order, $amount, $customer_id) { $order_id = PayplugWoocommerceHelper::is_pre_30() ? $order->id : $order->get_id(); try { if (!$this->check_oney_is_available()) { throw new \Exception(__('Payment processing failed. Please retry.', 'payplug')); } elseif ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED) { $country_code = WC()->customer->get_shipping_country(); throw new \Exception(__('Unavailable for the specified country.')); } elseif ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH) { throw new \Exception(sprintf(__('The payment with Oney is unavailable because you have more than %s items in your cart.', 'payplug'), self::ONEY_PRODUCT_QUANTITY_MAXIMUM)); } $country = PayplugWoocommerceHelper::is_pre_30() ? $order->billing_country : $order->get_billing_country(); $phone = PayplugWoocommerceHelper::is_pre_30() ? $order->billing_phone : $order->get_billing_phone(); $billing_email = PayplugWoocommerceHelper::is_pre_30() ? $order->billing_email : $order->get_billing_email(); $phone_number_util = PhoneNumberUtil::getInstance(); $phone_number = $phone_number_util->parse($phone, $country); if (PhoneNumberType::MOBILE !== $phone_number_util->getNumberType($phone_number)) { throw new \Exception(__('Mobile phone number fullfilled is invalid. Please retry.', 'payplug')); } if (!filter_var($billing_email, FILTER_VALIDATE_EMAIL) || strpos($billing_email, '+') !== false) { throw new \Exception(__('Your email address is too long and the + character is not valid, please change it to another address (max 100 characters).', 'payplug')); } $address_data = PayplugAddressData::from_order($order); $return_url = esc_url_raw($order->get_checkout_order_received_url()); if (!(substr($return_url, 0, 4) === 'http')) { $return_url = get_site_url() . $return_url; } $cart_items = []; $items = $order->get_items(); foreach ($items as $item) { $data = $item->get_data(); $total = floatval(round($data['total'], 2)) * 100; $cart_items[] = [ 'delivery_label' => 'storepickup', 'delivery_type' => 'storepickup', 'brand' => 'Woocommerce', 'merchant_item_id' => 'cart-' . $data['id'] . '-' . $data['product_id'], 'name' => $data['name'], 'expected_delivery_date' => date('Y-m-d'), 'total_amount' => (int) $total, 'price' => round($total / $data['quantity']), 'quantity' => $data['quantity'], ]; } $payment_data = [ 'authorized_amount' => $amount, 'auto_capture' => true, 'currency' => get_woocommerce_currency(), 'payment_method' => $this->id, 'billing' => $address_data->get_billing(), 'shipping' => $address_data->get_shipping(), 'payment_context' => [ 'cart' => $cart_items, ], 'notification_url' => esc_url_raw(WC()->api_request_url('PayplugGateway')), 'hosted_payment' => [ 'return_url' => $return_url, 'cancel_url' => esc_url_raw($order->get_cancel_order_url_raw()), ], 'metadata' => [ 'order_id' => $order_id, 'customer_id' => ((int) $customer_id > 0) ? $customer_id : 'guest', 'domain' => $this->limit_length(esc_url_raw(home_url()), 500), ], ]; if (PayplugWoocommerceHelper::is_checkout_block() && is_checkout()) { $payment_data['metadata']['woocommerce_block'] = 'CHECKOUT'; } elseif (PayplugWoocommerceHelper::is_cart_block() && is_cart()) { $payment_data['metadata']['woocommerce_block'] = 'CART'; } /** * Filter the payment data before it's used * * @param array $payment_data * @param int $order_id * @param array $customer_details * @param PayplugAddressData $address_data */ $payment_data = apply_filters('payplug_gateway_payment_data', $payment_data, $order_id, [], $address_data); $payment = $this->payplug_api->payment_create($payment_data); // Save transaction id for the order PayplugWoocommerceHelper::is_pre_30() ? update_post_meta($order_id, '_transaction_id', $payment->id) : $order->set_transaction_id($payment->id); if (is_callable([$order, 'save'])) { $order->save(); } /** * Fires once a payment has been created. * * @param int $order_id Order ID * @param PaymentResource $payment Payment resource */ \do_action('payplug_gateway_payment_created', $order_id, $payment); $metadata = PayplugWoocommerceHelper::extract_transaction_metadata($payment); PayplugWoocommerceHelper::save_transaction_metadata($order, $metadata); PayplugGateway::log(sprintf('Payment creation complete for order #%s', $order_id)); return [ 'result' => 'success', 'redirect' => $payment->hosted_payment->payment_url, 'cancel' => $payment->hosted_payment->cancel_url, ]; } catch (\HttpException $e) { PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error'); throw new \Exception(__($e->getMessage(), 'payplug')); } catch (\Exception $e) { PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, $e->getMessage()), 'error'); throw new \Exception(__($e->getMessage(), 'payplug')); } } /** * Check if the gatteway is allowed for the order amount * * @param array * * @return array */ public function check_gateway($gateways) { if (isset($gateways[$this->id]) && $gateways[$this->id]->id == $this->id) { //remove gateway if thresholds/country criteria are not met if ($this->check_oney_is_available() === false) { unset($gateways[$this->id]); } //remove gateway if account doesn't have permission if (!PayplugWoocommerceHelper::is_oney_available()) { unset($gateways[$this->id]); } else { $gateways = parent::check_gateway($gateways); } } return $gateways; } /** * Show Oney refund text * * @return void */ public function oney_refund_text($order): void { if ($this->id === $order->get_payment_method() && parent::can_refund_order($order) && $order->get_status() !== 'refunded' && $this->payplug_api) { $order_metadata = $order->get_meta('_payplug_metadata'); if (is_array($order_metadata) && !empty($order_metadata['transaction_id'])) { try { $payment = $this->payplug_api->payment_retrieve($order_metadata['transaction_id']); $today = current_time('Y-m-d H:i:s'); $can_refund_date = date('Y-m-d H:i:s', $payment->__get('refundable_after')); if ($can_refund_date >= $today) { echo "

" . __('Refund will be possible 48 hours after the last payment or refund transaction.', 'payplug') . '

'; } } catch (\Exception $e) { PayplugGateway::log(sprintf('Could not retrieve refundable_after for Oney order: %s', $e->getMessage()), 'error'); } } } } /** * Billing and shipping addresses should have the same country and allowed by Oney * https://payplug-prod.atlassian.net/browse/WOOC-227 * * @param $order * * @return bool */ public function validate_shipping_billing_country($shipping_country, $billing_country) { if ($billing_country === $shipping_country) { return true; } return false; } /** * Country of billing or shipping at oney payments should be on the allowedCountry that comes from pauyplug-api /account * * @param string $country * @param array $allowed * * @return bool */ public function allowed_country($country, $allowed) { if (in_array($country, $allowed)) { return true; } return false; } /** * Empty the cart and add all order_items * * @param $cart * @param $items * * @return void */ private function order_items_to_cart($cart, $items): void { $cart->empty_cart(); foreach ($items as $item) { $cart->add_to_cart($item->get_product_id(), $item->get_quantity(), $item->get_variation_id()); } } public function checkGateway() { $options = PayplugWoocommerceHelper::get_payplug_options(); // Now called unconditionally from the constructor (not just at checkout via // is_available()), so a legacy account whose stored config predates the oney block // (never went through Upgrade::run_upgrade()) would otherwise throw "undefined array // key" warnings on every admin page that builds the WC gateway list (PRE-3597 review). return $options['payment_methods']['configuration']['oney']['active'] ?? false; } }