PluginProbe
PayPlug for WooCommerce (Official) / 3.1.0
PayPlug for WooCommerce (Official) v3.1.0
3.1.0 3.0.0 2.18.0 1.0.17 1.0.18 1.0.19 1.0.20 1.0.21 1.0.22 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1.0 1.10.0 1.10.1 1.2.1 1.2.10 1.2.11 1.2.2 1.2.3 1.2.4 1.2.5 All 102 releases
payplug / src / Gateway / PayplugGatewayOney3x.php

PayplugGatewayOney3x.php in PayPlug for WooCommerce (Official) 3.1.0, at src/Gateway/PayplugGatewayOney3x.php

534 lines 21.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Payplug\PayplugWoocommerce\Gateway;
4
5 use libphonenumber\PhoneNumberType;
6 use libphonenumber\PhoneNumberUtil;
7 use Payplug\PayplugWoocommerce\Controller\PayplugGenericGateway;
8 use Payplug\PayplugWoocommerce\Gateway\Oney\OneyAccountMapper;
9 use Payplug\PayplugWoocommerce\PayplugWoocommerceHelper;
10
11 // Exit if accessed directly
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 /**
17 * PayPlug WooCommerce Gateway.
18 */
19 class PayplugGatewayOney3x extends PayplugGenericGateway
20 {
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;
24
25 public const ONEY_PRODUCT_QUANTITY_MAXIMUM = 1000;
26
27 protected $oney_response;
28 public const ENABLE_ON_TEST_MODE = true;
29
30 public $allowed_country_codes = [];
31
32 public function __construct()
33 {
34 parent::__construct();
35 $this->id = 'oney_x3_with_fees';
36 $this->method_title = _x('PayPlug Oney 3x', 'Gateway method title', 'payplug');
37 $this->method_description = __('Enable PayPlug Oney 3x for your customers.', 'payplug');
38 $this->title = __('Pay by card in 3x with Oney', 'payplug');
39 $this->has_fields = true;
40
41 add_action('woocommerce_order_item_add_action_buttons', [$this, 'oney_refund_text']);
42 add_action('woocommerce_after_checkout_validation', [$this, 'validate_checkout'], 10);
43
44 self::set_oney_configuration();
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
53 if (is_checkout()) {
54 if ($this->check_oney_is_available() === self::ONEY_DISALBE_CHECKOUT_OPTIONS) {
55 $this->enabled = 'no';
56 }
57 }
58 }
59
60 public function validate_checkout(): void
61 {
62 $posted_data = $this->get_post_data();
63
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)) {
70 if ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED) {
71 throw new \Exception(__('Unavailable for the specified country.'));
72 } elseif ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH) {
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));
74 } elseif (!$this->check_oney_is_available()) {
75 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));
76 }
77 }
78 }
79
80 /**
81 * Set oney settings
82 *
83 * @return void
84 */
85 private function set_oney_configuration(): void
86 {
87 $account = PayplugWoocommerceHelper::get_account_data_from_options();
88 if ($account) {
89 $oney_configuration = $account['configuration']['oney'];
90 $this->min_oney_price = $oney_configuration['min_amounts']['EUR'] / 100;
91 $this->max_oney_price = $oney_configuration['max_amounts']['EUR'] / 100;
92 $this->allowed_country_codes = $oney_configuration['allowed_countries'];
93 }
94 }
95
96 /**
97 * Get payment icons.
98 *
99 * @return string
100 */
101 public function get_icon()
102 {
103 $disable = $this->check_oney_is_available() === true ? '' : 'disable-checkout-icons';
104 $available_img = 'x3_with_fees.svg';
105
106 $icons = apply_filters('payplug_payment_icons', [
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)),
108 ]);
109 $icons_str = '';
110 foreach ($icons as $icon) {
111 $icons_str .= $icon;
112 }
113
114 return $icons_str;
115 }
116
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 /**
158 * Check if Oney is available
159 *
160 * @return bool|int true, false, or an ONEY_* status code
161 */
162 public function check_oney_is_available()
163 {
164 static $is_running = false;
165 if ($is_running) {
166 return false;
167 }
168 $is_running = true;
169 try {
170 $result = $this->do_check_oney_is_available();
171 } finally {
172 $is_running = false;
173 }
174
175 return $result;
176 }
177
178 private function do_check_oney_is_available()
179 {
180 $cart = WC()->cart;
181
182 //for backend orders
183 if (!empty(get_query_var('order-pay'))) {
184 $order = wc_get_order(get_query_var('order-pay'));
185
186 //wc_get_order @return bool|WC_Order|WC_Order_Refund
187 if ($order === false) {
188 return false;
189 }
190
191 $items = $order->get_items();
192
193 $country_code_shipping = $order->get_shipping_country();
194 $country_code_billing = $order->get_billing_country();
195
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 }
202 }
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
219 if (empty($cart->total)) {
220 return false;
221 }
222
223 if (empty($cart->cart_contents_count)) {
224 return false;
225 }
226
227 $total_price = floatval($cart->total);
228 $products_qty = (int) $cart->cart_contents_count;
229
230 // Min and max
231 if ($total_price < $oney_amount['min'] || $total_price > $oney_amount['max']) {
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>';
233
234 return false;
235 }
236
237 // Cart check
238 if ($products_qty >= self::ONEY_PRODUCT_QUANTITY_MAXIMUM) {
239 $this->description = '<div class="payment_method_oney_x3_with_fees_disabled">' . sprintf(__('The payment with Oney is unavailable because you have more than %s items in your cart.', 'payplug'), self::ONEY_PRODUCT_QUANTITY_MAXIMUM) . '</div>';
240
241 return self::ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH;
242 }
243
244 // Country check
245 if (empty($country_code_shipping) || empty($country_code_shipping)) {
246 $country_code_shipping = WC()->customer->get_shipping_country();
247 $country_code_billing = WC()->customer->get_billing_country();
248 }
249
250 //WOOC-663 exception for the description to be visible
251 //billing allowed?
252 if ($this->allowed_country($country_code_billing, $this->allowed_country_codes)) {
253
254 //if shipping is different from billing and billing is accepted
255 if (!$this->validate_shipping_billing_country($country_code_shipping, $country_code_billing)) {
256 $this->description = '<div class="payment_method_oney_x3_with_fees_disabled">' . __('Unavailable for the specified country.', 'payplug') . '</div>';
257
258 return self::ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED;
259 }
260 } else {
261 return self::ONEY_DISALBE_CHECKOUT_OPTIONS;
262 }
263
264 return true;
265 }
266
267 /**
268 * Check the order amount to ensure it's on the allowed range.
269 *
270 * @param int $amount
271 *
272 * @return int|\WP_Error
273 */
274 public function validate_order_amount($amount)
275 {
276 if ($amount / 100 < $this->min_oney_price || $amount / 100 > $this->max_oney_price) {
277 return new \WP_Error(
278 'invalid order amount',
279 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)
280 );
281 }
282
283 return $amount;
284 }
285
286 /**
287 * @param \WC_Order $order
288 * @param int $amount
289 * @param int $customer_id
290 *
291 * @throws \Exception
292 *
293 * @return array
294 */
295 public function process_standard_payment($order, $amount, $customer_id)
296 {
297 $order_id = PayplugWoocommerceHelper::is_pre_30() ? $order->id : $order->get_id();
298 try {
299 if (!$this->check_oney_is_available()) {
300 throw new \Exception(__('Payment processing failed. Please retry.', 'payplug'));
301 } elseif ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_COUNTRY_NOT_ALLOWED) {
302 $country_code = WC()->customer->get_shipping_country();
303 throw new \Exception(__('Unavailable for the specified country.'));
304 } elseif ($this->check_oney_is_available() === self::ONEY_UNAVAILABLE_CODE_CART_SIZE_TOO_HIGH) {
305 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));
306 }
307
308 $country = PayplugWoocommerceHelper::is_pre_30() ? $order->billing_country : $order->get_billing_country();
309 $phone = PayplugWoocommerceHelper::is_pre_30() ? $order->billing_phone : $order->get_billing_phone();
310 $billing_email = PayplugWoocommerceHelper::is_pre_30() ? $order->billing_email : $order->get_billing_email();
311 $phone_number_util = PhoneNumberUtil::getInstance();
312 $phone_number = $phone_number_util->parse($phone, $country);
313 if (PhoneNumberType::MOBILE !== $phone_number_util->getNumberType($phone_number)) {
314 throw new \Exception(__('Mobile phone number fullfilled is invalid. Please retry.', 'payplug'));
315 }
316
317 if (!filter_var($billing_email, FILTER_VALIDATE_EMAIL) || strpos($billing_email, '+') !== false) {
318 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'));
319 }
320
321 $address_data = PayplugAddressData::from_order($order);
322
323 $return_url = esc_url_raw($order->get_checkout_order_received_url());
324
325 if (!(substr($return_url, 0, 4) === 'http')) {
326 $return_url = get_site_url() . $return_url;
327 }
328
329 $cart_items = [];
330 $items = $order->get_items();
331 foreach ($items as $item) {
332 $data = $item->get_data();
333 $total = floatval(round($data['total'], 2)) * 100;
334 $cart_items[] = [
335 'delivery_label' => 'storepickup',
336 'delivery_type' => 'storepickup',
337 'brand' => 'Woocommerce',
338 'merchant_item_id' => 'cart-' . $data['id'] . '-' . $data['product_id'],
339 'name' => $data['name'],
340 'expected_delivery_date' => date('Y-m-d'),
341 'total_amount' => (int) $total,
342 'price' => round($total / $data['quantity']),
343 'quantity' => $data['quantity'],
344 ];
345 }
346
347 $payment_data = [
348 'authorized_amount' => $amount,
349 'auto_capture' => true,
350 'currency' => get_woocommerce_currency(),
351 'payment_method' => $this->id,
352 'billing' => $address_data->get_billing(),
353 'shipping' => $address_data->get_shipping(),
354 'payment_context' => [
355 'cart' => $cart_items,
356 ],
357 'notification_url' => esc_url_raw(WC()->api_request_url('PayplugGateway')),
358 'hosted_payment' => [
359 'return_url' => $return_url,
360 'cancel_url' => esc_url_raw($order->get_cancel_order_url_raw()),
361 ],
362 'metadata' => [
363 'order_id' => $order_id,
364 'customer_id' => ((int) $customer_id > 0) ? $customer_id : 'guest',
365 'domain' => $this->limit_length(esc_url_raw(home_url()), 500),
366 ],
367 ];
368
369 if (PayplugWoocommerceHelper::is_checkout_block() && is_checkout()) {
370 $payment_data['metadata']['woocommerce_block'] = 'CHECKOUT';
371 } elseif (PayplugWoocommerceHelper::is_cart_block() && is_cart()) {
372 $payment_data['metadata']['woocommerce_block'] = 'CART';
373 }
374
375 /**
376 * Filter the payment data before it's used
377 *
378 * @param array $payment_data
379 * @param int $order_id
380 * @param array $customer_details
381 * @param PayplugAddressData $address_data
382 */
383 $payment_data = apply_filters('payplug_gateway_payment_data', $payment_data, $order_id, [], $address_data);
384 $payment = $this->payplug_api->payment_create($payment_data);
385
386 // Save transaction id for the order
387 PayplugWoocommerceHelper::is_pre_30()
388 ? update_post_meta($order_id, '_transaction_id', $payment->id)
389 : $order->set_transaction_id($payment->id);
390
391 if (is_callable([$order, 'save'])) {
392 $order->save();
393 }
394
395 /**
396 * Fires once a payment has been created.
397 *
398 * @param int $order_id Order ID
399 * @param PaymentResource $payment Payment resource
400 */
401 \do_action('payplug_gateway_payment_created', $order_id, $payment);
402
403 $metadata = PayplugWoocommerceHelper::extract_transaction_metadata($payment);
404 PayplugWoocommerceHelper::save_transaction_metadata($order, $metadata);
405
406 PayplugGateway::log(sprintf('Payment creation complete for order #%s', $order_id));
407
408 return [
409 'result' => 'success',
410 'redirect' => $payment->hosted_payment->payment_url,
411 'cancel' => $payment->hosted_payment->cancel_url,
412 ];
413 } catch (\HttpException $e) {
414 PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error');
415 throw new \Exception(__($e->getMessage(), 'payplug'));
416 } catch (\Exception $e) {
417 PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, $e->getMessage()), 'error');
418 throw new \Exception(__($e->getMessage(), 'payplug'));
419 }
420 }
421
422 /**
423 * Check if the gatteway is allowed for the order amount
424 *
425 * @param array
426 *
427 * @return array
428 */
429 public function check_gateway($gateways)
430 {
431 if (isset($gateways[$this->id]) && $gateways[$this->id]->id == $this->id) {
432 //remove gateway if thresholds/country criteria are not met
433 if ($this->check_oney_is_available() === false) {
434 unset($gateways[$this->id]);
435 }
436
437 //remove gateway if account doesn't have permission
438 if (!PayplugWoocommerceHelper::is_oney_available()) {
439 unset($gateways[$this->id]);
440 } else {
441 $gateways = parent::check_gateway($gateways);
442 }
443 }
444
445 return $gateways;
446 }
447
448 /**
449 * Show Oney refund text
450 *
451 * @return void
452 */
453 public function oney_refund_text($order): void
454 {
455 if ($this->id === $order->get_payment_method() && parent::can_refund_order($order) && $order->get_status() !== 'refunded' && $this->payplug_api) {
456 $order_metadata = $order->get_meta('_payplug_metadata');
457
458 if (is_array($order_metadata) && !empty($order_metadata['transaction_id'])) {
459 try {
460 $payment = $this->payplug_api->payment_retrieve($order_metadata['transaction_id']);
461 $today = current_time('Y-m-d H:i:s');
462 $can_refund_date = date('Y-m-d H:i:s', $payment->__get('refundable_after'));
463 if ($can_refund_date >= $today) {
464 echo "<p style='color: red;'>" . __('Refund will be possible 48 hours after the last payment or refund transaction.', 'payplug') . '</p>';
465 }
466 } catch (\Exception $e) {
467 PayplugGateway::log(sprintf('Could not retrieve refundable_after for Oney order: %s', $e->getMessage()), 'error');
468 }
469 }
470 }
471 }
472
473 /**
474 * Billing and shipping addresses should have the same country and allowed by Oney
475 * https://payplug-prod.atlassian.net/browse/WOOC-227
476 *
477 * @param $order
478 *
479 * @return bool
480 */
481 public function validate_shipping_billing_country($shipping_country, $billing_country)
482 {
483 if ($billing_country === $shipping_country) {
484 return true;
485 }
486
487 return false;
488 }
489
490 /**
491 * Country of billing or shipping at oney payments should be on the allowedCountry that comes from pauyplug-api /account
492 *
493 * @param string $country
494 * @param array $allowed
495 *
496 * @return bool
497 */
498 public function allowed_country($country, $allowed)
499 {
500 if (in_array($country, $allowed)) {
501 return true;
502 }
503
504 return false;
505 }
506
507 /**
508 * Empty the cart and add all order_items
509 *
510 * @param $cart
511 * @param $items
512 *
513 * @return void
514 */
515 private function order_items_to_cart($cart, $items): void
516 {
517 $cart->empty_cart();
518 foreach ($items as $item) {
519 $cart->add_to_cart($item->get_product_id(), $item->get_quantity(), $item->get_variation_id());
520 }
521 }
522
523 public function checkGateway()
524 {
525 $options = PayplugWoocommerceHelper::get_payplug_options();
526
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;
532 }
533 }
534