| @@ -4,8 +4,9 @@ | ||
| 4 | 4 | |
| 5 | 5 | use libphonenumber\PhoneNumberType; |
| 6 | 6 | use libphonenumber\PhoneNumberUtil; |
| 7 | 7 | use Payplug\PayplugWoocommerce\Controller\PayplugGenericGateway; |
| 8 | +use Payplug\PayplugWoocommerce\Gateway\Oney\OneyAccountMapper; | |
| 8 | 9 | use Payplug\PayplugWoocommerce\PayplugWoocommerceHelper; |
| 9 | 10 | |
| 10 | 11 | // Exit if accessed directly |
| 11 | 12 | if (!defined('ABSPATH')) { |
| @@ -16,16 +17,16 @@ | ||
| 16 | 17 | * PayPlug WooCommerce Gateway. |
| 17 | 18 | */ |
| 18 | 19 | class PayplugGatewayOney3x extends PayplugGenericGateway |
| 19 | 20 | { |
| 20 | - const ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED = 2; | |
| 21 | - const ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH = 3; | |
| 22 | - const ONEY_DISALBE_CHECKOUT_OPTIONS = 4; | |
| 21 | + public const ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED = 2; | |
| 22 | + public const ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH = 3; | |
| 23 | + public const ONEY_DISALBE_CHECKOUT_OPTIONS = 4; | |
| 23 | 24 | |
| 24 | - const ONEY_PRODUCT_QUANTITY_MAXIMUM = 1000; | |
| 25 | + public const ONEY_PRODUCT_QUANTITY_MAXIMUM = 1000; | |
| 25 | 26 | |
| 26 | 27 | protected $oney_response; |
| 27 | - const ENABLE_ON_TEST_MODE = true; | |
| 28 | + public const ENABLE_ON_TEST_MODE = true; | |
| 28 | 29 | |
| 29 | 30 | public $allowed_country_codes = []; |
| 30 | 31 | |
| 31 | 32 | public function __construct() |
| @@ -34,9 +35,9 @@ | ||
| 34 | 35 | $this->id = 'oney_x3_with_fees'; |
| 35 | 36 | $this->method_title = _x('PayPlug Oney 3x', 'Gateway method title', 'payplug'); |
| 36 | 37 | $this->method_description = __('Enable PayPlug Oney 3x for your customers.', 'payplug'); |
| 37 | 38 | $this->title = __('Pay by card in 3x with Oney', 'payplug'); |
| 38 | - $this->has_fields = false; | |
| 39 | + $this->has_fields = true; | |
| 39 | 40 | |
| 40 | 41 | add_action('woocommerce_order_item_add_action_buttons', [$this, 'oney_refund_text']); |
| 41 | 42 | add_action('woocommerce_after_checkout_validation', [$this, 'validate_checkout'], 10); |
| 42 | 43 | |
| @@ -41,8 +42,15 @@ | ||
| 41 | 42 | add_action('woocommerce_after_checkout_validation', [$this, 'validate_checkout'], 10); |
| 42 | 43 | |
| 43 | 44 | self::set_oney_configuration(); |
| 44 | 45 | |
| 46 | + // Unlike the other sub-gateways (Bancontact, AmericanExpress, PPRO methods...), Oney never | |
| 47 | + // synced $this->enabled with the plugin's own "active" toggle, so disabling Oney from the | |
| 48 | + // Payplug config left it shown as enabled on WooCommerce's native Payments settings page. | |
| 49 | + if (!$this->checkGateway()) { | |
| 50 | + $this->enabled = 'no'; | |
| 51 | + } | |
| 52 | + | |
| 45 | 53 | if (is_checkout()) { |
| 46 | 54 | if ($this->check_oney_is_available() === self::ONEY_DISALBE_CHECKOUT_OPTIONS) { |
| 47 | 55 | $this->enabled = 'no'; |
| 48 | 56 | } |
| @@ -48,13 +56,18 @@ | ||
| 48 | 56 | } |
| 49 | 57 | } |
| 50 | 58 | } |
| 51 | 59 | |
| 52 | - public function validate_checkout() | |
| 60 | + public function validate_checkout(): void | |
| 53 | 61 | { |
| 54 | 62 | $posted_data = $this->get_post_data(); |
| 55 | 63 | |
| 56 | - if (in_array($posted_data['payment_method'], ['oney_x3_with_fees","oney_x4_with_fees", "oney_x3_without_fees", "oney_x4_without_fees'])) { | |
| 64 | + // Was a single malformed string (missing quotes around each id) instead of a 4-element | |
| 65 | + // array, so this guard never matched and the whole method was a silent no-op (PRE-3457 | |
| 66 | + // review / PRE-3457 follow-up fix). | |
| 67 | + $oney_gateway_ids = ['oney_x3_with_fees', 'oney_x4_with_fees', 'oney_x3_without_fees', 'oney_x4_without_fees']; | |
| 68 | + | |
| 69 | + if (in_array($posted_data['payment_method'] ?? '', $oney_gateway_ids, true)) { | |
| 57 | 70 | if ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED) { |
| 58 | 71 | throw new \Exception(__('Unavailable for the specified country.')); |
| 59 | 72 | } elseif ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH) { |
| 60 | 73 | 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)); |
| @@ -68,9 +81,9 @@ | ||
| 68 | 81 | * Set oney settings |
| 69 | 82 | * |
| 70 | 83 | * @return void |
| 71 | 84 | */ |
| 72 | - private function set_oney_configuration() | |
| 85 | + private function set_oney_configuration(): void | |
| 73 | 86 | { |
| 74 | 87 | $account = PayplugWoocommerceHelper::get_account_data_from_options(); |
| 75 | 88 | if ($account) { |
| 76 | 89 | $oney_configuration = $account['configuration']['oney']; |
| @@ -86,54 +99,9 @@ | ||
| 86 | 99 | * @return string |
| 87 | 100 | */ |
| 88 | 101 | public function get_icon() |
| 89 | 102 | { |
| 90 | - $disable = ''; | |
| 91 | - if ($this->check_oney_is_available() === true) { | |
| 92 | - $total_price = floatval(WC()->cart->total); | |
| 93 | - $this->oney_response = $this->payplug_api->simulate_oney_payment($total_price, 'with_fees'); | |
| 94 | - $currency = get_woocommerce_currency_symbol(get_option('woocommerce_currency')); | |
| 95 | - $total_price_oney = floatval($this->oney_response['x3_with_fees']['down_payment_amount']); | |
| 96 | - foreach ($this->oney_response['x3_with_fees']['installments'] as $installment) { | |
| 97 | - $total_price_oney = $total_price_oney + floatval($installment['amount']); | |
| 98 | - } | |
| 99 | - $f = function ($fn) { | |
| 100 | - return $fn; | |
| 101 | - }; | |
| 102 | - | |
| 103 | - $tax_cost = floatval($this->oney_response['x3_with_fees']['total_cost']) / 100; | |
| 104 | - | |
| 105 | - if (is_array($this->oney_response)) { | |
| 106 | - $this->description = <<<HTML | |
| 107 | - <p> | |
| 108 | - <div class="payplug-oney-flex"> | |
| 109 | - <div>{$f(__('Bring', 'payplug'))}:</div> | |
| 110 | - <div>{$this->oney_response['x3_with_fees']['down_payment_amount']} {$currency}</div> | |
| 111 | - </div> | |
| 112 | - <div class="payplug-oney-flex"> | |
| 113 | - <small>( {$f(__('oney_financing_cost', 'payplug'))} <b>{$tax_cost} {$currency}</b> TAEG : <b>{$this->oney_response['x3_with_fees']['effective_annual_percentage_rate']} %</b> )</small> | |
| 114 | - </div> | |
| 115 | - <div class="payplug-oney-flex"> | |
| 116 | - <div>{$f(__('1st monthly payment', 'payplug'))}:</div> | |
| 117 | - <div>{$this->oney_response['x3_with_fees']['installments'][0]['amount']} {$currency}</div> | |
| 118 | - </div> | |
| 119 | - <div class="payplug-oney-flex"> | |
| 120 | - <div>{$f(__('2nd monthly payment', 'payplug'))}:</div> | |
| 121 | - <div>{$this->oney_response['x3_with_fees']['installments'][1]['amount']} {$currency}</div> | |
| 122 | - </div> | |
| 123 | - <div class="payplug-oney-flex"> | |
| 124 | - <div><b>{$f(__('oney_total', 'payplug'))}</b></div> | |
| 125 | - <div><b>{$total_price_oney} {$currency}</b></div> | |
| 126 | - </div> | |
| 127 | - </p> | |
| 128 | -HTML; | |
| 129 | - } else { | |
| 130 | - $this->description = $this->oney_response; | |
| 131 | - } | |
| 132 | - } else { | |
| 133 | - $disable = 'disable-checkout-icons'; | |
| 134 | - } | |
| 135 | - | |
| 103 | + $disable = $this->check_oney_is_available() === true ? '' : 'disable-checkout-icons'; | |
| 136 | 104 | $available_img = 'x3_with_fees.svg'; |
| 137 | 105 | |
| 138 | 106 | $icons = apply_filters('payplug_payment_icons', [ |
| 139 | 107 | 'payplug' => sprintf('<img src="%s" alt="Oney 3x" class="payplug-payment-icon ' . $disable . '" />', esc_url(PAYPLUG_GATEWAY_PLUGIN_URL . '/assets/images/checkout/' . $available_img)), |
| @@ -146,8 +114,48 @@ | ||
| 146 | 114 | return $icons_str; |
| 147 | 115 | } |
| 148 | 116 | |
| 149 | 117 | /** |
| 118 | + * Render the placeholder + config the official Oney widget uses to display the inline | |
| 119 | + * payment-schedule section for this specific gateway (checkout obligation). | |
| 120 | + */ | |
| 121 | + public function payment_fields(): void | |
| 122 | + { | |
| 123 | + if ($this->check_oney_is_available() !== true) { | |
| 124 | + // check_oney_is_available() sets $this->description with the reason Oney can't be used. | |
| 125 | + $description = $this->get_description(); | |
| 126 | + | |
| 127 | + if (!empty($description)) { | |
| 128 | + echo wpautop(wptexturize($description)); | |
| 129 | + } | |
| 130 | + | |
| 131 | + return; | |
| 132 | + } | |
| 133 | + | |
| 134 | + $account = PayplugWoocommerceHelper::get_account_data_from_options(); | |
| 135 | + $oney_settings = $this->get_configuration()->get_option('payment_methods.configuration.oney'); | |
| 136 | + $country = PayplugWoocommerceHelper::getISOCountryCode(); | |
| 137 | + $mapper = OneyAccountMapper::map((array) $account, $oney_settings, $country); | |
| 138 | + $code = $mapper->business_transaction_code_for_gateway($this->id); | |
| 139 | + | |
| 140 | + if (!$mapper->is_enabled() || !$code) { | |
| 141 | + return; | |
| 142 | + } | |
| 143 | + | |
| 144 | + printf( | |
| 145 | + '<div id="oney-checkout-loading-%1$s" class="payplug-lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><div id="oney-checkout-%1$s"></div><script type="application/json" id="oney-checkout-config-%1$s">%2$s</script>', | |
| 146 | + esc_attr($this->id), | |
| 147 | + wp_json_encode([ | |
| 148 | + 'merchant_guid' => $mapper->get_merchant_guid(), | |
| 149 | + 'business_transaction_code' => $code, | |
| 150 | + 'country' => $country, | |
| 151 | + 'language' => strtoupper(substr(get_locale(), 0, 2)), | |
| 152 | + 'payment_amount' => floatval(WC()->cart->total), | |
| 153 | + ]) | |
| 154 | + ); | |
| 155 | + } | |
| 156 | + | |
| 157 | + /** | |
| 150 | 158 | * Check if Oney is available |
| 151 | 159 | * |
| 152 | 160 | * @return bool|int true, false, or an ONEY_* status code |
| 153 | 161 | */ |
| @@ -184,11 +192,31 @@ | ||
| 184 | 192 | |
| 185 | 193 | $country_code_shipping = $order->get_shipping_country(); |
| 186 | 194 | $country_code_billing = $order->get_billing_country(); |
| 187 | 195 | |
| 188 | - $this->order_items_to_cart($cart, $items); | |
| 196 | + // Skip cart population for subscription renewals: WC Subscriptions manages the cart | |
| 197 | + // itself and adding items here would cause a double entry in the order summary. | |
| 198 | + $is_renewal = function_exists('wcs_order_contains_renewal') && wcs_order_contains_renewal($order); | |
| 199 | + if (!$is_renewal) { | |
| 200 | + $this->order_items_to_cart($cart, $items); | |
| 201 | + } | |
| 189 | 202 | } |
| 190 | 203 | |
| 204 | + $oney_cfg = $this->get_configuration()->get_option('payment_methods.configuration.oney'); | |
| 205 | + $account = PayplugWoocommerceHelper::get_account_data_from_options(); | |
| 206 | + $oney_account_cfg = ((array) $account)['configuration']['oney'] ?? []; | |
| 207 | + // Same clamping OneyAccountMapper::map() uses for the badge/widget display, so | |
| 208 | + // checkout validation and display never silently drift apart (PRE-3457 review). | |
| 209 | + [$min_cents, $max_cents] = OneyAccountMapper::effective_bounds($oney_account_cfg, $oney_cfg); | |
| 210 | + $oney_amount = [ | |
| 211 | + 'min' => $min_cents / 100, | |
| 212 | + 'max' => $max_cents / 100, | |
| 213 | + ]; | |
| 214 | + // validate_checkout() reads these to build its exception message - populate them | |
| 215 | + // unconditionally (before any early return below) so they're never stale/unset there. | |
| 216 | + $this->oney_thresholds_min = $oney_amount['min']; | |
| 217 | + $this->oney_thresholds_max = $oney_amount['max']; | |
| 218 | + | |
| 191 | 219 | if (empty($cart->total)) { |
| 192 | 220 | return false; |
| 193 | 221 | } |
| 194 | 222 | |
| @@ -198,13 +226,8 @@ | ||
| 198 | 226 | |
| 199 | 227 | $total_price = floatval($cart->total); |
| 200 | 228 | $products_qty = (int) $cart->cart_contents_count; |
| 201 | 229 | |
| 202 | - $oney_cfg = $this->get_configuration()->get_option('payment_methods.configuration.oney'); | |
| 203 | - $oney_amount = json_decode($oney_cfg['custom_amounts'], true); | |
| 204 | - $oney_amount['min'] = (float) $oney_amount['min'] / 100; | |
| 205 | - $oney_amount['max'] = (float) $oney_amount['max'] / 100; | |
| 206 | - | |
| 207 | 230 | // Min and max |
| 208 | 231 | if ($total_price < $oney_amount['min'] || $total_price > $oney_amount['max']) { |
| 209 | 232 | $this->description = '<div class="payment_method_oney_x3_with_fees_disabled">' . sprintf(__('The total amount of your order should be between %s€ and %s€ to pay with Oney.', 'payplug'), $oney_amount['min'], $oney_amount['max']) . '</div>'; |
| 210 | 233 | |
| @@ -426,9 +449,9 @@ | ||
| 426 | 449 | * Show Oney refund text |
| 427 | 450 | * |
| 428 | 451 | * @return void |
| 429 | 452 | */ |
| 430 | - public function oney_refund_text($order) | |
| 453 | + public function oney_refund_text($order): void | |
| 431 | 454 | { |
| 432 | 455 | if ($this->id === $order->get_payment_method() && parent::can_refund_order($order) && $order->get_status() !== 'refunded' && $this->payplug_api) { |
| 433 | 456 | $order_metadata = $order->get_meta('_payplug_metadata'); |
| 434 | 457 | |
| @@ -446,16 +469,8 @@ | ||
| 446 | 469 | } |
| 447 | 470 | } |
| 448 | 471 | } |
| 449 | 472 | |
| 450 | - public function payment_fields() | |
| 451 | - { | |
| 452 | - $description = $this->get_description(); | |
| 453 | - if (!empty($description)) { | |
| 454 | - echo wpautop(wptexturize($description)); | |
| 455 | - } | |
| 456 | - } | |
| 457 | - | |
| 458 | 473 | /** |
| 459 | 474 | * Billing and shipping addresses should have the same country and allowed by Oney |
| 460 | 475 | * https://payplug-prod.atlassian.net/browse/WOOC-227 |
| 461 | 476 | * |
| @@ -496,9 +511,9 @@ | ||
| 496 | 511 | * @param $items |
| 497 | 512 | * |
| 498 | 513 | * @return void |
| 499 | 514 | */ |
| 500 | - private function order_items_to_cart($cart, $items) | |
| 515 | + private function order_items_to_cart($cart, $items): void | |
| 501 | 516 | { |
| 502 | 517 | $cart->empty_cart(); |
| 503 | 518 | foreach ($items as $item) { |
| 504 | 519 | $cart->add_to_cart($item->get_product_id(), $item->get_quantity(), $item->get_variation_id()); |
| @@ -508,7 +523,11 @@ | ||
| 508 | 523 | public function checkGateway() |
| 509 | 524 | { |
| 510 | 525 | $options = PayplugWoocommerceHelper::get_payplug_options(); |
| 511 | 526 | |
| 512 | - return empty($options) || !isset($options['payment_methods']) ? false : $options['payment_methods']['configuration']['oney']['active']; | |
| 527 | + // Now called unconditionally from the constructor (not just at checkout via | |
| 528 | + // is_available()), so a legacy account whose stored config predates the oney block | |
| 529 | + // (never went through Upgrade::run_upgrade()) would otherwise throw "undefined array | |
| 530 | + // key" warnings on every admin page that builds the WC gateway list (PRE-3597 review). | |
| 531 | + return $options['payment_methods']['configuration']['oney']['active'] ?? false; | |
| 513 | 532 | } |
| 514 | 533 | } |