PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.1.10
Pay with Vipps and MobilePay for WooCommerce v6.1.10
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 5.4.3 5.4.2 All 187 releases
woo-vipps / recurring / includes / wc-gateway-vipps-recurring.php

wc-gateway-vipps-recurring.php in Pay with Vipps and MobilePay for WooCommerce 6.1.10, at recurring/includes/wc-gateway-vipps-recurring.php

3,309 lines 121.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use Automattic\WooCommerce\Enums\OrderStatus;
4
5 defined( 'ABSPATH' ) || exit;
6
7 require_once( __DIR__ . '/wc-vipps-models.php' );
8 require_once( __DIR__ . '/wc-vipps-recurring-api.php' );
9
10 /**
11 * WC_Gateway_Vipps class.
12 *
13 * @extends WC_Payment_Gateway
14 */
15 class WC_Gateway_Vipps_Recurring extends WC_Payment_Gateway {
16 /**
17 * Which brand to use, will be one of: vipps, mobilepay
18 */
19 public string $brand;
20
21 /**
22 * Vipps MobilePay merchant serial number
23 */
24 public string $merchant_serial_number;
25
26 /**
27 * Whether Vipps MobilePay Checkout is enabled
28 */
29 public bool $checkout_enabled;
30
31 public string $order_prefix;
32
33 /**
34 * Is test mode active?
35 */
36 public bool $test_mode;
37
38 /**
39 * The default status to give pending renewals
40 */
41 public string $default_renewal_status;
42
43 /**
44 * The default status pending orders that have yet to be captured (reserved charges in Vipps/MobilePay) should be given
45 */
46 public string $default_reserved_charge_status;
47
48 /**
49 * Status where when transitioned to we will attempt to capture the payment
50 */
51 public array $statuses_to_attempt_capture;
52
53 /**
54 * Transition the order status to 'completed' when a renewal order has been charged successfully
55 * regardless of previous status
56 */
57 public bool $transition_renewals_to_completed;
58
59 /**
60 * The amount of charges to check in wp-cron at a time
61 */
62 public int $check_charges_amount;
63
64 /**
65 * The sort order in which we check charges in wp-cron
66 */
67 public string $check_charges_sort_order;
68
69 /**
70 * The reference the *Singleton* instance of this class
71 */
72 private static ?WC_Gateway_Vipps_Recurring $instance = null;
73
74 public WC_Vipps_Recurring_Api $api;
75
76 public ?bool $use_high_performance_order_storage = null;
77
78 public bool $auto_capture_mobilepay = false;
79
80 public ?string $continue_shopping_link_page = null;
81
82 /**
83 * Returns the *Singleton* instance of this class.
84 *
85 * @return WC_Gateway_Vipps_Recurring The *Singleton* instance.
86 */
87 public static function get_instance(): WC_Gateway_Vipps_Recurring {
88 if ( null === self::$instance ) {
89 self::$instance = new self();
90 }
91
92 return self::$instance;
93 }
94
95 /**
96 * Constructor
97 */
98 public function __construct() {
99 $this->id = 'vipps_recurring';
100 $this->method_title = __( 'Vipps/MobilePay Recurring Payments', 'woo-vipps' );
101 $this->method_description = __( 'Vipps/MobilePay Recurring Payments works by redirecting your customers to the Vipps MobilePay portal for confirmation. It creates a payment plan and charges your users on the intervals you specify.', 'woo-vipps' );
102 $this->has_fields = true;
103
104 /*
105 * Do not add 'multiple_subscriptions' to $supports.
106 * Vipps/MobilePay Recurring API does not have any concept of multiple line items at the time of writing this.
107 * It could technically be possible to support this, but it's very confusing for a customer in the Vipps/MobilePay app.
108 * There are a lot of edge cases to think about in order to support this functionality too,
109 * 'process_payment' would have to be rewritten entirely.
110 */
111 $this->supports = [
112 'products',
113 'subscriptions',
114 'refunds',
115 'subscription_cancellation',
116 'subscription_suspension',
117 'subscription_reactivation',
118 'subscription_amount_changes',
119 'subscription_date_changes',
120 'subscription_payment_method_change',
121 'subscription_payment_method_change_customer',
122 'subscription_payment_method_change_admin',
123 ];
124
125 // Load the form fields.
126 $this->init_form_fields();
127
128 // Load the settings.
129 $this->init_settings();
130
131 $this->brand = $this->get_option( 'brand' );
132 $this->title = $this->get_form_fields()['brand']['options'][ $this->brand ];
133 $this->description = str_replace( '{brand}', $this->title, $this->get_option( 'description' ) );
134 $this->enabled = $this->get_option( 'enabled' );
135 $this->test_mode = $this->get_option( 'test_mode' ) === "yes";
136 $this->merchant_serial_number = $this->get_option( 'merchant_serial_number' );
137 $this->checkout_enabled = $this->get_option( 'checkout_enabled' ) === "yes";
138 $this->order_prefix = $this->get_option( 'order_prefix' );
139 $this->default_renewal_status = $this->get_option( 'default_renewal_status' );
140 $this->default_reserved_charge_status = $this->get_option( 'default_reserved_charge_status' );
141 $this->transition_renewals_to_completed = $this->get_option( 'transition_renewals_to_completed' ) === "yes";
142 $this->check_charges_amount = $this->get_option( 'check_charges_amount' );
143 $this->check_charges_sort_order = $this->get_option( 'check_charges_sort_order' );
144 $this->auto_capture_mobilepay = $this->get_option( 'auto_capture_mobilepay' ) === "yes";
145 $this->continue_shopping_link_page = $this->get_option( 'continue_shopping_link_page' );
146
147 if ( WC_VIPPS_RECURRING_TEST_MODE ) {
148 $this->test_mode = true;
149 }
150
151 if ( $this->test_mode ) {
152 $this->merchant_serial_number = $this->get_option( 'test_merchant_serial_number' );
153 }
154
155 // translators: %s: brand name, Vipps or MobilePay
156 $this->order_button_text = sprintf( __( 'Pay with %s', 'woo-vipps' ), $this->title );
157
158 $this->api = new WC_Vipps_Recurring_Api( $this );
159
160 /*
161 * When transitioning an order to these statuses we should
162 * automatically try to capture the charge if it's not already captured
163 */
164 $capture_statuses = [
165 'completed',
166 'processing'
167 ];
168
169 /*
170 * We have to remove the status corresponding to `$this->default_reserved_charge_status` otherwise we end up
171 * prematurely capturing this reserved Vipps/MobilePay charge
172 */
173 $capture_status_transition_id = array_search( str_replace( 'wc-', '', $this->default_reserved_charge_status ), $capture_statuses, true );
174 if ( $capture_status_transition_id ) {
175 unset( $capture_statuses[ $capture_status_transition_id ] );
176 }
177
178 $this->statuses_to_attempt_capture = apply_filters( 'wc_vipps_recurring_captured_statuses', $capture_statuses );
179
180 // If we change a status that is currently on-hold to any of the $capture_statuses we should attempt to capture it
181 foreach ( $this->statuses_to_attempt_capture as $status ) {
182 add_action( 'woocommerce_order_status_' . $status, [ $this, 'maybe_capture_payment' ] );
183 }
184
185 add_filter( 'woocommerce_valid_order_statuses_for_payment_complete', [
186 $this,
187 'append_valid_statuses_for_payment_complete'
188 ] );
189
190 // Auto cancellation flow
191 add_action( 'woocommerce_subscription_status_pending-cancel_to_cancelled', [
192 $this,
193 'maybe_delete_subscription'
194 ], 99999 );
195 add_action( 'woocommerce_subscription_status_pending_to_cancelled', [
196 $this,
197 'maybe_delete_subscription'
198 ], 99999 );
199 add_action( 'woocommerce_new_subscription', [ $this, 'maybe_delete_subscription_later' ] );
200
201 // Hijack cancellation email
202 remove_action( 'woocommerce_subscription_status_updated', [
203 WC_Subscriptions_Email::class,
204 'send_cancelled_email'
205 ], 10 );
206 add_action( 'woocommerce_subscription_status_updated', [ $this, 'overwrite_send_cancelled_email' ], 10, 2 );
207 add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [
208 $this,
209 'process_admin_options'
210 ] );
211
212 add_action( 'woocommerce_account_view-order_endpoint', [ $this, 'check_charge_status' ], 1 );
213
214 add_action( 'set_logged_in_cookie', [ $this, 'set_cookie_on_current_request' ] );
215
216 add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, [
217 $this,
218 'scheduled_subscription_payment'
219 ], 1, 2 );
220
221 add_action( 'woocommerce_subscription_cancelled_' . $this->id, [
222 $this,
223 'maybe_cancel_subscription',
224 ] );
225
226 add_action( 'woocommerce_before_thankyou', [ $this, 'maybe_process_redirect_order' ], 1 );
227
228 add_action( 'woocommerce_subscription_failing_payment_method_updated_' . $this->id, [
229 $this,
230 'update_failing_payment_method'
231 ], 10, 2 );
232
233 /*
234 * When changing the payment method for a WooCommerce Subscription to Vipps MobilePay, let WooCommerce Subscription
235 * know that the payment method for that subscription should not be changed immediately. Instead, it should
236 * wait for the go ahead in cron, after the user confirmed the payment method change with Vipps MobilePay.
237 */
238 add_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [
239 $this,
240 'indicate_async_payment_method_update'
241 ], 10, 2 );
242
243 // Tell WooCommerce about our custom payment meta fields
244 add_action( 'woocommerce_subscription_payment_meta', [ $this, 'add_subscription_payment_meta' ], 10, 2 );
245
246 // Validate custom payment meta fields
247 add_action( 'woocommerce_subscription_validate_payment_meta', [
248 $this,
249 'validate_subscription_payment_meta'
250 ], 10, 2 );
251
252 // Handle subscription switches (free upgrades & downgrades)
253 add_action( 'woocommerce_subscriptions_switched_item', [ $this, 'handle_subscription_switch_completed' ] );
254
255 // If we are performing a subscription switch to Vipps Recurring we need to take a payment
256 // todo: Figure out how to force a redirect to Vipps here. We do need to sign a new agreement in a lot of cases...
257 // todo: Vipps has a 10x multiplier limit for the agreement pricing.
258 add_filter( 'woocommerce_cart_needs_payment', [ $this, 'cart_needs_payment' ], 100, 2 );
259
260 /*
261 * Handle in app updates when a subscription status changes, typically when status transitions to
262 * 'pending-cancel', 'cancelled' or 'pending-cancel' to any other status
263 */
264 add_action( 'woocommerce_subscription_status_updated', [
265 $this,
266 'maybe_handle_subscription_status_transitions'
267 ], 10, 3 );
268
269 // Delete idempotency key when renewal/resubscribe happens
270 add_action( 'wcs_resubscribe_order_created', [ $this, 'delete_resubscribe_meta' ] );
271 add_action( 'wcs_renewal_order_created', [ $this, 'delete_renewal_meta' ], 5, 1 );
272
273 // Cancel DUE charge if order transitions to 'cancelled' or 'failed'
274 $cancel_due_charge_statuses = apply_filters( 'wc_vipps_recurring_cancel_due_charge_statuses', [
275 'cancelled',
276 'failed'
277 ] );
278
279 foreach ( $cancel_due_charge_statuses as $status ) {
280 add_action( 'woocommerce_order_status_' . $status, [ $this, 'maybe_cancel_due_charge' ] );
281 }
282
283 add_action( 'woocommerce_payment_complete', [ $this, 'after_renew_early_from_another_gateway' ] );
284
285 add_filter( 'woocommerce_payment_complete_order_status', [
286 $this,
287 'prevent_backwards_transition_on_completed_order'
288 ], 100, 3 );
289
290 add_action( 'woocommerce_order_after_calculate_totals', [ $this, 'update_agreement_price_in_app' ], 10, 2 );
291
292 // Woo Subscriptions uses `wp_safe_redirect()` during a gateway change, which will not allow us to redirect to the Vipps MobilePay API
293 // Unless we whitelist the domains specifically
294 // https://developer.vippsmobilepay.com/docs/knowledge-base/servers/
295 add_filter( 'allowed_redirect_hosts', function ( $hosts ) {
296 return array_merge( $hosts, [
297 // Production servers
298 'api.vipps.no',
299 'api.mobilepay.dk',
300 'api.mobilepay.fi',
301 'pay.vipps.no',
302 'pay.mobilepay.dk',
303 'pay.mobilepay.fi',
304 // MT servers
305 'apitest.vipps.no',
306 'pay-mt.vipps.no',
307 'pay-mt.mobilepay.dk',
308 'pay-mt.mobilepay.fi'
309 ] );
310 } );
311 }
312
313 /**
314 * Indicate to WooCommerce Subscriptions that the payment method change for Vipps/MobilePay Recurring Payments
315 * should be asynchronous.
316 *
317 * WC_Subscriptions_Change_Payment_Gateway::change_payment_method_via_pay_shortcode uses the
318 * result to decide whether to change the payment method information on the subscription
319 * right away or not.
320 *
321 * In our case, the payment method will not be updated until after the user confirms the
322 * payment method change with Vipps MobilePay. Once that's done, we'll take care of finishing
323 * the payment method update with the subscription.
324 *
325 * @param bool $should_update Current value of whether the payment method should be updated immediately.
326 * @param string $new_payment_method The new payment method name.
327 *
328 * @return bool Whether the subscription's payment method should be updated on checkout or async when a response is returned.
329 */
330 public function indicate_async_payment_method_update( bool $should_update, string $new_payment_method ): bool {
331 if ( $this->id === $new_payment_method ) {
332 $should_update = false;
333 }
334
335 return $should_update;
336 }
337
338 /**
339 * @param $subscription
340 * @param $renewal_order
341 */
342 public function update_failing_payment_method( $subscription, $renewal_order ): void {
343 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $renewal_order ) );
344 }
345
346 /**
347 * @param $order_id
348 *
349 * @throws WC_Vipps_Recurring_Exception
350 */
351 public function process_redirect_payment( $order_id ): void {
352 $order = wc_get_order( $order_id );
353 if ( ! is_object( $order ) ) {
354 return;
355 }
356
357 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $order );
358 if ( $payment_method !== $this->id ) {
359 // If this is not the payment method, an agreement would not be available.
360 return;
361 }
362
363 // check latest charge status
364 $status = $this->check_charge_status( $order_id );
365
366 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] process_redirect_payment: charge status is: %s", $order_id, $status ) );
367 }
368
369 /**
370 * @param $order_id
371 *
372 * @throws WC_Vipps_Recurring_Exception
373 */
374 public function maybe_process_redirect_order( $order_id ): void {
375 if ( ! is_order_received_page() ) {
376 return;
377 }
378
379 $order = wc_get_order( $order_id );
380 if ( $order->get_payment_method() !== $this->id ) {
381 return;
382 }
383
384 $this->process_redirect_payment( $order_id );
385 }
386
387 /**
388 * Check if we are using the new HPOS feature from WooCommerce
389 * This function is used for backwards compatibility in certain places
390 */
391 public function use_high_performance_order_storage(): bool {
392 if ( $this->use_high_performance_order_storage === null ) {
393 $this->use_high_performance_order_storage = function_exists( 'wc_get_container' ) && // 4.4.0
394 function_exists( 'wc_get_page_screen_id' ) && // Exists in the HPOS update
395 class_exists( "Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController" ) &&
396 wc_get_container()->get( Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled();
397 }
398
399 return $this->use_high_performance_order_storage;
400 }
401
402 /**
403 * Retrieves the latest charge for an order if any. False if none or invalid agreement id
404 *
405 * @param $order
406 *
407 * @return bool|WC_Vipps_Charge
408 * @throws WC_Vipps_Recurring_Exception
409 */
410 public function get_latest_charge_from_order( $order ) {
411 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $order );
412
413 if ( ! $agreement_id ) {
414 return false;
415 }
416
417 $charge_id = WC_Vipps_Recurring_Helper::get_charge_id_from_order( $order );
418 $charge = false;
419
420 if ( $charge_id ) {
421 try {
422 $charge = $this->api->get_charge( $agreement_id, $charge_id );
423 } catch ( WC_Vipps_Recurring_Temporary_Exception $e ) {
424 // Let the caller decide how to handle transient failures so we don't conflate them with "no charge exists"
425 throw $e;
426 } catch ( Exception $e ) {
427 $charge = $this->get_latest_charge_for_agreement( $order, $agreement_id, $charge_id );
428 }
429 } else {
430 $charge = $this->get_latest_charge_for_agreement( $order, $agreement_id, $charge_id );
431 }
432
433 return $charge;
434 }
435
436 /**
437 * @param object|WC_Order $order
438 * @param string $agreement_id
439 * @param string $charge_id
440 *
441 * @return false|null|WC_Vipps_Charge
442 * @throws WC_Vipps_Recurring_Config_Exception
443 * @throws WC_Vipps_Recurring_Exception
444 * @throws WC_Vipps_Recurring_Temporary_Exception
445 */
446 public function get_latest_charge_for_agreement( $order, string $agreement_id, string $charge_id ) {
447 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Failed checking charge directly for charge: %s and agreement: %s. This might mean we have not set the right charge id somewhere. Finding latest charge instead.', WC_Vipps_Recurring_Helper::get_id( $order ), $charge_id, $agreement_id ) );
448
449 $charges = $this->api->get_charges_for( $agreement_id );
450
451 // return false if there is no charge
452 // this will tell us if this was directly captured or not
453 if ( count( $charges ) === 0 ) {
454 return false;
455 }
456
457 $charge = null;
458 $latestDate = null;
459
460 // find the latest charge by due date
461 foreach ( $charges as $row ) {
462 $currentDate = $row->due;
463
464 if ( $latestDate === null || $currentDate > $latestDate ) {
465 $latestDate = $currentDate;
466 $charge = $row;
467 }
468 }
469
470 return $charge;
471 }
472
473 /**
474 * Receives the agreement associated with an order
475 *
476 * @param object|WC_Order $order
477 *
478 * @return bool|WC_Vipps_Agreement
479 * @throws WC_Vipps_Recurring_Exception
480 */
481 public function get_agreement_from_order( $order ) {
482 $order_agreement_id = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID );
483
484 $subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => 'any' ] );
485 $subscription = array_shift( $subscriptions );
486 $subscription_agreement_id = $subscription ? WC_Vipps_Recurring_Helper::get_meta( $subscription, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID ) : '';
487
488 $primary_id = $order_agreement_id ?: $subscription_agreement_id;
489 $fallback_id = ( $order_agreement_id && $subscription_agreement_id && $subscription_agreement_id !== $order_agreement_id ) ? $subscription_agreement_id : '';
490
491 if ( ! $primary_id ) {
492 return false;
493 }
494
495 try {
496 return $this->api->get_agreement( $primary_id );
497 } catch ( WC_Vipps_Recurring_Exception $e ) {
498 if ( $e->response_code !== 404 || ! $fallback_id ) {
499 return false;
500 }
501
502 try {
503 $agreement = $this->api->get_agreement( $fallback_id );
504 } catch ( Exception $retry_e ) {
505 return false;
506 }
507
508 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Order agreement id %s returned 404, falling back to subscription agreement id %s and updating order meta', WC_Vipps_Recurring_Helper::get_id( $order ), $primary_id, $fallback_id ) );
509
510 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, $fallback_id );
511 $order->save();
512
513 return $agreement;
514 } catch ( Exception $e ) {
515 return false;
516 }
517 }
518
519 /**
520 * @param object|WC_Order $order
521 */
522 public function unlock_order( $order ): void {
523 WC_Vipps_Recurring_Helper::update_meta_data( $order, '_vipps_recurring_locked_for_update_time', 0 );
524 $order->save();
525 }
526
527 /**
528 * @param $order_id
529 * @param bool $skip_lock
530 *
531 * @return string
532 * @throws WC_Vipps_Recurring_Config_Exception
533 * @throws WC_Vipps_Recurring_Exception
534 * @throws WC_Vipps_Recurring_Temporary_Exception
535 */
536 public function check_charge_status( $order_id, bool $skip_lock = false ): string {
537 if ( empty( $order_id ) || absint( $order_id ) <= 0 ) {
538 return 'INVALID';
539 }
540
541 $order = wc_get_order( absint( $order_id ) );
542
543 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $order );
544 if ( $payment_method !== $this->id ) {
545 // if this is not the payment method, an agreement would not be available.
546 return 'INVALID';
547 }
548
549 $is_renewal = wcs_order_contains_renewal( $order );
550
551 // we need to tell WooCommerce that this is in fact a scheduled payment that should be retried in the case of failure.
552 if ( $is_renewal ) {
553 add_filter( 'wcs_is_scheduled_payment_attempt', '__return_true' );
554 }
555
556 if ( ! $this->use_high_performance_order_storage() ) {
557 clean_post_cache( WC_Vipps_Recurring_Helper::get_id( $order ) );
558 }
559
560 // hold on to the lock for 30 seconds
561 $lock = (int) WC_Vipps_Recurring_Helper::get_meta( $order, '_vipps_recurring_locked_for_update_time' );
562 if ( ( $lock && $lock > time() - 30 ) && ! $skip_lock ) {
563 return 'SUCCESS';
564 }
565
566 WC_Vipps_Recurring_Helper::update_meta_data( $order, '_vipps_recurring_locked_for_update_time', time() );
567 $order->save();
568
569 $order_status = $order->get_status();
570
571 if ( $order_status === 'failed' ) {
572 WC_Vipps_Recurring_Helper::set_order_as_not_pending( $order );
573 $this->unlock_order( $order );
574
575 return 'CANCELLED';
576 }
577
578 $agreement = $this->get_agreement_from_order( $order );
579
580 if ( ! $agreement ) {
581 // If there is no agreement we can't complete Checkout orders. Let Checkout deal with this through an action.
582 $order_initial = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_INITIAL );
583
584 if ( $order_initial && $order->get_payment_method() === $this->id ) {
585 do_action( 'wc_vipps_recurring_check_charge_status_no_agreement', $order );
586 }
587
588 $this->unlock_order( $order );
589
590 return 'INVALID';
591 }
592
593 $zero_amount_result = $this->handle_zero_amount_order( $order, $agreement, $is_renewal );
594 if ( $zero_amount_result !== null ) {
595 $this->unlock_order( $order );
596
597 return $zero_amount_result;
598 }
599
600 try {
601 /** @var WC_Vipps_Charge $charge */
602 $charge = $this->get_latest_charge_from_order( $order );
603 } catch ( WC_Vipps_Recurring_Temporary_Exception $e ) {
604 // Transient failure (rate limit, no response). Keep the order in the pending queue and retry on the next tick.
605 $this->unlock_order( $order );
606
607 return 'SUCCESS';
608 }
609
610 if ( ! $charge ) {
611 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Unable to determine latest charge, stopped checking order", $order_id ) );
612
613 WC_Vipps_Recurring_Helper::set_order_as_not_pending( $order );
614 $this->unlock_order( $order );
615
616 return 'CANCELLED';
617 }
618
619 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_ID, $charge->id );
620 WC_Vipps_Recurring_Helper::set_latest_api_status_for_order( $order, $charge->status );
621
622 $initial = empty( WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_INITIAL ) )
623 && ! $is_renewal;
624 $pending_charge = $initial ? 1 : (int) WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING );
625 $did_fail = WC_Vipps_Recurring_Helper::is_charge_failed_for_order( $order );
626
627 if ( ( $charge->status === WC_Vipps_Charge::STATUS_CANCELLED && ! $is_renewal ) || $charge->status === WC_Vipps_Charge::STATUS_REFUNDED ) {
628 WC_Vipps_Recurring_Helper::set_order_as_not_pending( $order );
629 $this->unlock_order( $order );
630
631 return 'CANCELLED';
632 }
633
634 // If payment has already been captured, this function is redundant, unless the charge failed
635 if ( ! $pending_charge && ! $did_fail ) {
636 $this->unlock_order( $order );
637
638 return 'SUCCESS';
639 }
640
641 $is_captured = ! in_array( $charge->status, [
642 WC_Vipps_Charge::STATUS_PENDING,
643 WC_Vipps_Charge::STATUS_RESERVED
644 ], true ) && $agreement->status === WC_Vipps_Agreement::STATUS_ACTIVE;
645
646 $capture_result = $this->attempt_charge_capture( $order, $order_id, $is_captured, $order_status );
647 if ( $capture_result !== null ) {
648 $this->unlock_order( $order );
649
650 return $capture_result;
651 }
652
653 return $this->finalize_charge_check( $order, $charge, $agreement, $initial, $is_captured );
654 }
655
656 /**
657 * Handle the special case where the order's subtotal is zero. There is no charge to look up; the
658 * agreement status alone tells us whether to complete, fail, or wait for the order.
659 *
660 * @return string|null One of 'SUCCESS' / 'CANCELLED' when this branch applies, null otherwise.
661 * @throws WC_Vipps_Recurring_Config_Exception
662 * @throws WC_Vipps_Recurring_Exception
663 * @throws WC_Vipps_Recurring_Temporary_Exception
664 */
665 private function handle_zero_amount_order( $order, WC_Vipps_Agreement $agreement, bool $is_renewal ): ?string {
666 if ( ! WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_ZERO_AMOUNT ) || $is_renewal ) {
667 return null;
668 }
669
670 // if there's a campaign with a price of 0 we can complete the order immediately
671 if ( $agreement->status === WC_Vipps_Agreement::STATUS_ACTIVE ) {
672 $this->complete_order( $order, $agreement->id );
673
674 $order->add_order_note( __( 'The subtotal is zero, the order is free for this subscription period.', 'woo-vipps' ) );
675 $order->save();
676 }
677
678 // if EXPIRED or STOPPED we can fail this order
679 if ( in_array( $agreement->status, [
680 WC_Vipps_Agreement::STATUS_EXPIRED,
681 WC_Vipps_Agreement::STATUS_STOPPED
682 ], true ) ) {
683 $this->check_charge_agreement_cancelled( $order, $agreement );
684
685 return 'CANCELLED';
686 }
687
688 return 'SUCCESS';
689 }
690
691 /**
692 * Decide whether an uncaptured charge should be captured now, skipped (cancelled order), or
693 * left for the normal finalize path. Returns the status string when this branch terminates the
694 * check, or null to continue with finalize_charge_check().
695 *
696 * @return string|null
697 * @throws WC_Vipps_Recurring_Config_Exception
698 * @throws WC_Vipps_Recurring_Exception
699 * @throws WC_Vipps_Recurring_Temporary_Exception
700 */
701 private function attempt_charge_capture( $order, $order_id, bool $is_captured, string $order_status ): ?string {
702 if ( $is_captured ) {
703 return null;
704 }
705
706 // Stop processing if the order has a cancelled status — there's nothing left to capture.
707 if ( $order_status === 'cancelled' ) {
708 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] This order has not been captured, but has a cancelled status. Skipping capture attempt.', WC_Vipps_Recurring_Helper::get_id( $order ) ) );
709 WC_Vipps_Recurring_Helper::set_order_as_not_pending( $order );
710
711 return 'SUCCESS';
712 }
713
714 $is_mobilepay_auto_capture = $this->brand === WC_Vipps_Recurring_Helper::BRAND_MOBILEPAY && $this->auto_capture_mobilepay;
715
716 // MobilePay auto-releases and refunds reservations after 7 days, so we capture eagerly when configured.
717 if ( $is_mobilepay_auto_capture ) {
718 // maybe_capture_payment re-fetches the order, so persist META_CHARGE_ID and META_LATEST_API_STATUS first.
719 $order->save();
720
721 if ( $this->maybe_capture_payment( $order_id ) ) {
722 $order->add_order_note( __( 'MobilePay payments are automatically captured to prevent the payment reservation from automatically getting cancelled after 7 days.', 'woo-vipps' ) );
723
724 return 'SUCCESS';
725 }
726
727 // Capture failed; fall through to finalize so we still record the current charge state.
728 return null;
729 }
730
731 $is_direct_capture = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_DIRECT_CAPTURE );
732 $has_completed_state = $order_status === 'completed';
733
734 // Capture if direct capture is enabled or the order has already been marked Completed.
735 if ( $is_direct_capture || $has_completed_state ) {
736 $order->save();
737 $this->maybe_capture_payment( $order_id );
738
739 return 'SUCCESS';
740 }
741
742 return null;
743 }
744
745 /**
746 * Write the final state for the charge, unlock the order, and hand off to process_order_charge.
747 * Returns 'CANCELLED' if the agreement is expired/stopped, otherwise 'SUCCESS'.
748 */
749 private function finalize_charge_check( $order, WC_Vipps_Charge $charge, WC_Vipps_Agreement $agreement, bool $initial, bool $is_captured ): string {
750 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_CAPTURED, $is_captured );
751
752 if ( $initial ) {
753 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_INITIAL, true );
754 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING, true );
755 }
756
757 $this->unlock_order( $order );
758
759 $order->save();
760
761 if ( ! $this->use_high_performance_order_storage() ) {
762 clean_post_cache( WC_Vipps_Recurring_Helper::get_id( $order ) );
763 }
764
765 $this->process_order_charge( $order, $charge );
766
767 if ( in_array( $agreement->status, [
768 WC_Vipps_Agreement::STATUS_STOPPED,
769 WC_Vipps_Agreement::STATUS_EXPIRED
770 ], true ) ) {
771 $this->check_charge_agreement_cancelled( $order, $agreement, $charge );
772
773 return 'CANCELLED';
774 }
775
776 return 'SUCCESS';
777 }
778
779 /**
780 * @param $order
781 * @param WC_Vipps_Agreement $agreement
782 * @param bool|WC_Vipps_Charge $charge
783 *
784 * @throws WC_Vipps_Recurring_Config_Exception
785 * @throws WC_Vipps_Recurring_Exception
786 * @throws WC_Vipps_Recurring_Temporary_Exception
787 */
788 public function check_charge_agreement_cancelled( $order, WC_Vipps_Agreement $agreement, $charge = false ): void {
789 $order->update_status( 'cancelled', __( 'The agreement was cancelled or expired in Vipps/MobilePay', 'woo-vipps' ) );
790
791 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING, false );
792 $order->save();
793
794 // cancel charge
795 if ( $charge && in_array( $charge->status, [
796 WC_Vipps_Charge::STATUS_DUE,
797 WC_Vipps_Charge::STATUS_PENDING
798 ], true ) ) {
799 $idempotency_key = $this->get_idempotency_key( $order );
800
801 $this->api->cancel_charge( $agreement->id, $charge->id, $idempotency_key );
802 }
803 }
804
805 /**
806 * @param $order
807 * @param $transaction_id
808 */
809 public function complete_order( $order, $transaction_id ): void {
810 $order->payment_complete( $transaction_id );
811
812 // Controlled by the `transition_renewals_to_completed` setting
813 // Only applicable to renewal orders
814 if ( wcs_order_contains_renewal( $order ) && $order->get_status() !== 'completed' ) {
815 $order->update_status( $this->transition_renewals_to_completed ? 'completed' : 'processing' );
816 }
817
818 // Unlock the order and make sure we tell our cronjob to stop periodically checking the status of this order
819 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING, false );
820 $this->unlock_order( $order );
821
822 if ( is_callable( [ $order, 'save' ] ) ) {
823 $order->save();
824 }
825
826 do_action( 'wc_vipps_recurring_after_payment_complete', $order );
827 }
828
829 /**
830 * @param $order
831 * @param WC_Vipps_Charge|null $charge
832 */
833 public function process_order_charge( $order, ?WC_Vipps_Charge $charge = null ): void {
834 if ( ! $charge ) {
835 // No charge
836 return;
837 }
838
839 // If payment has already been completed, this function is redundant.
840 if ( ! WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING ) ) {
841 return;
842 }
843
844 do_action( 'wc_vipps_recurring_before_process_order_charge', $order );
845
846 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_ID, $charge->id );
847 $transaction_id = WC_Vipps_Recurring_Helper::get_transaction_id_for_order( $order );
848
849 // Reduce stock
850 $reduce_stock = $charge->status === WC_Vipps_Charge::STATUS_CHARGED || in_array( $charge->status, [
851 WC_Vipps_Charge::STATUS_DUE,
852 WC_Vipps_Charge::STATUS_PENDING
853 ], true );
854
855 if ( $reduce_stock ) {
856 $order_stock_reduced = WC_Vipps_Recurring_Helper::is_stock_reduced_for_order( $order );
857
858 if ( ! $order_stock_reduced ) {
859 WC_Vipps_Recurring_Helper::reduce_stock_for_order( $order );
860 }
861 }
862
863 // status: CHARGED
864 if ( $charge->status === WC_Vipps_Charge::STATUS_CHARGED ) {
865 $this->complete_order( $order, $charge->id );
866
867 /* translators: Vipps/MobilePay Charge ID */
868 $message = sprintf( __( 'Charge completed (Charge ID: %s)', 'woo-vipps' ), $charge->id );
869 $order->add_order_note( $message );
870
871 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Completed order for charge: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id ) );
872 }
873
874 // status: RESERVED
875 // not auto captured, so we need to put the order status to `$this->default_reserved_charge_status`
876 if ( empty( $transaction_id ) && $charge->status === WC_Vipps_Charge::STATUS_RESERVED
877 && ! wcs_order_contains_renewal( $order ) ) {
878 WC_Vipps_Recurring_Helper::set_transaction_id_for_order( $order, $charge->id );
879
880 $message = __( 'Waiting for you to capture the payment', 'woo-vipps' );
881 $order->update_status( $this->default_reserved_charge_status, $message );
882 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Charge reserved: %s (%s)', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id, $charge->status ) );
883 }
884
885 // status: RESERVED
886 // Part of a renewal, so we need to put the order status to `$this->default_renewal_status` or completed and charge the payment.
887 if ( empty( $transaction_id ) && $charge->status === WC_Vipps_Charge::STATUS_RESERVED
888 && wcs_order_contains_renewal( $order ) ) {
889 // NOTE: should we respect $order->needs_processing() here?
890 // If the answer is yes, we also need to make sure we create a RESERVE_CAPTURE charge when creating new charges as well.
891 if ( $this->maybe_capture_payment( WC_Vipps_Recurring_Helper::get_id( $order ) ) ) {
892 WC_Vipps_Recurring_Helper::set_transaction_id_for_order( $order, $charge->id );
893 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Charge captured for renewal order: %s (%s)', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id, $charge->status ) );
894
895 $status = $this->transition_renewals_to_completed ? 'completed' : $this->default_renewal_status;
896 $order->update_status( $status );
897 }
898 }
899
900 // status: DUE or PENDING
901 // when DUE, we need to check that it becomes another status in a cron
902 $initial = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_INITIAL )
903 && ! wcs_order_contains_renewal( $order );
904
905 if ( ! $initial && ! $transaction_id && ( $charge->status === WC_Vipps_Charge::STATUS_DUE
906 || ( $charge->status === WC_Vipps_Charge::STATUS_PENDING
907 && wcs_order_contains_renewal( $order ) ) ) ) {
908 WC_Vipps_Recurring_Helper::set_transaction_id_for_order( $order, $charge->id );
909
910 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_CAPTURED, true );
911
912 $order->update_status( $this->default_renewal_status, $this->get_due_charge_note( $charge ) );
913
914 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Charge due or pending: %s (%s)', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id, $charge->status ) );
915 }
916
917 // status: CANCELLED
918 if ( $charge->status === WC_Vipps_Charge::STATUS_CANCELLED && ! wcs_order_contains_renewal( $order ) ) {
919 $order->update_status( 'cancelled', __( 'Vipps/MobilePay payment cancelled.', 'woo-vipps' ) );
920 WC_Vipps_Recurring_Helper::set_order_as_not_pending( $order );
921
922 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Charge cancelled: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id ) );
923 }
924
925 // status: FAILED, or CANCELLED (renewal)
926 if ( in_array( $charge->status, [
927 WC_Vipps_Charge::STATUS_FAILED,
928 WC_Vipps_Charge::STATUS_CANCELLED
929 ], true ) ) {
930 $order->update_status( 'failed', __( 'Vipps/MobilePay payment failed.', 'woo-vipps' ) );
931 WC_Vipps_Recurring_Helper::set_order_charge_failed( $order, $charge );
932
933 // if subscription status is already pending-cancel, we should cancel it completely
934 // this is not WooCommerce Subscription's job as this issue is caused by Vipps' internal retry logic
935 $subscriptions = $this->get_subscriptions_for_order( $order );
936 foreach ( $subscriptions as $subscription ) {
937 if ( $subscription->get_status() !== 'pending-cancel' ) {
938 continue;
939 }
940
941 $subscription->update_status( 'cancelled' );
942 }
943
944 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Charge failed: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id ) );
945 }
946
947 // if status was FAILED, but no longer is
948 if ( $charge->status !== WC_Vipps_Charge::STATUS_FAILED
949 && WC_Vipps_Recurring_Helper::is_charge_failed_for_order( $order )
950 && in_array( $charge->status, [ WC_Vipps_Charge::STATUS_PROCESSING, 'DUE', 'PENDING' ], true ) ) {
951 WC_Vipps_Recurring_Helper::set_order_charge_not_failed( $order, $charge->id );
952 }
953
954 $order->save();
955 }
956
957 /**
958 * Proceed with current request using new login session (to ensure consistent nonce).
959 *
960 * @param $cookie
961 */
962 public function set_cookie_on_current_request( $cookie ): void {
963 $_COOKIE[ LOGGED_IN_COOKIE ] = $cookie;
964 }
965
966 /**
967 * Returns all supported currencies for this payment method.
968 *
969 * @return array
970 * @version 4.0.0
971 * @since 4.0.0
972 */
973 public function get_supported_currency(): array {
974 return apply_filters(
975 'wc_vipps_recurring_supported_currencies',
976 [
977 'NOK',
978 'DKK',
979 'EUR',
980 'SEK'
981 ]
982 );
983 }
984
985 /**
986 * Checks to see if all criteria is met before showing payment method.
987 *
988 * @return bool
989 * @version 4.0.0
990 * @since 4.0.0
991 */
992 public function is_available(): bool {
993 if ( ! in_array( get_woocommerce_currency(), $this->get_supported_currency(), true ) ) {
994 return false;
995 }
996
997 return parent::is_available();
998 }
999
1000 /**
1001 * Triggered when it's time to charge a subscription
1002 *
1003 * @param $amount_to_charge
1004 * @param $order
1005 *
1006 * @return bool
1007 *
1008 * @throws Exception
1009 */
1010 public function scheduled_subscription_payment( $amount_to_charge, $order ): bool {
1011 try {
1012 return $this->process_subscription_payment( $amount_to_charge, $order );
1013 } catch ( Exception $e ) {
1014 // if we reach this point we consider the error to be completely unrecoverable.
1015 $order->update_status( 'failed' );
1016
1017 /* translators: Error message */
1018 $message = sprintf( __( 'Failed creating a charge: %s', 'woo-vipps' ), $e->getMessage() );
1019 $order->add_order_note( $message );
1020
1021 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Error in process_subscription_payment: %s', $order->get_id(), $e->getMessage() ) );
1022
1023 return false;
1024 }
1025 }
1026
1027 /**
1028 * Triggered when a subscription is cancelled
1029 *
1030 * @param WC_Subscription $subscription
1031 *
1032 * @throws WC_Vipps_Recurring_Exception
1033 */
1034 public function maybe_cancel_subscription( $subscription ): void {
1035 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $subscription );
1036 if ( $payment_method !== $this->id ) {
1037 // If this is not the payment method, an agreement would not be available.
1038 return;
1039 }
1040
1041 // Prevent temporary cancellations from reaching this code
1042 $new_status = $subscription->get_status();
1043 if ( $new_status !== 'cancelled' ) {
1044 return;
1045 }
1046
1047 $subscription_id = WC_Vipps_Recurring_Helper::get_id( $subscription );
1048
1049 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $subscription );
1050 if ( $agreement_id === null ) {
1051 return;
1052 }
1053
1054 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] maybe_cancel_subscription for agreement: %s', $subscription_id, $agreement_id ) );
1055
1056 $agreement = $this->api->get_agreement( $agreement_id );
1057
1058 if ( $agreement->status === WC_Vipps_Agreement::STATUS_ACTIVE ) {
1059 $this->maybe_handle_subscription_status_transitions( $subscription, $new_status, 'active' );
1060 $this->maybe_update_subscription_details_in_app( WC_Vipps_Recurring_Helper::get_id( $subscription ) );
1061
1062 // Woo Subscriptions sets the next payment date to 0 when a subscription is cancelled, thus we have to calculate it ourselves.
1063 $last_payment_time = max( $subscription->get_time( 'last_order_date_created' ), $subscription->get_time( 'last_order_date_paid' ) );
1064 $next_payment = wcs_add_time( $subscription->get_billing_interval( 'edit' ), $subscription->get_billing_period( 'edit' ), $last_payment_time, 'offset_site_time' );
1065
1066 if ( ! wp_next_scheduled( 'woocommerce_vipps_recurring_cancel_subscription', [
1067 $subscription_id,
1068 $agreement_id
1069 ] ) ) {
1070 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Scheduled a cancellation event for agreement %s at timestamp: %s', $subscription_id, $agreement_id, $next_payment ) );
1071
1072 wp_schedule_single_event( $next_payment, 'woocommerce_vipps_recurring_cancel_subscription', [
1073 $subscription_id,
1074 $agreement_id
1075 ] );
1076 }
1077 }
1078 }
1079
1080 /**
1081 * @param int $order_id
1082 * @param null $amount
1083 * @param string $reason
1084 *
1085 * @throws Exception
1086 */
1087 public function process_refund( $order_id, $amount = null, $reason = '' ): ?bool {
1088 $order = wc_get_order( $order_id );
1089 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $order );
1090 $charge_id = WC_Vipps_Recurring_Helper::get_charge_id_from_order( $order );
1091
1092 $created = $order->get_date_created( null );
1093 if ( $created ) {
1094 $diff = ( new DateTime() )->diff( $created );
1095
1096 if ( $diff->days > 365 ) {
1097 /* translators: %s is the days as an integer since the order was created */
1098 $err = sprintf( __( 'You cannot refund a charge that was made more than 365 days ago. This order was created %s days ago.', 'woo-vipps' ), $diff->days );
1099 throw new \RuntimeException( $err );
1100 }
1101 }
1102
1103 try {
1104 if ( $amount !== null ) {
1105 $amount = WC_Vipps_Recurring_Helper::get_vipps_amount( $amount );
1106 }
1107
1108 $this->api->refund_charge( $agreement_id, $charge_id, $amount, $reason );
1109
1110 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] process_refund for charge: %s and agreement: %s', $order_id, $charge_id, $agreement_id ) );
1111
1112 return true;
1113 } catch ( WC_Vipps_Recurring_Temporary_Exception $e ) {
1114 $msg = __( 'A temporary error occurred when refunding a payment through Vipps MobilePay. Please ensure the order is refunded manually or reset the order to "Processing" and try again.', 'woo-vipps' );
1115 throw new \RuntimeException( $msg );
1116 } catch ( WC_Vipps_Recurring_Exception $e ) {
1117 // attempt to cancel charge instead
1118 if ( (float) $order->get_remaining_refund_amount() === 0.00 ) {
1119 $idempotency_key = $this->get_idempotency_key( $order );
1120
1121 $this->api->cancel_charge( $agreement_id, $charge_id, $idempotency_key );
1122
1123 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING, false );
1124 $order->save();
1125
1126 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] process_refund cancelled charge instead of refunding: %s and agreement: %s', $order_id, $charge_id, $agreement_id ) );
1127
1128 return true;
1129 }
1130
1131 // if the remaining refund amount is not equal to the amount we're trying to refund
1132 // that probably means we are trying to partially refund a charge that hasn't yet cleared
1133 if ( (float) $order->get_remaining_refund_amount() !== 0.00 ) {
1134 $err = __( 'You can not partially refund a pending or due charge. Please wait till the payment clears first or refund the full amount instead.', 'woo-vipps' );
1135 throw new \RuntimeException( $err );
1136 }
1137
1138 $err = __( 'An unexpected error occurred while refunding a payment in Vipps/MobilePay.', 'woo-vipps' );
1139 throw new \RuntimeException( $err );
1140 }
1141 }
1142
1143 /**
1144 * @param $order
1145 *
1146 * @return mixed|string
1147 */
1148 public function get_idempotency_key( $order ) {
1149 $idempotency_key = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_IDEMPOTENCY_KEY );
1150
1151 if ( ! $idempotency_key ) {
1152 $idempotency_key = $this->generate_idempotency_key( $order );
1153 }
1154
1155 return $idempotency_key;
1156 }
1157
1158 /**
1159 * @param $order
1160 *
1161 * @return string
1162 */
1163 protected function generate_idempotency_key( $order ): string {
1164 $idempotency_key = $this->api->generate_idempotency_key();
1165
1166 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_IDEMPOTENCY_KEY, $idempotency_key );
1167 $order->save();
1168
1169 return $idempotency_key;
1170 }
1171
1172 /**
1173 * @param $amount
1174 * @param WC_Order $renewal_order
1175 *
1176 * @return bool
1177 * @throws WC_Vipps_Recurring_Config_Exception
1178 * @throws WC_Vipps_Recurring_Exception
1179 * @throws WC_Vipps_Recurring_Temporary_Exception
1180 * @throws WC_Data_Exception
1181 */
1182 public function process_subscription_payment( $amount, $renewal_order ): bool {
1183 $renewal_order_id = WC_Vipps_Recurring_Helper::get_id( $renewal_order );
1184
1185 if ( WC_Vipps_Recurring_Helper::order_locked( $renewal_order ) ) {
1186 return true;
1187 }
1188
1189 WC_Vipps_Recurring_Helper::lock_order( $renewal_order );
1190
1191 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] process_subscription_payment attempting to create charge', $renewal_order->get_id() ) );
1192
1193 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $renewal_order );
1194
1195 if ( ! $agreement_id ) {
1196 throw new WC_Vipps_Recurring_Exception( 'Fatal error: Vipps/MobilePay agreement id does not exist.' );
1197 }
1198
1199 $agreement = $this->api->get_agreement( $agreement_id );
1200 $amount = WC_Vipps_Recurring_Helper::get_vipps_amount( $amount );
1201
1202 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] process_subscription_payment on agreement: %s', $renewal_order->get_id(), json_encode( $agreement->to_array() ) ) );
1203
1204 /*
1205 * if this is triggered by the Woo Subscriptions retry system we need to delete the data related to the old payment
1206 * and create an entirely new charge.
1207 */
1208 $charge_has_failed = WC_Vipps_Recurring_Helper::is_charge_failed_for_order( $renewal_order );
1209
1210 // if the previous charge is 'FAILED' we can assume this is an automatic retry instead of a normal renewal process.
1211 if ( $charge_has_failed ) {
1212 // check that the currently attached charge is in fact failed
1213 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] it looks like the charge on agreement: %s failed. Deleting renewal meta and creating a new charge.', $renewal_order->get_id(), $agreement->id ) );
1214
1215 // note: delete transaction id as we use this to determine whether to update the order status in check_charge_status.
1216 $renewal_order->set_transaction_id( 0 );
1217 $renewal_order = $this->delete_renewal_meta( $renewal_order );
1218
1219 $this->generate_idempotency_key( $renewal_order );
1220
1221 // clean the post cache for the renewal order to force Woo to fetch meta again.
1222 if ( ! $this->use_high_performance_order_storage() ) {
1223 clean_post_cache( $renewal_order_id );
1224 }
1225 }
1226
1227 $idempotency_key = $this->get_idempotency_key( $renewal_order );
1228
1229 $vipps_order_id = WC_Vipps_Recurring_Helper::generate_vipps_order_id( $renewal_order, $this->order_prefix );
1230 $charge = $this->api->create_charge( $agreement, $idempotency_key, $renewal_order_id, $vipps_order_id, $amount );
1231
1232 WC_Vipps_Recurring_Helper::update_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_CHARGE_ID, $charge['chargeId'] );
1233 WC_Vipps_Recurring_Helper::set_order_as_pending( $renewal_order, $charge['chargeId'] );
1234
1235 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] process_subscription_payment created charge: %s', $renewal_order->get_id(), json_encode( $charge ) ) );
1236
1237 $charge = $this->api->get_charge( $agreement->id, $charge['chargeId'] );
1238
1239 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] process_subscription_payment fetched charge: %s', $renewal_order->get_id(), json_encode( $charge->to_array() ) ) );
1240
1241 $this->process_order_charge( $renewal_order, $charge );
1242 $renewal_order->save();
1243
1244 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] process_subscription_payment for charge: %s and agreement: %s', $renewal_order->get_id(), $charge->id, $agreement->id ) );
1245
1246 return true;
1247 }
1248
1249 private function get_due_charge_note( WC_Vipps_Charge $charge ): string {
1250 $timestamp_gmt = WC_Vipps_Recurring_Helper::rfc_3999_date_to_unix( $charge->due );
1251 $date_to_display = ucfirst( wcs_get_human_time_diff( $timestamp_gmt ) );
1252
1253 // translators: Vipps/MobilePay Charge ID, human diff timestamp
1254 return sprintf( __( 'Vipps/MobilePay charge created: %1$s. The charge will be complete %2$s.', 'woo-vipps' ), $charge->id, strtolower( $date_to_display ) );
1255 }
1256
1257 /**
1258 * Maybe capture a payment if it has not already been captured
1259 *
1260 * @param $order_id
1261 *
1262 * @return bool
1263 * @throws WC_Vipps_Recurring_Exception
1264 */
1265 public function maybe_capture_payment( $order_id ): bool {
1266 $order = wc_get_order( $order_id );
1267
1268 if ( ! WC_Vipps_Recurring_Helper::can_capture_charge_for_order( $order ) ) {
1269 return false;
1270 }
1271
1272 return $this->capture_payment( $order );
1273 }
1274
1275 /**
1276 * Capture an initial payment manually
1277 *
1278 * @param $order
1279 *
1280 * @return bool
1281 * @throws WC_Vipps_Recurring_Exception
1282 */
1283 public function capture_payment( $order ): bool {
1284 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $order );
1285 $charge_id = WC_Vipps_Recurring_Helper::get_charge_id_from_order( $order );
1286
1287 try {
1288 $agreement = new WC_Vipps_Agreement( [
1289 'id' => $agreement_id
1290 ] );
1291
1292 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Fetching charge to prepare for capture', WC_Vipps_Recurring_Helper::get_id( $order ) ) );
1293 $charge = $this->api->get_charge( $agreement->id, $charge_id );
1294
1295 $idempotency_key = $this->get_idempotency_key( $order );
1296
1297 if ( $charge->status ) {
1298 if ( ! in_array( $charge->status, [
1299 WC_Vipps_Charge::STATUS_RESERVED,
1300 WC_Vipps_Charge::STATUS_PARTIALLY_CAPTURED
1301 ], true ) ) {
1302 if ( $this->complete_duplicate_capture_if_charged( $order, $charge ) ) {
1303 return true;
1304 }
1305
1306 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Charge does not have the status RESERVED or PARTIALLY_CAPTURED for agreement: %s in capture_payment. Found status: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $agreement_id, $charge->status ) );
1307 WC_Vipps_Recurring_Helper::set_order_as_not_pending( $order );
1308
1309 /* translators: %s: The charge's status */
1310 $order->add_order_note( sprintf( __( 'Could not capture charge because the status is not RESERVED or PARTIALLY_CAPTURED. Found status: %s', 'woo-vipps' ), $charge->status ) );
1311 $order->save();
1312
1313 return false;
1314 }
1315 }
1316
1317 $captured_charge = $this->capture_reserved_charge( $charge, $agreement, $order, $idempotency_key );
1318
1319 WC_Vipps_Recurring_Helper::set_order_as_pending( $order, $captured_charge->id );
1320 $order->save();
1321
1322 $this->process_order_charge( $order, $captured_charge );
1323
1324 if ( ! $this->use_high_performance_order_storage() ) {
1325 clean_post_cache( WC_Vipps_Recurring_Helper::get_id( $order ) );
1326 }
1327
1328 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Finished running capture_payment successfully', WC_Vipps_Recurring_Helper::get_id( $order ) ) );
1329
1330 return true;
1331 } catch ( WC_Vipps_Recurring_Temporary_Exception $e ) {
1332 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Temporary error in capture_payment: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $e->getMessage() ) );
1333 $this->admin_error( __( 'Vipps/MobilePay is temporarily unavailable.', 'woo-vipps' ) );
1334
1335 return false;
1336 } catch ( WC_Vipps_Recurring_Config_Exception $e ) {
1337 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Configuration error in capture_payment: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $e->getMessage() ) );
1338 $this->admin_error( $e->getMessage() );
1339
1340 return false;
1341 } catch ( WC_Vipps_Recurring_Exception $e ) {
1342 // Only treat HTTP 400 as terminal here (e.g. trying to capture a charge older than 180 days).
1343 // Other 4xx/unknown errors are re-thrown so they aren't silently swallowed.
1344 if ( $e->response_code !== 400 ) {
1345 throw $e;
1346 }
1347
1348 if ( $this->is_duplicate_capture_amount_error( $e )
1349 && $this->complete_duplicate_capture_after_status_check( $order, $agreement_id, $charge_id ) ) {
1350 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Treating capture_payment HTTP 400 as duplicate capture after charge status re-check: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $e->getMessage() ) );
1351
1352 return true;
1353 }
1354
1355 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Unrecoverable error in capture_payment (HTTP 400): %s', WC_Vipps_Recurring_Helper::get_id( $order ), $e->getMessage() ) );
1356
1357 // Stop the periodic charge-status check from retrying this capture forever.
1358 WC_Vipps_Recurring_Helper::set_order_as_not_pending( $order );
1359
1360 /* translators: %s: error message from Vipps/MobilePay */
1361 $note = sprintf( __( 'Could not capture Vipps/MobilePay charge (HTTP 400): %s', 'woo-vipps' ), $e->getMessage() );
1362
1363 if ( $order->get_status() !== 'completed' ) {
1364 $order->update_status( 'failed', $note );
1365 } else {
1366 $order->add_order_note( $note );
1367 }
1368
1369 $order->save();
1370
1371 return false;
1372 }
1373 }
1374
1375 private function is_duplicate_capture_amount_error( WC_Vipps_Recurring_Exception $e ): bool {
1376 return stripos( $e->getMessage(), 'higher than the remaining capturable amount' ) !== false;
1377 }
1378
1379 private function complete_duplicate_capture_after_status_check( $order, string $agreement_id, string $charge_id ): bool {
1380 try {
1381 return $this->complete_duplicate_capture_if_charged( $order, $this->api->get_charge( $agreement_id, $charge_id ) );
1382 } catch ( Exception $retry_e ) {
1383 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Could not re-check charge status after duplicate capture error: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $retry_e->getMessage() ) );
1384
1385 return false;
1386 }
1387 }
1388
1389 private function complete_duplicate_capture_if_charged( $order, WC_Vipps_Charge $charge ): bool {
1390 if ( $charge->status !== WC_Vipps_Charge::STATUS_CHARGED ) {
1391 return false;
1392 }
1393
1394 WC_Vipps_Recurring_Helper::set_latest_api_status_for_order( $order, $charge->status );
1395 WC_Vipps_Recurring_Helper::set_order_as_pending( $order, $charge->id );
1396 $order->save();
1397
1398 $this->process_order_charge( $order, $charge );
1399
1400 if ( ! $this->use_high_performance_order_storage() ) {
1401 clean_post_cache( WC_Vipps_Recurring_Helper::get_id( $order ) );
1402 }
1403
1404 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Finished capture_payment from already charged charge: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id ) );
1405
1406 return true;
1407 }
1408
1409 /**
1410 * @param WC_Vipps_Charge $charge
1411 * @param WC_Vipps_Agreement $agreement
1412 * @param $order
1413 * @param string $idempotency_key
1414 */
1415 public function capture_reserved_charge( WC_Vipps_Charge $charge, WC_Vipps_Agreement $agreement, $order, string $idempotency_key ): WC_Vipps_Charge {
1416 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Attempting to capture reserve charge: %s for agreement: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id, $agreement->id ) );
1417
1418 $this->api->capture_reserved_charge( $agreement, $charge, $idempotency_key );
1419
1420 // Set the charge status manually as we can safely assume it was charged here. This avoids making another API request.
1421 $charge->set_status( WC_Vipps_Charge::STATUS_CHARGED );
1422
1423 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Captured reserve charge: %s for agreement: %s', WC_Vipps_Recurring_Helper::get_id( $order ), $charge->id, $agreement->id ) );
1424
1425 return $charge;
1426 }
1427
1428 /**
1429 * @param $order
1430 *
1431 * @return array
1432 */
1433 public function get_subscriptions_for_order( $order ): array {
1434 return WC_Vipps_Recurring_Helper::get_subscriptions_for_order( $order );
1435 }
1436
1437 private function end_gateway_change_checking( $subscription ) {
1438 $subscription->delete_meta_data( WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_WAITING_FOR_GATEWAY_CHANGE );
1439 $subscription->delete_meta_data( '_new_agreement_id' );
1440 $subscription->delete_meta_data( '_old_agreement_id' );
1441 }
1442
1443 /**
1444 * @param $subscription_id
1445 *
1446 * @throws WC_Vipps_Recurring_Exception
1447 */
1448 public function maybe_process_gateway_change( $subscription_id ): void {
1449 $subscription = wcs_get_subscription( $subscription_id );
1450
1451 if ( $subscription->meta_exists( '_new_agreement_id' ) ) {
1452 $new_agreement_id = WC_Vipps_Recurring_Helper::get_meta( $subscription, '_new_agreement_id' );
1453
1454 try {
1455 $agreement = $this->api->get_agreement( $new_agreement_id );
1456 } catch ( WC_Vipps_Recurring_Exception $e ) {
1457 if ( $e->response_code === 404 ) {
1458 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Agreement %s not found (404) during gateway change check, ending gateway change checking', WC_Vipps_Recurring_Helper::get_id( $subscription ), $new_agreement_id ) );
1459
1460 $this->end_gateway_change_checking( $subscription );
1461 $subscription->save();
1462
1463 return;
1464 }
1465
1466 throw $e;
1467 }
1468
1469 if ( $agreement->status === WC_Vipps_Agreement::STATUS_ACTIVE ) {
1470 $old_agreement_id = WC_Vipps_Recurring_Helper::get_meta( $subscription, '_old_agreement_id' );
1471
1472 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Processing subscription gateway change with new agreement id: %s and old agreement id: %s', WC_Vipps_Recurring_Helper::get_id( $subscription ), $new_agreement_id, $old_agreement_id ) );
1473
1474 if ( ! empty( $old_agreement_id ) && $new_agreement_id !== $old_agreement_id ) {
1475 try {
1476 $charges = $this->api->get_charges_for( $old_agreement_id );
1477 $pending_charges = array_filter( $charges, function ( WC_Vipps_Charge $charge ) {
1478 return in_array( $charge->status, [
1479 WC_Vipps_Charge::STATUS_PENDING,
1480 WC_Vipps_Charge::STATUS_RESERVED,
1481 WC_Vipps_Charge::STATUS_DUE
1482 ] );
1483 } );
1484
1485 // We should not under any circumstances cancel agreements that still have pending charges when changing gateways.
1486 if ( empty( $pending_charges ) ) {
1487 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Cancelling old agreement id: %s in Vipps/MobilePay due to gateway change', WC_Vipps_Recurring_Helper::get_id( $subscription ), $old_agreement_id ) );
1488
1489 $idempotency_key = $this->get_idempotency_key( $subscription );
1490 $this->api->cancel_agreement( $old_agreement_id, $idempotency_key );
1491 }
1492 } catch ( WC_Vipps_Recurring_Exception $cleanup_e ) {
1493 // If the old agreement is already gone there's nothing to clean up — proceed with the gateway change.
1494 if ( $cleanup_e->response_code !== 404 ) {
1495 throw $cleanup_e;
1496 }
1497
1498 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Old agreement %s no longer exists in Vipps/MobilePay (404), skipping cleanup', WC_Vipps_Recurring_Helper::get_id( $subscription ), $old_agreement_id ) );
1499 }
1500 }
1501
1502 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, $new_agreement_id );
1503
1504 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Subscription gateway change completed', WC_Vipps_Recurring_Helper::get_id( $subscription ) ) );
1505 WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $subscription, $this->id );
1506 $this->end_gateway_change_checking( $subscription );
1507 }
1508
1509 if ( in_array( $agreement->status, [ WC_Vipps_Agreement::STATUS_STOPPED, 'EXPIRED' ], true ) ) {
1510 $subscription->add_order_note( __( 'Payment gateway change request cancelled in Vipps/MobilePay', 'woo-vipps' ) );
1511 }
1512
1513 if ( in_array( $agreement->status, [
1514 WC_Vipps_Agreement::STATUS_STOPPED,
1515 WC_Vipps_Agreement::STATUS_EXPIRED
1516 ], true ) ) {
1517 $this->end_gateway_change_checking( $subscription );
1518 }
1519
1520 $subscription->save();
1521 }
1522 }
1523
1524 /**
1525 * @param $subscription_id
1526 *
1527 * @throws WC_Vipps_Recurring_Config_Exception
1528 * @throws WC_Vipps_Recurring_Exception
1529 * @throws WC_Vipps_Recurring_Temporary_Exception
1530 */
1531 public function maybe_update_subscription_details_in_app( $subscription_id ): void {
1532 $subscription = wcs_get_subscription( $subscription_id );
1533
1534 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $subscription );
1535 if ( $payment_method !== $this->id ) {
1536 return;
1537 }
1538
1539 $agreement = $this->get_agreement_from_order( $subscription );
1540 if ( $agreement !== false && $agreement->status !== WC_Vipps_Agreement::STATUS_ACTIVE ) {
1541 WC_Vipps_Recurring_Helper::set_update_in_app_completed( $subscription );
1542
1543 return;
1544 }
1545
1546 $parent_order = $subscription->get_parent();
1547 $items = array_reverse( $subscription->get_items() );
1548
1549 // we can only ever have one subscription as long as 'multiple_subscriptions' is disabled
1550 $item = array_pop( $items );
1551
1552 if ( ! $item ) {
1553 return;
1554 }
1555
1556 $item_name = $item->get_name();
1557 $parent_product = wc_get_product( $item->get_product_id() );
1558 $product_description = WC_Vipps_Recurring_Helper::get_product_description( $parent_product );
1559
1560 $agreement_description = null;
1561 if ( $prefix = WC_Vipps_Recurring_Helper::get_meta( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP_DESCRIPTION_PREFIX ) ) {
1562 $agreement_description = "[$prefix]";
1563 }
1564
1565 if ( $product_description ) {
1566 $agreement_description .= " $product_description";
1567 }
1568
1569 $updated_agreement = ( new WC_Vipps_Agreement() )
1570 ->set_pricing( ( new WC_Vipps_Agreement_Pricing() )
1571 ->set_amount( WC_Vipps_Recurring_Helper::get_vipps_amount( $subscription->get_total() ) ) )
1572 ->set_product_name( $item_name );
1573
1574 if ( $agreement_description ) {
1575 $updated_agreement = $updated_agreement->set_product_description( $agreement_description );
1576 }
1577
1578 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $subscription );
1579 if ( empty( $agreement_id ) ) {
1580 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $parent_order );
1581 }
1582
1583 if ( $agreement_id ) {
1584 try {
1585 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Agreement updated in app for agreement id: %s', $subscription_id, $agreement_id ) );
1586
1587 $idempotency_key = $this->get_idempotency_key( $subscription );
1588 $this->api->update_agreement( $agreement_id, $updated_agreement, $idempotency_key );
1589 } catch ( Exception $e ) {
1590 // do nothing
1591 }
1592 }
1593
1594 WC_Vipps_Recurring_Helper::set_update_in_app_completed( $subscription );
1595 }
1596
1597 /**
1598 * @param $order
1599 * @param $subscription
1600 * @param bool $is_gateway_change
1601 *
1602 * @return WC_Vipps_Agreement
1603 * @throws WC_Vipps_Recurring_Invalid_Value_Exception
1604 */
1605 public function create_vipps_agreement_from_order( $order, $subscription = null, bool $is_gateway_change = false, bool $is_failed_renewal_order = false ): WC_Vipps_Agreement {
1606 $order_id = WC_Vipps_Recurring_Helper::get_id( $order );
1607
1608 if ( ! $subscription ) {
1609 $subscription = $order;
1610 }
1611
1612 $subscription_id = WC_Vipps_Recurring_Helper::get_id( $subscription );
1613
1614 // This supports not yet having a subscription, purely because of Express and Checkout orders
1615 if ( is_a( $subscription, 'WC_Subscription' ) ) {
1616 $subscription_period = $subscription->get_billing_period();
1617 $subscription_interval = $subscription->get_billing_interval();
1618 } else {
1619 $subscription_groups = $this->create_partial_subscription_groups_from_order( $order );
1620 $items = array_pop( $subscription_groups );
1621 $product = $items[0]->get_product();
1622
1623 $subscription_period = WC_Subscriptions_Product::get_period( $product );
1624 $subscription_interval = WC_Subscriptions_Product::get_interval( $product );
1625 }
1626
1627 $items = array_reverse( $order->get_items() );
1628
1629 $has_more_products = count( $items ) > 1;
1630
1631 // we can only ever have one subscription as long as 'multiple_subscriptions' is disabled, so we can fetch the first subscription
1632 $subscription_items = array_filter( $items, static function ( $item ) {
1633 return apply_filters( 'wc_vipps_recurring_item_is_subscription', WC_Subscriptions_Product::is_subscription( $item['product_id'] ), $item );
1634 } );
1635
1636 $subscription_item = array_pop( $subscription_items );
1637 $product = $subscription_item->get_product();
1638 $parent_product = wc_get_product( $subscription_item->get_product_id() );
1639
1640 $extra_initial_charge_description = '';
1641
1642 if ( $has_more_products ) {
1643 $other_items = array_filter( $items, static function ( $other_item ) use ( $subscription_item ) {
1644 return $subscription_item['product_id'] !== $other_item['product_id'];
1645 } );
1646
1647 foreach ( $other_items as $product_item ) {
1648 $extra_initial_charge_description .= WC_Vipps_Recurring_Helper::get_product_description( $product_item->get_product() ) . ', ';
1649 }
1650
1651 $extra_initial_charge_description = rtrim( $extra_initial_charge_description, ', ' );
1652 }
1653
1654 $is_virtual = $product->is_virtual();
1655 $direct_capture = $parent_product->get_meta( WC_Vipps_Recurring_Helper::META_PRODUCT_DIRECT_CAPTURE ) === 'yes';
1656
1657 $agreement_url = filter_var( get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ), FILTER_VALIDATE_URL )
1658 ? get_permalink( get_option( 'woocommerce_myaccount_page_id' ) )
1659 : wc_get_account_endpoint_url( 'dashboard' );
1660
1661 if ( ! filter_var( $agreement_url, FILTER_VALIDATE_URL ) ) {
1662 $agreement_url = get_home_url();
1663 }
1664
1665 $redirect_url = WC_Vipps_Recurring_Helper::get_payment_redirect_url( $order, $is_gateway_change );
1666
1667 // total no longer returns the order amount when gateway is being changed
1668 $agreement_total = $subscription->get_total( 'code' );
1669
1670 // when we're performing a variation switch we need some special logic in Vipps
1671 $is_subscription_switch = wcs_order_contains_switch( $order );
1672
1673 if ( $is_subscription_switch ) {
1674 $subscription_switch_data = WC_Vipps_Recurring_Helper::get_meta( $order, '_subscription_switch_data' );
1675
1676 if ( isset( $subscription_switch_data[ array_key_first( $subscription_switch_data ) ]['switches'] ) ) {
1677 $switches = $subscription_switch_data[ array_key_first( $subscription_switch_data ) ]['switches'];
1678 $switch_data = $switches[ array_key_first( $switches ) ];
1679 $direction = $switch_data['switch_direction'];
1680
1681 if ( $direction === 'upgrade' ) {
1682 $agreement_total += $order->get_total();
1683 }
1684 }
1685 }
1686
1687 $has_trial = WC_Subscriptions_Product::get_trial_length( $product ) !== 0;
1688 $is_zero_amount = (float) $order->get_total() === 0.0 || $is_gateway_change;
1689 $capture_immediately = $is_virtual || $direct_capture || $is_gateway_change || $is_failed_renewal_order;
1690 $has_synced_product = WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $subscription );
1691
1692 // WC_Subscriptions_Order::get_sign_up_fee() may divide by the original
1693 // order line total, which causes a DivisionByZeroError for subscriptions
1694 // where nothing is charged at sign-up.
1695 $sign_up_fee = 0;
1696 if ( (float) $subscription_item->get_total() !== 0.0 ) {
1697 $sign_up_fee = WC_Subscriptions_Order::get_sign_up_fee( $order );
1698 }
1699
1700 // A synchronized first payment does not need a campaign when nothing is due
1701 // now. Actual promotions and subscription switches still need a campaign,
1702 // even when they reduce the initial total to zero.
1703 $has_campaign = $has_trial || ( $has_synced_product && ! $is_zero_amount ) || $order->get_total_discount() !== 0.00 || $is_subscription_switch || $sign_up_fee;
1704 $has_free_campaign = $is_subscription_switch || $sign_up_fee || $has_synced_product || $has_trial;
1705
1706 // when Prorate First Renewal is set to "Never (charge the full recurring amount at sign-up)" we don't want to have a campaign
1707 // also not when the order total is the same as the agreement total
1708 if ( $has_free_campaign && $has_synced_product && $order->get_total() === $agreement_total ) {
1709 $has_campaign = false;
1710 }
1711
1712 $agreement = ( new WC_Vipps_Agreement() )
1713 ->set_external_id( $subscription_id )
1714 ->set_pricing(
1715 ( new WC_Vipps_Agreement_Pricing() )
1716 ->set_type( WC_Vipps_Agreement_Pricing::TYPE_LEGACY )
1717 ->set_currency( $order->get_currency() )
1718 ->set_amount( apply_filters( 'wc_vipps_recurring_agreement_pricing_amount', WC_Vipps_Recurring_Helper::get_vipps_amount( $agreement_total ), $order ) )
1719 )
1720 ->set_interval(
1721 ( new WC_Vipps_Agreement_Interval() )
1722 ->set_unit( strtoupper( $subscription_period ) )
1723 ->set_count( (int) $subscription_interval )
1724 )
1725 ->set_product_name( $subscription_item->get_name() )
1726 ->set_merchant_agreement_url( apply_filters( 'wc_vipps_recurring_merchant_agreement_url', $agreement_url ) )
1727 ->set_merchant_redirect_url( apply_filters( 'wc_vipps_recurring_merchant_redirect_url', $redirect_url ) );
1728
1729 $product_description = WC_Vipps_Recurring_Helper::get_product_description( $product );
1730 if ( $product_description ) {
1731 $agreement = $agreement->set_product_description( $product_description );
1732 }
1733
1734 // validate phone number and only add it if it's up to Vipps' standard to avoid errors
1735 if ( WC_Vipps_Recurring_Helper::is_valid_phone_number( $order->get_billing_phone() ) ) {
1736 $agreement = $agreement->set_phone_number( $order->get_billing_phone() );
1737 }
1738
1739 if ( ! $is_zero_amount ) {
1740 $initial_charge_description = WC_Vipps_Recurring_Helper::get_product_description( $parent_product );
1741 if ( ! empty( $extra_initial_charge_description ) ) {
1742 $initial_charge_description .= ' + ' . $extra_initial_charge_description;
1743
1744 if ( $has_campaign ) {
1745 $initial_charge_description = $extra_initial_charge_description;
1746 }
1747 }
1748
1749 $vipps_order_id = WC_Vipps_Recurring_Helper::generate_vipps_order_id( $order, $this->order_prefix );
1750
1751 $agreement = $agreement->set_initial_charge(
1752 ( new WC_Vipps_Agreement_Initial_Charge() )
1753 ->set_amount( apply_filters( 'wc_vipps_recurring_agreement_initial_charge_amount', WC_Vipps_Recurring_Helper::get_vipps_amount( $order->get_total() ), $order ) )
1754 ->set_description( empty( $initial_charge_description ) ? $subscription_item->get_name() : $initial_charge_description )
1755 ->set_transaction_type( $capture_immediately ? WC_Vipps_Agreement_Initial_Charge::TRANSACTION_TYPE_DIRECT_CAPTURE : WC_Vipps_Agreement_Initial_Charge::TRANSACTION_TYPE_RESERVE_CAPTURE )
1756 ->set_order_id( $vipps_order_id )
1757 ->set_external_id( $order_id )
1758 );
1759
1760 WC_Vipps_Recurring_Helper::update_meta_data( $order, $capture_immediately ? WC_Vipps_Recurring_Helper::META_ORDER_DIRECT_CAPTURE : WC_Vipps_Recurring_Helper::META_ORDER_RESERVED_CAPTURE, true );
1761 }
1762
1763 if ( $has_campaign ) {
1764 $campaign_price = $has_free_campaign ? 0 : $order->get_total();
1765
1766 $campaign_type = WC_Vipps_Agreement_Campaign::TYPE_PRICE_CAMPAIGN;
1767 $campaign_period = null;
1768
1769 if ( $has_trial ) {
1770 $campaign_type = WC_Vipps_Agreement_Campaign::TYPE_PERIOD_CAMPAIGN;
1771 $campaign_end_date = null;
1772 $campaign_period = ( new WC_Vipps_Agreement_Campaign_Period() )
1773 ->set_count( WC_Subscriptions_Product::get_trial_length( $product ) )
1774 ->set_unit( strtoupper( WC_Subscriptions_Product::get_trial_period( $product ) ) );
1775 } else {
1776 $next_payment = WC_Subscriptions_Product::get_first_renewal_payment_date( $product );
1777 $end_date = WC_Subscriptions_Product::get_expiration_date( $product );
1778
1779 $campaign_end_date = $end_date === 0 ? $next_payment : $end_date;
1780 }
1781
1782 $agreement = $agreement->set_campaign(
1783 ( new WC_Vipps_Agreement_Campaign() )
1784 ->set_type( $campaign_type )
1785 ->set_price( WC_Vipps_Recurring_Helper::get_vipps_amount( $campaign_price ) )
1786 ->set_end( $campaign_end_date )
1787 ->set_period( $campaign_period )
1788 );
1789 }
1790
1791 $order->save();
1792
1793 return apply_filters( 'wc_vipps_recurring_process_payment_agreement', $agreement, $subscription, $order );
1794 }
1795
1796 /**
1797 * Process a payment when checking out in WooCommerce
1798 *
1799 * https://docs.woocommerce.com/document/subscriptions/develop/payment-gateway-integration/
1800 * "Putting it all Together"
1801 *
1802 * @param int $order_id
1803 *
1804 * @throws Exception
1805 */
1806 public function process_payment( $order_id, bool $retry = true, bool $previous_error = false ): array {
1807 $is_gateway_change = wcs_is_subscription( $order_id );
1808
1809 $subscription = null;
1810
1811 $order = wc_get_order( $order_id );
1812 $debug_msg = sprintf( '[%s] process_payment (gateway change: %s)', $order_id, $is_gateway_change ? 'Yes' : 'No' ) . "\n";
1813
1814 $is_failed_renewal_order = wcs_cart_contains_failed_renewal_order_payment() !== false;
1815 $contains_manual_subscription = wcs_order_contains_manual_subscription( $order );
1816
1817 if ( ! $is_gateway_change
1818 && ! $is_failed_renewal_order
1819 && ! $contains_manual_subscription
1820 && ! wcs_order_contains_subscription( $order )
1821 && ! wcs_order_contains_early_renewal( $order ) ) {
1822 return [
1823 'result' => 'fail',
1824 'redirect' => ''
1825 ];
1826 }
1827
1828 // If we have an early renewal order on our hands we should deal with it accordingly
1829 if ( wcs_order_contains_early_renewal( $order ) ) {
1830 $renewal_order = $order;
1831
1832 $subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order );
1833 $subscription = $subscriptions[ array_key_first( $subscriptions ) ];
1834
1835 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $subscription );
1836 if ( $agreement_id ) {
1837 $existing_agreement = $this->api->get_agreement( $agreement_id );
1838
1839 if ( $existing_agreement->status === WC_Vipps_Agreement::STATUS_ACTIVE ) {
1840 WC_Subscriptions_Payment_Gateways::trigger_gateway_renewal_payment_hook( $renewal_order );
1841
1842 // Trigger the subscription payment complete hooks and reset suspension counts and user roles.
1843 $subscription->payment_complete();
1844
1845 wcs_update_dates_after_early_renewal( $subscription, $renewal_order );
1846 wc_add_notice( __( 'Your early renewal order was successful.', 'woocommerce-subscriptions' ) );
1847
1848 $renewal_order = wc_get_order( $renewal_order->get_id() );
1849
1850 return [
1851 'result' => 'success',
1852 'redirect' => $renewal_order->get_view_order_url()
1853 ];
1854 }
1855 }
1856
1857 // We need to update the gateway to Vipps/MobilePay after the payment is completed if an agreement is active
1858 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_RENEWING_WITH_VIPPS, true );
1859 $subscription->save();
1860 }
1861
1862 try {
1863 if ( ! $subscription ) {
1864 $subscription = $order;
1865 }
1866
1867 if ( ! $is_gateway_change ) {
1868 $subscriptions = $this->get_subscriptions_for_order( $order );
1869
1870 // we can only ever have one subscription as long as 'multiple_subscriptions' is disabled
1871 $subscription = $subscriptions[ array_key_first( $subscriptions ) ];
1872 }
1873
1874 /*
1875 * if this order has a PENDING or ACTIVE agreement in Vipps/MobilePay we should not allow checkout anymore
1876 * this will prevent duplicate transactions
1877 */
1878 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $order );
1879 $already_swapping_to_vipps = WC_Vipps_Recurring_Helper::get_meta( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_WAITING_FOR_GATEWAY_CHANGE );
1880
1881 if ( $agreement_id && ( ! $is_gateway_change || $already_swapping_to_vipps ) && ! $is_failed_renewal_order ) {
1882 if ( ! $already_swapping_to_vipps ) {
1883 $existing_agreement = $this->get_agreement_from_order( $order );
1884 } else {
1885 $new_agreement_id = WC_Vipps_Recurring_Helper::get_meta( $subscription, '_new_agreement_id' );
1886 $existing_agreement = $this->api->get_agreement( $new_agreement_id );
1887 }
1888
1889 if ( $existing_agreement->status === WC_Vipps_Agreement::STATUS_PENDING ) {
1890 return [
1891 'result' => 'success',
1892 'redirect' => $existing_agreement->vipps_confirmation_url,
1893 ];
1894 }
1895
1896 if ( $existing_agreement->status === WC_Vipps_Agreement::STATUS_ACTIVE ) {
1897 throw new WC_Vipps_Recurring_Temporary_Exception( __( 'This subscription is already active in Vipps/MobilePay. You can leave this page.', 'woo-vipps' ) );
1898 }
1899 }
1900
1901 $items = array_reverse( $order->get_items() );
1902
1903 /*
1904 * WooCommerce Subscriptions should really deal with this themselves, but when other gateways with support for `multiple_subscriptions`
1905 * are enabled WooCommerce Subscriptions allows this scenario for our plugin too. This is not ideal so let's deny it here.
1906 * There's no good way to make this obvious to a customer in the app, especially if the products have different durations
1907 * i.e. one yearly and one monthly
1908 */
1909 $has_more_products = count( $items ) > 1;
1910 if ( $has_more_products ) {
1911 $counter = 1;
1912
1913 foreach ( $items as $item ) {
1914 if ( ! WC_Subscriptions_Product::is_subscription( $item['product_id'] ) ) {
1915 continue;
1916 }
1917
1918 if ( $counter > 1 ) {
1919 // translators: %s: brand (Vipps or MobilePay)
1920 wc_add_notice( sprintf( __( 'Different subscription products can not be purchased at the same time using %s.', 'woo-vipps' ), $this->title ), 'error' );
1921
1922 return [
1923 'result' => 'fail',
1924 'redirect' => ''
1925 ];
1926 }
1927
1928 $counter ++;
1929 }
1930 }
1931
1932 $agreement = $this->create_vipps_agreement_from_order( $order, $subscription, $is_gateway_change, $is_failed_renewal_order );
1933
1934 $idempotency_key = $this->get_idempotency_key( $order );
1935 if ( $is_gateway_change ) {
1936 $idempotency_key = $this->api->generate_idempotency_key();
1937 }
1938
1939 $response = $this->api->create_agreement( $agreement, $idempotency_key );
1940
1941 $is_subscription_switch = wcs_order_contains_switch( $order );
1942 $is_zero_amount = (float) $order->get_total() === 0.0 || $is_gateway_change;
1943
1944 // mark the old agreement for cancellation to leave no dangling agreements in Vipps
1945 $should_cancel_old = $is_gateway_change || $is_subscription_switch || $is_failed_renewal_order;
1946 if ( $should_cancel_old ) {
1947 if ( $is_gateway_change ) {
1948 /* translators: Vipps/MobilePay Agreement ID */
1949 $message = sprintf( __( 'Request to change gateway to Vipps/MobilePay with agreement ID: %s.', 'woo-vipps' ), $response['agreementId'] );
1950 $subscription->add_order_note( $message );
1951 $debug_msg .= 'Request to change gateway to Vipps' . "\n";
1952 } elseif ( $is_subscription_switch ) {
1953 $debug_msg .= 'Request to switch subscription variation' . "\n";
1954 } else {
1955 $debug_msg .= 'Request to pay for a failed renewal order' . "\n";
1956 }
1957
1958 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, '_old_agreement_id', WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $subscription ) );
1959 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, '_new_agreement_id', $response['agreementId'] );
1960 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_WAITING_FOR_GATEWAY_CHANGE, true );
1961 }
1962
1963 if ( ! $is_gateway_change ) {
1964 if ( ! $should_cancel_old ) {
1965 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, $response['agreementId'] );
1966 }
1967
1968 if ( $is_zero_amount ) {
1969 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_ZERO_AMOUNT, true );
1970 }
1971
1972 if ( $contains_manual_subscription ) {
1973 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, $response['agreementId'] );
1974 $subscription->set_payment_method( $this->id );
1975 $subscription->save();
1976 }
1977
1978 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, $response['agreementId'] );
1979 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING, true );
1980
1981 /* translators: Vipps/MobilePay Agreement ID */
1982 $message = sprintf( __( 'Agreement created: %s. Customer sent to Vipps/MobilePay for confirmation.', 'woo-vipps' ), $response['agreementId'] );
1983 $order->add_order_note( $message );
1984 }
1985
1986 $debug_msg .= sprintf( 'Created agreement with agreement ID: %s', $response['agreementId'] ) . "\n";
1987
1988 if ( isset( $response['chargeId'] ) ) {
1989 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_ID, $response['chargeId'] );
1990 }
1991
1992 WC_Vipps_Recurring_Helper::delete_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_IS_CHECKOUT );
1993 WC_Vipps_Recurring_Helper::delete_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_IS_EXPRESS );
1994 WC_Vipps_Recurring_Helper::delete_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_ORDER_IS_CHECKOUT );
1995 WC_Vipps_Recurring_Helper::delete_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_ORDER_IS_EXPRESS );
1996
1997 // save meta
1998 $order->save();
1999 $subscription->save();
2000
2001 $debug_msg .= sprintf( 'Debug body: %s', json_encode( $agreement->to_array() ) ) . "\n";
2002 $debug_msg .= sprintf( 'Debug response: %s', json_encode( array_merge( $response, [ 'vippsConfirmationUrl' => 'redacted' ] ) ) );
2003 WC_Vipps_Recurring_Logger::log( $debug_msg );
2004
2005 return [
2006 'result' => 'success',
2007 'redirect' => $response['vippsConfirmationUrl'],
2008 ];
2009 } catch ( WC_Vipps_Recurring_Temporary_Exception $e ) {
2010 $error_message = $e->getLocalizedMessage() ?: $e->getMessage();
2011 wc_add_notice( $error_message, 'error' );
2012
2013 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Temporary error in process_payment: %s', $order_id, $e->getMessage() ) );
2014
2015 return [
2016 'result' => 'fail',
2017 'redirect' => '',
2018 ];
2019 } catch ( WC_Vipps_Recurring_Exception $e ) {
2020 wc_add_notice( $e->getLocalizedMessage(), 'error' );
2021
2022 $order->update_status( 'failed' );
2023
2024 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Error in process_payment: %s', $order_id, $e->getMessage() ) );
2025
2026 return [
2027 'result' => 'fail',
2028 'redirect' => '',
2029 ];
2030 }
2031 }
2032
2033 /**
2034 * All payment icons that work with Vipps. Some icons references
2035 * WC core icons.
2036 *
2037 * @return array
2038 * @since 4.1.0 Changed to using img with svg (colored) instead of fonts.
2039 * @since 4.0.0
2040 */
2041 public function payment_icons(): array {
2042 return apply_filters(
2043 'wc_vipps_recurring_payment_icons',
2044 [
2045 'vippsmobilepay' => '<img src="' . WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/images/' . $this->brand . '-mark.svg" class="vipps-recurring-icon" alt="' . $this->title . '" />',
2046 ]
2047 );
2048 }
2049
2050 /**
2051 * Get_icon function.
2052 *
2053 * @return string
2054 * @version 4.0.0
2055 * @since 1.0.0
2056 */
2057 public function get_icon(): string {
2058 $icons = $this->payment_icons();
2059
2060 return apply_filters( 'woocommerce_gateway_icon', $icons['vippsmobilepay'], $this->id );
2061 }
2062
2063 /**
2064 * @param $key
2065 * @param $data
2066 *
2067 * @return false|string
2068 */
2069 public function generate_page_dropdown_html( $key, $data ) {
2070 $field_key = $this->get_field_key( $key );
2071 $defaults = [
2072 'title' => '',
2073 'class' => '',
2074 'type' => 'page_dropdown',
2075 'desc_tip' => false,
2076 'description' => '',
2077 'show_option_none' => ''
2078 ];
2079
2080 $data = wp_parse_args( $data, $defaults );
2081
2082 $dropdown_pages = wp_dropdown_pages( [
2083 'echo' => 0,
2084 'show_option_none' => $data['show_option_none'],
2085 'selected' => $this->get_option( $key ),
2086 'id' => $field_key,
2087 'name' => $field_key,
2088 'class' => $data['class']
2089 ] );
2090
2091 ob_start();
2092 ?>
2093 <tr valign="top">
2094 <th
2095 scope="row"
2096 class="titledesc"
2097 >
2098 <label
2099 for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?><?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
2100 </th>
2101 <td class="forminp">
2102 <fieldset>
2103 <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span>
2104 </legend>
2105 <?php echo $dropdown_pages; ?>
2106 <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
2107 </fieldset>
2108 </td>
2109 </tr>
2110 <?php
2111
2112 return ob_get_clean();
2113 }
2114
2115 /**
2116 * Initialise Gateway Settings Form Fields
2117 */
2118 public function init_form_fields(): void {
2119 $this->form_fields = require( __DIR__ . '/admin/vipps-recurring-settings.php' );
2120
2121 // Set some options and default values
2122 $this->form_fields['brand']['default'] = $this->detect_default_brand();
2123
2124 $this->form_fields['order_prefix']['default'] = WC_Vipps_Recurring::get_instance()->generate_order_prefix();
2125
2126 // Hide checkout unless it is already enabled.
2127 // todo: remove the checkout settings in a future release.
2128 if ( $this->get_option( 'checkout_enabled' ) !== "yes" && ! WC_VIPPS_RECURRING_ALLOW_CHECKOUT_OPTION ) {
2129 foreach ( array_keys( $this->form_fields ) as $key ) {
2130 if ( $key === 'title_checkout' || strpos( $key, 'checkout_' ) === 0 ) {
2131 unset( $this->form_fields[ $key ] );
2132 }
2133 }
2134 }
2135
2136 if ( $this->get_option( 'test_mode' ) === "yes" || WC_VIPPS_RECURRING_TEST_MODE ) {
2137 $this->form_fields['title_test_api'] = [
2138 'type' => 'title',
2139 'title' => __( 'Test API settings', 'woo-vipps' ),
2140 ];
2141
2142 $this->form_fields['test_merchant_serial_number'] = $this->form_fields['merchant_serial_number'];
2143 $this->form_fields['test_client_id'] = $this->form_fields['client_id'];
2144 $this->form_fields['test_secret_key'] = $this->form_fields['secret_key'];
2145 $this->form_fields['test_subscription_key'] = $this->form_fields['subscription_key'];
2146 }
2147 }
2148
2149 function validate_text_field( $key, $value ) {
2150 if ( $key !== 'order_prefix' ) {
2151 return parent::validate_text_field( $key, $value );
2152 }
2153
2154 return preg_replace( '![^a-zA-Z0-9\-]!', '', $value );
2155 }
2156
2157 public function detect_default_brand(): string {
2158 $locale = get_locale();
2159 $store_location = wc_get_base_location();
2160 $store_country = $store_location['country'] ?? '';
2161 $currency = get_woocommerce_currency();
2162
2163 $default_brand = "mobilepay";
2164
2165 // If store location, locale, or currency is Norwegian, use Vipps
2166 if ( $store_country == "NO" || preg_match( "/_NO/", $locale ) || $currency == "NOK" ) {
2167 $default_brand = "vipps";
2168 }
2169
2170 return $default_brand;
2171 }
2172
2173 /**
2174 * @param $what
2175 */
2176 private function admin_error( $what ): void {
2177 add_action( 'admin_notices', static function () use ( $what ) {
2178 echo "<div class='notice notice-error is-dismissible'><p>$what</p></div>";
2179 } );
2180 }
2181
2182 /**
2183 * @param $what
2184 */
2185 private function admin_notify( $what, $type = "info" ): void {
2186 add_action( 'admin_notices', static function () use ( $what, $type ) {
2187 echo "<div class='notice notice-$type is-dismissible'><p>$what</p></div>";
2188 } );
2189 }
2190
2191 public function process_admin_options(): bool {
2192 $saved = parent::process_admin_options();
2193 delete_transient( '_vipps_keyset' ); // Same transient as for the payment api IOK 2024-12-03
2194
2195 $this->init_form_fields();
2196
2197 if ( $this->get_option( 'enabled' ) === "yes" ) {
2198 $this->webhook_initialize();
2199
2200 try {
2201 $this->api->get_access_token( true );
2202 update_option( WC_Vipps_Recurring_Helper::OPTION_CONFIGURED, 1, true );
2203
2204 $this->admin_notify( __( 'Successfully authenticated with the Vipps/MobilePay API', 'woo-vipps' ) );
2205 } catch ( Exception $e ) {
2206 update_option( WC_Vipps_Recurring_Helper::OPTION_CONFIGURED, 0, true );
2207
2208 /* translators: %s: the error message returned from Vipps/MobilePay */
2209 $this->admin_error( sprintf( __( 'Could not authenticate with the Vipps/MobilePay API: %s', 'woo-vipps' ), $e->getMessage() ) );
2210 }
2211 }
2212
2213 $checkout_enabled = $this->get_option( 'checkout_enabled' ) === "yes";
2214 $test_mode = $this->get_option( 'test_mode' ) === "yes" || WC_VIPPS_RECURRING_TEST_MODE;
2215
2216 // Validate that we have an MSN set, as it is required in Checkout.
2217 $merchant_serial_number = $test_mode ? $this->get_option( 'test_merchant_serial_number' ) : $this->get_option( 'merchant_serial_number' );
2218 if ( empty( $merchant_serial_number ) ) {
2219 $this->admin_notify( __( 'You need to provide a Merchant Serial Number before you can enable Checkout.', 'woo-vipps' ), "error" );
2220
2221 // Disable checkout if we do not have an MSN value.
2222 $this->update_option( 'checkout_enabled', 'no' );
2223 update_option( WC_Vipps_Recurring_Helper::OPTION_CHECKOUT_ENABLED, false, true );
2224
2225 return $saved;
2226 }
2227
2228 update_option( WC_Vipps_Recurring_Helper::OPTION_CHECKOUT_ENABLED, $checkout_enabled, true );
2229
2230 if ( $checkout_enabled ) {
2231 WC_Vipps_Recurring::get_instance()->maybe_create_checkout_page();
2232 }
2233
2234 return $saved;
2235 }
2236
2237 /**
2238 * @param $statuses
2239 *
2240 * @return array
2241 */
2242 public function append_valid_statuses_for_payment_complete( $statuses ): array {
2243 $statuses = array_merge( $statuses, $this->statuses_to_attempt_capture );
2244
2245 if ( $this->transition_renewals_to_completed && ! in_array( 'completed', $statuses, true ) ) {
2246 $statuses[] = 'completed';
2247 }
2248
2249 return $statuses;
2250 }
2251
2252 /**
2253 * @param $payment_meta
2254 * @param $subscription
2255 *
2256 * @return mixed
2257 */
2258 public function add_subscription_payment_meta( $payment_meta, $subscription ) {
2259 $payment_meta[ $this->id ] = [
2260 'post_meta' => [
2261 '_agreement_id' => [
2262 'value' => WC_Vipps_Recurring_Helper::get_meta( $subscription, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID ),
2263 'label' => __( 'Vipps/MobilePay Agreement ID', 'woo-vipps' ),
2264 ]
2265 ],
2266 ];
2267
2268 return $payment_meta;
2269 }
2270
2271 /**
2272 * @param $payment_method_id
2273 * @param $payment_meta
2274 *
2275 * @throws Exception
2276 */
2277 public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ): void {
2278 if ( $this->id !== $payment_method_id ) {
2279 return;
2280 }
2281
2282 $agreement_id = $payment_meta['post_meta']['_agreement_id']['value'];
2283
2284 try {
2285 $this->api->get_agreement( $agreement_id );
2286 } catch ( Exception $e ) {
2287 throw new \RuntimeException( __( 'This Vipps/MobilePay agreement ID is invalid.', 'woo-vipps' ) );
2288 }
2289 }
2290
2291 public function cart_needs_payment( $needs_payment, $cart ) {
2292 $cart_switch_items = wcs_cart_contains_switches();
2293
2294 if ( false === $needs_payment && 0 == $cart->total && false !== $cart_switch_items && ! wcs_is_manual_renewal_required() ) {
2295 foreach ( $cart_switch_items as $cart_switch_details ) {
2296 $subscription = wcs_get_subscription( $cart_switch_details['subscription_id'] );
2297
2298 if ( $this->id === $subscription->get_payment_method() ) {
2299 $needs_payment = true;
2300 break;
2301 }
2302 }
2303 }
2304
2305 return $needs_payment;
2306 }
2307
2308 /**
2309 * @param $subscription
2310 */
2311 public function handle_subscription_switch_completed( $subscription ): void {
2312 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $subscription );
2313 if ( $this->id !== $payment_method ) {
2314 return;
2315 }
2316
2317 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Subscription switch completed", WC_Vipps_Recurring_Helper::get_id( $subscription ) ) );
2318
2319 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP, 1 );
2320 WC_Vipps_Recurring_Helper::delete_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP_DESCRIPTION_PREFIX );
2321 $subscription->save();
2322
2323 try {
2324 $this->maybe_update_subscription_details_in_app( WC_Vipps_Recurring_Helper::get_id( $subscription ) );
2325 } catch ( Exception $exception ) {
2326 // do nothing, we don't want to throw an error in the user's face
2327 }
2328 }
2329
2330 /**
2331 * @param WC_Subscription $subscription
2332 * @param $new_status
2333 * @param $old_status
2334 */
2335 public function maybe_handle_subscription_status_transitions( WC_Subscription $subscription, $new_status, $old_status ): void {
2336 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $subscription );
2337
2338 /*
2339 * note: if we reverse the if statement to reduce nesting I fear we may run into an early stop of code execution issue
2340 */
2341 if ( $this->id === $payment_method ) {
2342 $order = wc_get_order( WC_Vipps_Recurring_Helper::get_id( $subscription ) );
2343
2344 if ( $new_status === 'pending-cancel' ) {
2345 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP, 1 );
2346 WC_Vipps_Recurring_Helper::update_meta_data(
2347 $order,
2348 WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP_DESCRIPTION_PREFIX,
2349 __( 'Pending cancellation', 'woo-vipps' )
2350 );
2351
2352 $order->save();
2353 }
2354
2355 if ( $new_status === 'cancelled' ) {
2356 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP, 1 );
2357 WC_Vipps_Recurring_Helper::update_meta_data(
2358 $order,
2359 WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP_DESCRIPTION_PREFIX,
2360 __( 'Cancelled', 'woo-vipps' )
2361 );
2362
2363 $order->save();
2364 }
2365
2366 if ( $new_status === 'on-hold' ) {
2367 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP, 1 );
2368 WC_Vipps_Recurring_Helper::update_meta_data(
2369 $order,
2370 WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP_DESCRIPTION_PREFIX,
2371 __( 'On hold', 'woo-vipps' )
2372 );
2373
2374 $order->save();
2375 }
2376
2377 if ( ( $old_status === 'pending-cancel' || $old_status === 'on-hold' ) && $new_status !== 'cancelled' ) {
2378 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP, 1 );
2379 $order->delete_meta_data( WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP_DESCRIPTION_PREFIX );
2380
2381 $order->save();
2382 }
2383 }
2384 }
2385
2386 /**
2387 * Don't transfer Vipps/MobilePay idempotency key or any other keys unique to a charge or order to resubscribe orders.
2388 *
2389 * @param mixed $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
2390 */
2391 public function delete_resubscribe_meta( $resubscribe_order ): void {
2392 WC_Vipps_Recurring_Helper::delete_meta_data( $resubscribe_order, WC_Vipps_Recurring_Helper::META_CHARGE_ID );
2393 WC_Vipps_Recurring_Helper::delete_meta_data( $resubscribe_order, WC_Vipps_Recurring_Helper::META_CHARGE_CAPTURED );
2394
2395 WC_Vipps_Recurring_Helper::delete_meta_data( $resubscribe_order, WC_Vipps_Recurring_Helper::META_ORDER_INITIAL );
2396 WC_Vipps_Recurring_Helper::delete_meta_data( $resubscribe_order, WC_Vipps_Recurring_Helper::META_ORDER_IS_EXPRESS );
2397 WC_Vipps_Recurring_Helper::delete_meta_data( $resubscribe_order, WC_Vipps_Recurring_Helper::META_ORDER_EXPRESS_AUTH_TOKEN );
2398
2399 $this->delete_renewal_meta( $resubscribe_order );
2400 }
2401
2402 /**
2403 * Don't transfer Vipps/MobilePay idempotency key or any other keys unique to a charge or order to renewal orders.
2404 *
2405 * @param mixed $renewal_order The renewal order
2406 */
2407 public function delete_renewal_meta( $renewal_order ) {
2408 if ( ! $renewal_order instanceof WC_Order ) {
2409 return $renewal_order;
2410 }
2411
2412 // Do not delete the idempotency key if the order has failed previously
2413 $has_failed_previously = WC_Vipps_Recurring_Helper::get_meta( $renewal_order, '_failed_renewal_order' );
2414 if ( $has_failed_previously !== "yes" ) {
2415 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_ORDER_IDEMPOTENCY_KEY );
2416 }
2417
2418 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_CHARGE_FAILED );
2419 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_CHARGE_FAILED_DESCRIPTION );
2420 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_CHARGE_FAILED_REASON );
2421 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_CHARGE_LATEST_STATUS );
2422 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP );
2423 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP_DESCRIPTION_PREFIX );
2424 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_CHARGE_CAPTURED );
2425
2426 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_ORDER_INITIAL );
2427 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_ORDER_IS_EXPRESS );
2428 WC_Vipps_Recurring_Helper::delete_meta_data( $renewal_order, WC_Vipps_Recurring_Helper::META_ORDER_EXPRESS_AUTH_TOKEN );
2429
2430 $renewal_order->save();
2431
2432 return $renewal_order;
2433 }
2434
2435 /**
2436 * @param $order_id
2437 */
2438 public function maybe_cancel_due_charge( $order_id ): void {
2439 $order = wc_get_order( $order_id );
2440
2441 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $order );
2442 if ( $this->id !== $payment_method ) {
2443 return;
2444 }
2445
2446 $agreement_id = WC_Vipps_Recurring_Helper::get_agreement_id_from_order( $order );
2447 $charge_id = WC_Vipps_Recurring_Helper::get_charge_id_from_order( $order );
2448
2449 if ( $agreement_id === null || $charge_id === null ) {
2450 WC_Vipps_Recurring_Helper::delete_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING );
2451
2452 return;
2453 }
2454
2455 $pending_charge = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING );
2456
2457 if ( $pending_charge ) {
2458 try {
2459 if ( get_transient( 'maybe_cancel_due_charge_lock' . $order_id ) ) {
2460 return;
2461 }
2462
2463 set_transient( 'maybe_cancel_due_charge_lock' . $order_id, uniqid( '', true ), 30 );
2464
2465 $charge = $this->api->get_charge( $agreement_id, $charge_id );
2466 if ( in_array( $charge->status, [
2467 WC_Vipps_Charge::STATUS_DUE,
2468 WC_Vipps_Charge::STATUS_PENDING
2469 ], true ) ) {
2470 $idempotency_key = $this->get_idempotency_key( $order );
2471 $this->api->cancel_charge( $agreement_id, $charge_id, $idempotency_key );
2472 $order->add_order_note( __( 'Cancelled due charge in Vipps/MobilePay.', 'woo-vipps' ) );
2473 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Cancelled DUE charge with ID: %s for agreement with ID: %s', $order_id, $charge_id, $agreement_id ) );
2474 }
2475 } catch ( Exception $e ) {
2476 $order->add_order_note( __( 'Could not cancel charge in Vipps/MobilePay. Please manually check the status of this order if you plan to process a new renewal order!', 'woo-vipps' ) );
2477 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Failed cancelling DUE charge with ID: %s for agreement with ID: %s. Error: %s', $order_id, $charge_id, $agreement_id, $e->getMessage() ) );
2478 }
2479 }
2480 }
2481
2482 /**
2483 * When renewing early from a different gateway WooCommerce does not update the gateway for you.
2484 * If we detect that you've renewed early with Vipps/MobilePay and the gateway is not set to Vipps/MobilePay we will
2485 * update the gateway for you.
2486 *
2487 * @param $order_id
2488 *
2489 * @return void
2490 * @throws WC_Vipps_Recurring_Exception
2491 */
2492 public function after_renew_early_from_another_gateway( $order_id ): void {
2493 if ( ! wcs_order_contains_early_renewal( $order_id ) ) {
2494 return;
2495 }
2496
2497 $subscriptions = $this->get_subscriptions_for_order( $order_id );
2498
2499 foreach ( $subscriptions as $subscription ) {
2500 if ( $subscription->get_payment_method() === $this->id ) {
2501 continue;
2502 }
2503
2504 if ( ! WC_Vipps_Recurring_Helper::get_meta( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_RENEWING_WITH_VIPPS ) ) {
2505 continue;
2506 }
2507
2508 $agreement = $this->get_agreement_from_order( $subscription );
2509 if ( ! $agreement || $agreement->status !== WC_Vipps_Agreement::STATUS_ACTIVE ) {
2510 continue;
2511 }
2512
2513 $subscription->set_payment_method( $this->id );
2514 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_RENEWING_WITH_VIPPS, false );
2515 $subscription->save();
2516 }
2517 }
2518
2519 /**
2520 * Prevent an order from being transitioned to "processing" when the status is already "completed"
2521 *
2522 * @param $status
2523 * @param $order_id
2524 * @param $order
2525 *
2526 * @return string|null
2527 */
2528 public function prevent_backwards_transition_on_completed_order( $status, $order_id, $order ): ?string {
2529 if ( $status === 'processing'
2530 && $order->has_status( 'completed' )
2531 && $order->get_payment_method() === $this->id ) {
2532 return 'completed';
2533 }
2534
2535 return $status;
2536 }
2537
2538 public function update_agreement_price_in_app( $and_taxes, $subscription ) {
2539 if ( ! wcs_is_subscription( $subscription ) ) {
2540 return;
2541 }
2542
2543 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $subscription );
2544 if ( $this->id !== $payment_method ) {
2545 return;
2546 }
2547
2548 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Totals were recalculated. Marking subscription for update in the Vipps/MobilePay app.', WC_Vipps_Recurring_Helper::get_id( $subscription ) ) );
2549 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP, 1 );
2550 $subscription->save();
2551 }
2552
2553 /**
2554 * Initialize webhooks if we don't already have one
2555 *
2556 * @throws WC_Vipps_Recurring_Exception
2557 * @throws WC_Vipps_Recurring_Temporary_Exception
2558 * @throws WC_Vipps_Recurring_Config_Exception
2559 */
2560 private function webhook_initialize_initial() {
2561 $msn = $this->merchant_serial_number;
2562
2563 // We cannot use webhooks without the MSN being set.
2564 if ( empty( $msn ) ) {
2565 return;
2566 }
2567
2568 $webhooks = $this->api->get_webhooks( $msn );
2569 $local_webhooks = $this->webhook_get_local();
2570 $callback_url = $this->webhook_callback_url();
2571
2572 // Delete webhooks that are not for our MSN + hostname combination. This prevents us from conflicting with Pay with WooCommerce for Vipps and MobilePay
2573 $deletion_tracker = [];
2574
2575 $webhook_initialized = false;
2576
2577 foreach ( $webhooks['webhooks'] as $webhook ) {
2578 if ( ! isset( $local_webhooks[ $msn ][ $webhook['id'] ] ) ) {
2579 // this webhook is not created by us, so we can continue
2580 continue;
2581 }
2582
2583 // If we have multiple webhooks for our MSN we can delete the other ones. We do not want duplicates.
2584 if ( $webhook_initialized ) {
2585 $deletion_tracker[] = $webhook['id'];
2586 }
2587
2588 $webhook_initialized = true;
2589 }
2590
2591 // Create webhook and save it to WC_Vipps_Recurring_Helper::OPTION_WEBHOOKS
2592 if ( ! $webhook_initialized ) {
2593 $response = $this->api->register_webhook( $msn );
2594
2595 $local_webhooks[ $msn ][ $response['id'] ] = [
2596 'secret' => $response['secret'],
2597 'url' => $callback_url
2598 ];
2599 }
2600
2601 // Delete superfluous webhooks
2602 if ( ! empty( $deletion_tracker ) ) {
2603 foreach ( $deletion_tracker as $id ) {
2604 try {
2605 $this->api->delete_webhook( $msn, $id );
2606 unset( $local_webhooks[ $msn ][ $id ] );
2607 } catch ( Exception $e ) {
2608 WC_Vipps_Recurring_Logger::log( sprintf( 'Failed to delete webhook %s. Error: %s', $id, $e->getMessage() ) );
2609 }
2610 }
2611 }
2612
2613 update_option( WC_Vipps_Recurring_Helper::OPTION_WEBHOOKS, $local_webhooks );
2614 }
2615
2616 /**
2617 * Check that our local webhooks point to this site.
2618 * If they don't we should delete them, otherwise we end up with hitting the limit of 5.
2619 *
2620 * @throws WC_Vipps_Recurring_Exception
2621 * @throws WC_Vipps_Recurring_Temporary_Exception
2622 * @throws WC_Vipps_Recurring_Config_Exception
2623 */
2624 public function webhook_ensure_this_site(): bool {
2625 $msn = $this->merchant_serial_number;
2626
2627 // We cannot use webhooks without the MSN being set.
2628 if ( empty( $msn ) ) {
2629 return false;
2630 }
2631
2632 $local_webhooks = $this->webhook_get_local();
2633 $callback_url = $this->webhook_callback_url();
2634 $callback_url_base = strtok( $callback_url, "?" );
2635
2636 // Make sure we don't do this more than once every 30 minutes, and only if the callback url base changes.
2637 $webhook_transient = "vipps-recurring-webhooks-site";
2638 if ( get_transient( $webhook_transient ) === $callback_url_base ) {
2639 return true;
2640 }
2641
2642 set_transient( $webhook_transient, $callback_url_base, 1800 );
2643
2644 if ( empty( $local_webhooks[ $msn ] ) ) {
2645 return false;
2646 }
2647
2648 $remote_webhooks = $this->api->get_webhooks( $msn );
2649
2650 $ok = true;
2651 foreach ( $local_webhooks[ $msn ] as $webhook_id => $webhook ) {
2652 $webhook_base_url = strtok( $webhook['url'], "?" );
2653
2654 if ( $webhook_base_url === $callback_url_base ) {
2655 continue;
2656 }
2657
2658 // Check that this webhookId indeed exists in $remote_webhooks before we attempt to delete it
2659 $webhook_exists = ! empty( array_filter( $remote_webhooks['webhooks'], function ( $remote_webhook ) use ( $webhook_id ) {
2660 return $remote_webhook['id'] === $webhook_id;
2661 } ) );
2662
2663 if ( $webhook_exists ) {
2664 $this->api->delete_webhook( $msn, $webhook_id );
2665 }
2666
2667 unset( $local_webhooks[ $msn ][ $webhook_id ] );
2668
2669 $ok = false;
2670 }
2671
2672 update_option( WC_Vipps_Recurring_Helper::OPTION_WEBHOOKS, $local_webhooks );
2673
2674 return $ok;
2675 }
2676
2677 /**
2678 * Delete all webhooks during uninstall for this MSN
2679 *
2680 * @throws WC_Vipps_Recurring_Exception
2681 * @throws WC_Vipps_Recurring_Temporary_Exception
2682 * @throws WC_Vipps_Recurring_Config_Exception
2683 */
2684 public function webhook_teardown() {
2685 $msn = $this->merchant_serial_number;
2686
2687 if ( empty( $msn ) ) {
2688 return;
2689 }
2690
2691 $local_webhooks = $this->webhook_get_local();
2692
2693 if ( empty( $local_webhooks[ $msn ] ) ) {
2694 return;
2695 }
2696
2697 $remote_webhooks = $this->api->get_webhooks( $msn );
2698
2699 // We can have multiple MSNs, hence the nested loop
2700 foreach ( $local_webhooks as $webhooks ) {
2701 foreach ( $webhooks as $webhook_id => $webhook ) {
2702 // Check that this webhookId indeed exists in $remote_webhooks before we attempt to delete it
2703 $webhook_exists = ! empty( array_filter( $remote_webhooks['webhooks'], function ( $remote_webhook ) use ( $webhook_id ) {
2704 return $remote_webhook['id'] === $webhook_id;
2705 } ) );
2706
2707 if ( ! $webhook_exists ) {
2708 continue;
2709 }
2710
2711 $this->api->delete_webhook( $msn, $webhook_id );
2712 }
2713 }
2714
2715 delete_option( WC_Vipps_Recurring_Helper::OPTION_WEBHOOKS );
2716 }
2717
2718 public function webhook_get_local(): array {
2719 return get_option( WC_Vipps_Recurring_Helper::OPTION_WEBHOOKS, [ $this->merchant_serial_number => [] ] );
2720 }
2721
2722 public function webhook_initialize() {
2723 try {
2724 $this->webhook_ensure_this_site();
2725 $this->webhook_initialize_initial();
2726 } catch ( Exception $e ) {
2727 WC_Vipps_Recurring_Logger::log( sprintf( 'Failed to initialize webhooks. Error: %s', $e->getMessage() ) );
2728 }
2729 }
2730
2731 public function webhook_callback_url(): string {
2732 $base_url = home_url( '/', 'https' );
2733
2734 $args = [ 'callback' => 'webhook' ];
2735 $action = 'wc_gateway_vipps_recurring';
2736
2737 if ( ! get_option( 'permalink_structure' ) ) {
2738 $args['wc-api'] = $action;
2739 } else {
2740 $base_url = trailingslashit( home_url( "wc-api/$action", 'https' ) );
2741 }
2742
2743 return add_query_arg( $args, $base_url );
2744 }
2745
2746 private function maybe_get_subscription_from_agreement_webhook( array $webhook_data ): ?WC_Subscription {
2747 if ( isset( $webhook_data['agreementId'] ) ) {
2748 $agreement_id = $webhook_data['agreementId'];
2749
2750 $args = [
2751 'subscriptions_per_page' => 1,
2752 'subscription_status' => [ 'active', 'pending', 'on-hold' ],
2753 ];
2754
2755 $args = WC_Vipps_Recurring_Helper::add_meta_query_to_args( $args, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, '=', $agreement_id, $this->use_high_performance_order_storage() );
2756
2757 $subscriptions = wcs_get_subscriptions( $args );
2758
2759 if ( ! empty( $subscriptions ) ) {
2760 return array_pop( $subscriptions );
2761 }
2762 }
2763
2764 // Otherwise we were unable to find the subscription
2765 return null;
2766 }
2767
2768 /**
2769 * @throws WC_Vipps_Recurring_Exception
2770 * @throws WC_Vipps_Recurring_Temporary_Exception
2771 * @throws WC_Vipps_Recurring_Config_Exception
2772 */
2773 public function handle_webhook_callback( array $webhook_data ): void {
2774 WC_Vipps_Recurring_Logger::log( sprintf( "Handling webhook with data: %s", json_encode( $webhook_data ) ) );
2775
2776 $event_type = $webhook_data['eventType'];
2777
2778 if ( in_array( $event_type, [
2779 'recurring.charge-reserved.v1',
2780 'recurring.charge-captured.v1',
2781 'recurring.charge-canceled.v1',
2782 'recurring.charge-failed.v1',
2783 ] ) ) {
2784 $order_id = null;
2785
2786 $args = [
2787 'limit' => 1,
2788 'payment_method' => $this->id,
2789 'order_by' => 'post_date'
2790 ];
2791
2792 $args = WC_Vipps_Recurring_Helper::add_meta_query_to_args( $args, WC_Vipps_Recurring_Helper::META_CHARGE_ID, '=', $webhook_data['chargeId'], $this->use_high_performance_order_storage() );
2793
2794 $orders = wc_get_orders( $args );
2795
2796 $order = array_pop( $orders );
2797
2798 if ( $order ) {
2799 $order_id = WC_Vipps_Recurring_Helper::get_id( $order );
2800 }
2801
2802 if ( empty( $order_id ) ) {
2803 return;
2804 }
2805
2806 $this->check_charge_status( $order_id );
2807 }
2808
2809 // Customers can now cancel their agreements directly from the app.
2810 if ( $event_type === 'recurring.agreement-stopped.v1' ) {
2811 // If the initiator of this webhook is ourselves, we must discard it.
2812 // Otherwise, we risk cancelling payment gateway changes etc.
2813 if ( $webhook_data['actor'] === 'MERCHANT' ) {
2814 return;
2815 }
2816
2817 $subscription = $this->maybe_get_subscription_from_agreement_webhook( $webhook_data );
2818
2819 if ( empty( $subscription ) ) {
2820 return;
2821 }
2822
2823 $message = __( 'Subscription cancelled by the customer via the Vipps MobilePay app.', 'woo-vipps' );
2824 $subscription->set_status( 'cancelled', $message );
2825 $subscription->save();
2826 }
2827 }
2828
2829 public function cart_supports_checkout( $cart = null ) {
2830 if ( ! $cart ) {
2831 $cart = WC()->cart;
2832 }
2833
2834 if ( ! $cart ) {
2835 return false;
2836 }
2837
2838 # Not supported by Vipps MobilePay Checkout
2839 if ( $cart->cart_contents_total <= 0 ) {
2840 return false;
2841 }
2842
2843 $supports = WC_Vipps_Recurring::get_instance()->gateway_should_be_active()
2844 && WC_Subscriptions_Cart::cart_contains_subscription();
2845
2846 return apply_filters( 'wc_vipps_recurring_cart_supports_checkout', $supports, $cart );
2847 }
2848
2849 public function checkout_is_available() {
2850 if ( ! $this->checkout_enabled
2851 || ! $this->is_available()
2852 || ! $this->cart_supports_checkout() ) {
2853 return false;
2854 }
2855
2856 $checkout_id = wc_get_page_id( 'vipps_recurring_checkout' );
2857 if ( ! $checkout_id ) {
2858 return false;
2859 }
2860
2861 if ( ! get_post_status( $checkout_id ) ) {
2862 delete_option( 'woocommerce_vipps_recurring_checkout_page_id' );
2863
2864 return false;
2865 }
2866
2867 return apply_filters( 'wc_vipps_recurring_checkout_available', $checkout_id, $this );
2868 }
2869
2870 // Creating a partial order & subscription
2871 // Heavily based on the single payments plugins logic. The comments are also copied for readability.
2872 /**
2873 * @throws WC_Data_Exception
2874 * @throws Exception
2875 */
2876 public function create_partial_order( $checkout = false ): int {
2877 $order_id = apply_filters( 'woocommerce_create_order', null );
2878 if ( $order_id ) {
2879 return $order_id;
2880 }
2881
2882 // This is necessary for some plugins, like YITH Dynamic Pricing, that adds filters to get_price depending on whether is_checkout is true.
2883 // so basically, since we are impersonating WC_Checkout here, we should define this constant too
2884 wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
2885
2886 // In *some* cases you may need to actually load classes and reload the cart, because some plugins do not load when DOING_AJAX.
2887 do_action( 'wc_vipps_recurring_express_checkout_before_calculate_totals' );
2888
2889 WC()->cart->calculate_fees();
2890 WC()->cart->calculate_shipping();
2891 WC()->cart->calculate_totals();
2892 do_action( 'wc_vipps_recurring_before_create_express_checkout_order', WC()->cart );
2893
2894 $order_id = absint( WC()->session->get( 'order_awaiting_payment' ) );
2895 $cart_hash = WC()->cart->get_cart_hash();
2896 $order = $order_id ? wc_get_order( $order_id ) : null;
2897 $customer_id = absint( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) );
2898
2899 if ( $order ) {
2900 $order_customer_id = $order->get_customer_id( 'edit' );
2901 $belongs_to_customer = ( $customer_id && ( ! $order_customer_id || $order_customer_id === $customer_id ) )
2902 || ( ! $customer_id && ! $order_customer_id );
2903
2904 if ( ! $belongs_to_customer ) {
2905 $order = null;
2906 $order_id = 0;
2907 }
2908 }
2909
2910 /**
2911 * If there is an order pending payment, we can resume it here so
2912 * long as it has not changed. If the order has changed, i.e.
2913 * different items or cost, create a new order. We use a hash to
2914 * detect changes which is based on cart items + order total.
2915 */
2916 if ( $order && $order->has_cart_hash( $cart_hash ) && $order->has_status( array( 'pending', 'failed' ) ) ) {
2917 /**
2918 * Indicates that we are resuming checkout for an existing order (which is pending payment, and which
2919 * has not changed since it was added to the current shopping session).
2920 *
2921 * @param int $order_id The ID of the order being resumed.
2922 *
2923 * @since 3.0.0 or earlier
2924 *
2925 */
2926 do_action( 'woocommerce_resume_order', $order_id );
2927
2928 // Remove all items - we will re-add them later.
2929 $order->remove_order_items();
2930 } else {
2931 $order = new WC_Order();
2932 }
2933
2934 // We store this in the order, so we don't have to access the cart when initiating payment. This allows us to restart orders etc.
2935 $needs_shipping = WC()->cart->needs_shipping();
2936
2937 $order->set_payment_method( $this );
2938
2939 try {
2940 if ( $checkout ) {
2941 $order->update_meta_data( WC_Vipps_Recurring_Helper::META_ORDER_IS_CHECKOUT, 1 );
2942 $order->set_payment_method_title( 'Vipps/MobilePay Recurring Checkout' );
2943 } else {
2944 $order->set_payment_method_title( 'Vipps/MobilePay Recurring Express Checkout' );
2945 }
2946
2947 // We use 'checkout' as the created_via key as per requests, but allow merchants to use their own.
2948 $created_via = apply_filters( 'wc_vipps_recurring_express_checkout_created_via', 'checkout', $order, $checkout );
2949
2950 $order->set_cart_hash( $cart_hash );
2951 $order->set_created_via( $created_via );
2952
2953 $order->update_meta_data( WC_Vipps_Recurring_Helper::META_ORDER_IS_EXPRESS, 1 );
2954
2955 # To help with address fields, scope etc in initiate payment
2956 $order->update_meta_data( WC_Vipps_Recurring_Helper::META_ORDER_NEEDS_SHIPPING, $needs_shipping );
2957
2958 $order->set_customer_id( $customer_id );
2959 $order->set_currency( get_woocommerce_currency() );
2960 $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
2961 $order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
2962 $order->set_customer_user_agent( wc_get_user_agent() );
2963 $order->set_discount_total( WC()->cart->get_discount_total() );
2964 $order->set_discount_tax( WC()->cart->get_discount_tax() );
2965 $order->set_cart_tax( WC()->cart->get_cart_contents_tax() + WC()->cart->get_fee_tax() );
2966
2967 // Use these methods directly - they should be safe.
2968 WC()->checkout->create_order_line_items( $order, WC()->cart );
2969 WC()->checkout->create_order_fee_lines( $order, WC()->cart );
2970 WC()->checkout->create_order_tax_lines( $order, WC()->cart );
2971 WC()->checkout->create_order_coupon_lines( $order, WC()->cart );
2972
2973 if ( $needs_shipping ) {
2974 WC()->checkout->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping->get_packages() );
2975 }
2976
2977 do_action( 'wc_vipps_recurring_before_calculate_totals_partial_order', $order );
2978 $order->calculate_totals();
2979
2980 // Added to support third-party plugins that wants to do stuff with the order before it is saved.
2981 do_action( 'woocommerce_checkout_create_order', $order, array() );
2982
2983 $order_id = $order->save();
2984
2985 do_action( 'wc_vipps_recurring_express_checkout_order_created', $order_id );
2986
2987 // Normally done by the WC_Checkout::create_order method, so call it here too.
2988 do_action( 'woocommerce_checkout_update_order_meta', $order_id, array() );
2989
2990 // // It isn't possible to remove the javascript or 'after order notice' actions, because these are added as closures
2991 // // before anything else is run. But we can disable the hook that saves data.
2992 // if (WC_Gateway_Vipps::instance()->get_option('vippsorderattribution') != 'yes') {
2993 remove_all_filters( 'woocommerce_order_save_attribution_data' );
2994 // }
2995
2996 do_action( 'woocommerce_checkout_order_created', $order );
2997 } catch ( Exception $exception ) {
2998 if ( $order instanceof WC_Order ) {
2999 $order->get_data_store()->release_held_coupons( $order );
3000 do_action( 'woocommerce_checkout_order_exception', $order );
3001 }
3002
3003 // Any errors gets passed upstream
3004 throw $exception;
3005 }
3006
3007 return $order_id;
3008 }
3009
3010 /**
3011 * @throws Exception
3012 */
3013 public function create_or_get_anonymous_system_customer(): WC_Customer {
3014 $customer_id = get_option( WC_Vipps_Recurring_Helper::OPTION_ANONYMOUS_SYSTEM_CUSTOMER_ID );
3015
3016 // Create a user if it does not exist
3017 if ( ! get_user_by( 'ID', $customer_id ) ) {
3018 $email = WC_Vipps_Recurring_Helper::FAKE_USER_EMAIL;
3019
3020 $customer = get_user_by( 'email', $email );
3021 if ( ! $customer ) {
3022 $username = wc_create_new_customer_username( $email );
3023 $customer_id = wc_create_new_customer( $email, $username, null, [
3024 'first_name' => 'Anonymous Vipps MobilePay Customer',
3025 'user_nicename' => __( 'Anonymous Vipps MobilePay Customer', 'woo-vipps' ),
3026 ] );
3027 } else {
3028 $customer_id = $customer->ID;
3029 }
3030
3031 update_option( WC_Vipps_Recurring_Helper::OPTION_ANONYMOUS_SYSTEM_CUSTOMER_ID, $customer_id );
3032 }
3033
3034 return new WC_Customer( $customer_id );
3035 }
3036
3037 public function create_partial_subscription_groups_from_order( WC_Order $order ): array {
3038 $subscription_groups = [];
3039
3040 // Group the order items into subscription groups.
3041 foreach ( $order->get_items() as $item ) {
3042 $product = $item->get_product();
3043
3044 if ( ! WC_Subscriptions_Product::is_subscription( $product ) ) {
3045 continue;
3046 }
3047
3048 $subscription_groups[ wcs_get_subscription_item_grouping_key( $item ) ][] = $item;
3049 }
3050
3051 return $subscription_groups;
3052 }
3053
3054 /**
3055 * Based on WC_REST_Subscriptions_Controller::create_subscriptions_from_order
3056 *
3057 * @throws Exception
3058 */
3059 public function create_partial_subscriptions_from_order( WC_Order $order ) {
3060 $order_id = WC_Vipps_Recurring_Helper::get_id( $order );
3061
3062 if ( ! $order->get_customer_id() ) {
3063 return false;
3064 }
3065
3066 if ( wcs_order_contains_subscription( $order, 'any' ) ) {
3067 return false;
3068 }
3069
3070 $subscriptions = [];
3071 $subscription_groups = $this->create_partial_subscription_groups_from_order( $order );
3072
3073 if ( empty( $subscription_groups ) ) {
3074 return false;
3075 }
3076
3077 foreach ( $subscription_groups as $items ) {
3078 // Get the first item in the group to use as the base for the subscription.
3079 $product = $items[0]->get_product();
3080
3081 $start_date = wcs_get_datetime_utc_string( $order->get_date_created( 'edit' ) );
3082 $subscription = wcs_create_subscription( [
3083 'order_id' => $order_id,
3084 'created_via' => $order->get_created_via( 'edit' ),
3085 'start_date' => $start_date,
3086 'status' => $order->is_paid() ? 'active' : 'pending',
3087 'billing_period' => apply_filters( 'wc_vipps_recurring_checkout_product_billing_period', WC_Subscriptions_Product::get_period( $product ), $product ),
3088 'billing_interval' => apply_filters( 'wc_vipps_recurring_checkout_product_billing_interval', WC_Subscriptions_Product::get_interval( $product ), $product ),
3089 'customer_note' => $order->get_customer_note(),
3090 ] );
3091
3092 if ( is_wp_error( $subscription ) ) {
3093 throw new Exception( $subscription->get_error_message() );
3094 }
3095
3096 wcs_copy_order_address( $order, $subscription );
3097
3098 $subscription->update_dates( [
3099 'trial_end' => WC_Subscriptions_Product::get_trial_expiration_date( $product, $start_date ),
3100 'next_payment' => WC_Subscriptions_Product::get_first_renewal_payment_date( $product, $start_date ),
3101 'end' => WC_Subscriptions_Product::get_expiration_date( $product, $start_date ),
3102 ] );
3103
3104 $subscription->set_payment_method( $order->get_payment_method() );
3105
3106 wcs_copy_order_meta( $order, $subscription, 'subscription' );
3107
3108 // Add items.
3109 $subscription_needs_shipping = false;
3110 foreach ( $items as $item ) {
3111 // Create order line item.
3112 $item_id = wc_add_order_item(
3113 $subscription->get_id(),
3114 [
3115 'order_item_name' => $item->get_name(),
3116 'order_item_type' => $item->get_type(),
3117 ]
3118 );
3119
3120 $subscription_item = $subscription->get_item( $item_id );
3121
3122 wcs_copy_order_item( $item, $subscription_item );
3123
3124 // Don't include sign-up fees or $0 trial periods when setting the subscriptions item totals.
3125 wcs_set_recurring_item_total( $subscription_item );
3126
3127 $subscription_item->save();
3128
3129 // Check if this subscription will need shipping.
3130 if ( ! $subscription_needs_shipping ) {
3131 $product = $item->get_product();
3132
3133 if ( $product ) {
3134 $subscription_needs_shipping = $product->needs_shipping() && ! WC_Subscriptions_Product::needs_one_time_shipping( $product );
3135 }
3136 }
3137 }
3138
3139 // Add coupons.
3140 foreach ( $order->get_coupons() as $coupon_item ) {
3141 $coupon = new WC_Coupon( $coupon_item->get_code() );
3142
3143 try {
3144 // validate_subscription_coupon_for_order will throw an exception if the coupon cannot be applied to the subscription.
3145 WC_Subscriptions_Coupon::validate_subscription_coupon_for_order( true, $coupon, $subscription );
3146
3147 $subscription->apply_coupon( $coupon->get_code() );
3148 } catch ( Exception $e ) {
3149 // Do nothing. The coupon will not be applied to the subscription.
3150 }
3151 }
3152
3153 // Add shipping.
3154 if ( $subscription_needs_shipping ) {
3155 foreach ( $order->get_shipping_methods() as $shipping_item ) {
3156 $rate = new WC_Shipping_Rate( $shipping_item->get_method_id(), $shipping_item->get_method_title(), $shipping_item->get_total(), $shipping_item->get_taxes(), $shipping_item->get_instance_id() );
3157
3158 $item = new WC_Order_Item_Shipping();
3159 $item->set_order_id( $subscription->get_id() );
3160 $item->set_shipping_rate( $rate );
3161
3162 $subscription->add_item( $item );
3163 }
3164 }
3165
3166 // Add fees.
3167 foreach ( $order->get_fees() as $fee_item ) {
3168 if ( ! apply_filters( 'wcs_should_copy_fee_item_to_subscription', true, $fee_item, $subscription, $order ) ) {
3169 continue;
3170 }
3171
3172 $item = new WC_Order_Item_Fee();
3173 $item->set_props(
3174 array(
3175 'name' => $fee_item->get_name(),
3176 'tax_class' => $fee_item->get_tax_class(),
3177 'amount' => $fee_item->get_amount(),
3178 'total' => $fee_item->get_total(),
3179 'total_tax' => $fee_item->get_total_tax(),
3180 'taxes' => $fee_item->get_taxes(),
3181 )
3182 );
3183
3184 $subscription->add_item( $item );
3185 }
3186
3187 // Remove this action to avoid making an unnecessary API request
3188 remove_action( 'woocommerce_order_after_calculate_totals', [
3189 $this,
3190 'update_agreement_price_in_app'
3191 ] );
3192
3193 $subscription->calculate_totals();
3194 $subscription->save();
3195
3196 $subscriptions[] = wcs_get_subscription( $subscription->get_id() );
3197 }
3198
3199 return $subscriptions;
3200 }
3201
3202 public function maybe_delete_subscription_later( $subscription_id ) {
3203 if ( $this->get_option( 'checkout_cleanup_abandoned_orders' ) !== 'yes' ) {
3204 return;
3205 }
3206
3207 if ( ! wp_next_scheduled( 'woocommerce_vipps_recurring_delete_pending_subscription', [ $subscription_id ] ) ) {
3208 wp_schedule_single_event( time() + 7200, 'woocommerce_vipps_recurring_delete_pending_subscription', [ $subscription_id ] );
3209 }
3210 }
3211
3212 public function maybe_delete_subscription( $subscription_id ): bool {
3213 if ( is_a( $subscription_id, 'WC_Subscription' ) ) {
3214 $subscription = $subscription_id;
3215 $subscription_id = $subscription->get_id();
3216 } else {
3217 $subscription = wcs_get_subscription( $subscription_id );
3218 }
3219
3220 if ( ! $subscription ) {
3221 return false;
3222 }
3223
3224 if ( $this->id !== $subscription->get_payment_method() ) {
3225 return false;
3226 }
3227
3228 $express = WC_Vipps_Recurring_Helper::get_meta( $subscription, WC_Vipps_Recurring_Helper::META_ORDER_IS_EXPRESS );
3229 $empty_email = trim( $subscription->get_billing_email() ) === trim( WC_Vipps_Recurring_Helper::FAKE_USER_EMAIL ) || ! $subscription->get_billing_email();
3230
3231 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Running maybe_delete_subscription, (is express: %s, empty email: %s)", $subscription_id, $express ? 'Yes' : 'No', $empty_email ? 'Yes' : 'No' ) );
3232
3233 if ( ! $express ) {
3234 return false;
3235 }
3236
3237 if ( ! $empty_email ) {
3238 return false;
3239 }
3240
3241 if ( $this->get_option( 'checkout_cleanup_abandoned_orders' ) !== 'yes' ) {
3242 return false;
3243 }
3244
3245 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Subscription marked for deletion because it has no billing details after two hours. Billing email was: %s", WC_Vipps_Recurring_Helper::get_id( $subscription ), $subscription->get_billing_email() ) );
3246 $subscription->add_order_note( __( 'This subscription has been automatically marked for deletion as it has no billing details after two hours.', 'woo-vipps' ) );
3247
3248 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_MARKED_FOR_DELETION, 1 );
3249 $subscription->save();
3250
3251 return true;
3252 }
3253
3254 /**
3255 * @throws WC_Vipps_Recurring_Config_Exception
3256 * @throws WC_Vipps_Recurring_Exception
3257 * @throws WC_Vipps_Recurring_Temporary_Exception
3258 */
3259 public function cancel_subscription( $subscription_id, $agreement_id ): void {
3260 $subscription = wcs_get_subscription( $subscription_id );
3261
3262 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] cancel_subscription for agreement: %s', $subscription_id, $agreement_id ) );
3263
3264 // Prevent temporary cancellations from reaching this code
3265 if ( ! $subscription ) {
3266 return;
3267 }
3268
3269 $new_status = $subscription->get_status();
3270 if ( $new_status !== 'cancelled' ) {
3271 return;
3272 }
3273
3274 if ( get_transient( 'cancel_subscription_lock' . $subscription_id ) ) {
3275 return;
3276 }
3277
3278 set_transient( 'cancel_subscription_lock' . $subscription_id, uniqid( '', true ), 30 );
3279
3280 $agreement = $this->api->get_agreement( $agreement_id );
3281
3282 if ( $agreement->status === WC_Vipps_Agreement::STATUS_ACTIVE ) {
3283 $idempotency_key = $this->get_idempotency_key( $subscription );
3284 $this->api->cancel_agreement( $agreement_id, $idempotency_key );
3285 }
3286 }
3287
3288 // If the subscription is about to be deleted, we do not want to send the cancellation email anymore.
3289 public function overwrite_send_cancelled_email( $subscription ) {
3290 // Proxy through to the normal flow if the gateway is not ours.
3291 if ( $this->id !== $subscription->get_payment_method() ) {
3292 WC_Subscriptions_Email::send_cancelled_email( $subscription );
3293
3294 return;
3295 }
3296
3297 // Check if we should delete this subscription
3298 $this->maybe_delete_subscription( $subscription->get_id() );
3299 $subscription = wcs_get_subscription( $subscription->get_id() );
3300
3301 $marked_for_deletion = WC_Vipps_Recurring_Helper::get_meta( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_MARKED_FOR_DELETION );
3302 if ( $marked_for_deletion ) {
3303 return;
3304 }
3305
3306 WC_Subscriptions_Email::send_cancelled_email( $subscription );
3307 }
3308 }
3309