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('
', 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;
}
}