File "class-kp-subscriptions.php"

Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/klarna-payments-for-woocommerce/classes/class-kp-subscriptions.php
File size: 14.28 KB
MIME-type: text/x-php
Charset: utf-8

<?php //phpcs:ignore
/**
 * Class for handling WC subscriptions.
 *
 * @package WC_Klarna_Payments/Classes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class for handling subscriptions.
 */
class KP_Subscription {

	public const GATEWAY_ID      = 'klarna_payments';
	public const RECURRING_TOKEN = '_' . self::GATEWAY_ID . '_recurring_token';

	/**
	 * Register hooks.
	 */
	public function __construct() {
		add_action( 'woocommerce_scheduled_subscription_payment_' . self::GATEWAY_ID, array( $this, 'process_scheduled_payment' ), 10, 2 );
		add_action( 'woocommerce_subscription_cancelled_' . self::GATEWAY_ID, array( $this, 'cancel_scheduled_payment' ) );

		// Set the purchase intent to 'tokenize' for trial subscriptions. The 'buy_and_tokenize' intent is not allowed for 0 order amounts.
		add_filter( 'wc_klarna_payments_create_session_args', array( $this, 'set_tokenize_intent' ) );
		add_filter( 'wc_klarna_payments_place_order_args', array( $this, 'set_tokenize_intent' ) );
		add_filter( 'wc_klarna_payments_create_customer_token_args', array( $this, 'set_tokenize_intent' ) );
		add_filter( 'wc_klarna_payments_update_session_args', array( $this, 'set_tokenize_intent' ) );

		// For free or trial subscription, we set the order as captured to prevent KOM from setting the order to on-hold when the merchant set the order to "Completed".
		add_filter( 'woocommerce_payment_complete', array( $this, 'set_subscription_as_captured' ) );

		// Override the redirect URLs to redirect back to the change payment method page on failure or to the subscription view on success.
		add_filter( 'wc_klarna_payments_create_hpp_args', array( $this, 'set_subscription_order_redirect_urls' ) );
		// Override the subscription cost when change payment method.
		add_filter( 'wc_klarna_payments_create_session_args', array( $this, 'set_subscription_to_free' ) );
		// On successful payment method change, the customer is redirected back to the subscription view page. We need to handle the redirect and create a recurring token.
		add_action( 'woocommerce_account_view-subscription_endpoint', array( $this, 'handle_redirect_from_change_payment_method' ) );

		// Show the recurring token on the subscription page in the billing fields.
		add_action( 'woocommerce_admin_order_data_after_billing_address', array( $this, 'show_recurring_token' ) );
		// Ensure wp_safe_redirect do not redirect back to default dashboard or home page.
		add_filter( 'allowed_redirect_hosts', array( $this, 'extend_allowed_domains_list' ) );
	}

	/**
	 * Flags a free or trial subscription parent order as captured.
	 *
	 * This is required to prevent Klarna Order Management from attempting to process the order for capture when the customer sets the order to completed as there is nothing to capture.
	 *
	 * @param  int $order_id WooCommerce order ID.
	 * @return int WooCommerce order ID.
	 */
	public function set_subscription_as_captured( $order_id ) {
		$order = wc_get_order( $order_id );
		if ( self::GATEWAY_ID !== $order->get_payment_method() ) {
			return $order_id;
		}

		if ( self::order_has_subscription( $order ) && 0.0 === floatval( $order->get_total() ) ) {
			$order->update_meta_data( '_wc_klarna_capture_id', 'trial' );
			$order->save();
		}

		return $order_id;
	}

	/**
	 * Process subscription renewal.
	 *
	 * @param float    $amount_to_charge
	 * @param WC_Order $renewal_order The WooCommerce order that will be created as a result of the renewal.
	 * @return void
	 */
	public function process_scheduled_payment( $amount_to_charge, $renewal_order ) {
		$recurring_token = $this->get_recurring_tokens( $renewal_order->get_id() );

		$response = KP_WC()->api->create_recurring_order( kp_get_klarna_country( $renewal_order ), $recurring_token, $renewal_order->get_id() );
		if ( ! is_wp_error( $response ) ) {
			$klarna_order_id = $response['order_id'];
			$renewal_order->add_order_note( sprintf( __( 'Subscription payment made with Klarna. Klarna order id: %s', 'klarna-payments-for-woocommerce' ), $klarna_order_id ) );
			kp_save_order_meta_data( $renewal_order, $response );
		} else {
			$error_message = $response->get_error_message();
			// Translators: Error message.
			$renewal_order->add_order_note( sprintf( __( 'Subscription payment failed with Klarna. Reason: %1$s', 'klarna-payments-for-woocommerce' ), $error_message ) );
		}

		$subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order->get_id() );
		foreach ( $subscriptions as $subscription ) {
			if ( isset( $klarna_order_id ) ) {
				$subscription->payment_complete( $klarna_order_id );
			} else {
				$subscription->payment_failed();
			}

			// Save to the subscription.
			self::save_recurring_token( $subscription->get_id(), $recurring_token );
		}

		// Save to the WC order.
		self::save_recurring_token( $renewal_order->get_id(), $recurring_token );
	}

	/**
	 * Cancel the customer token to prevent further payments using the token.
	 *
	 * Note: When changing payment method, WC Subscriptions will cancel the subscription with existing payment gateway (which triggers this functions), and create a new one. Thus the new subscription must generate a new customer token.
	 *
	 * @see WC_Subscriptions_Change_Payment_Gateway::update_payment_method
	 *
	 * @param mixed $subscription WC_Subscription.
	 * @return void
	 */
	public function cancel_scheduled_payment( $subscription ) {
		// Prevent a recursion of this function when we save the subscription.
		if ( did_action( 'woocommerce_subscription_cancelled_' . self::GATEWAY_ID ) > 1 ) {
			return;
		}

		$recurring_token = $this->get_recurring_tokens( $subscription->get_id() );

		$response = KP_WC()->api->cancel_recurring_order( kp_get_klarna_country( $subscription ), $recurring_token );
		if ( ! is_wp_error( $response ) ) {
			$subscription->add_order_note( __( 'Subscription cancelled with Klarna Payments.', 'klarna-payments-for-woocommerce' ) );
		} else {
			$error_message = $response->get_error_message();
			// Translators: Error message.
			$subscription->add_order_note( sprintf( __( 'Subscription cancellation failed with Klarna Payments. Reason: %1$s', 'klarna-payments-for-woocommerce' ), $error_message ) );
		}

		// The session data must be deleted since Klarna doesn't allow reusing a session when generating a new customer token to change payment method.
		$subscription->delete_meta_data( '_kp_session_data' );
		$subscription->save();
	}

	/**
	 * Set the redirect URLs for the hosted payment page.
	 *
	 * Used for changing payment method.
	 *
	 * @param array $request The Klarna request.
	 * @return array
	 */
	public function set_subscription_order_redirect_urls( $request ) {
		if ( ! self::is_change_payment_method() ) {
			return $request;
		}

		$key          = filter_input( INPUT_GET, 'key', FILTER_SANITIZE_SPECIAL_CHARS );
		$order_id     = wc_get_order_id_by_order_key( $key );
		$subscription = wc_get_order( $order_id );
		$body         = json_decode( $request['body'], true );

		$success_url           = add_query_arg(
			array(
				'authorization_token' => '{{authorization_token}}',
			),
			$subscription->get_view_order_url()
		);
		$body['merchant_urls'] = array(
			'success' => $success_url,
			'cancel'  => $subscription->get_change_payment_method_url(),
			'back'    => $subscription->get_change_payment_method_url(),
			'failure' => $subscription->get_change_payment_method_url(),
			'error'   => $subscription->get_change_payment_method_url(),
		);

		$request['body'] = wp_json_encode( $body );
		return $request;
	}

	/**
	 * Handle the redirect from the hosted payment page.
	 *
	 * Used for changing payment method.
	 *
	 * @param int $subscription_id The subscription ID.
	 * @return void
	 */
	public function handle_redirect_from_change_payment_method( $subscription_id ) {
		$auth_token = filter_input( INPUT_GET, 'authorization_token', FILTER_SANITIZE_SPECIAL_CHARS );
		if ( ! isset( $auth_token ) ) {
			return;
		}

		$subscription = wcs_get_subscription( $subscription_id );
		$response     = KP_WC()->api->create_customer_token( kp_get_klarna_country( $subscription ), $auth_token, $subscription_id );
		if ( is_wp_error( $response ) ) {
			$message = sprintf(
				/* translators: Error message. */
				__( 'Failed to create recurring token. Reason: %s', 'klarna-payments-for-woocommerce' ),
				$response->get_error_message()
			);
		} else {
			$message = sprintf(
			/* translators: Recurring token. */
				__( 'Recurring token created: %s', 'klarna-payments-for-woocommerce' ),
				$response['token_id']
			);

			self::save_recurring_token( $subscription_id, $response['token_id'] );
		}

		$subscription->add_order_note( $message );
		$subscription->save();
	}

	/**
	 * Set the subscription cost to 0.
	 * This is required when changing payment method.
	 *
	 * @param array $request The Klarna request.
	 * @return array
	 */
	public function set_subscription_to_free( $request ) {
		if ( ! self::is_change_payment_method() ) {
			return $request;
		}

		$body = json_decode( $request['body'], true );
		foreach ( $body['order_lines'] as $item => $order_line ) {
			$body['order_lines'][ $item ]['unit_price']   = 0;
			$body['order_lines'][ $item ]['total_amount'] = 0;
		}

		// 0 order amounts are allowed if the purchase intent is 'tokenize'. On the intent 'buy_and_tokenize' 0 order amounts are not allowed.
		$body['intent'] = 'tokenize';

		$request['body'] = wp_json_encode( $body );
		return $request;
	}

	/**
	 * Set the purchase intent to 'tokenize' for trial subscriptions.
	 *
	 * The 'buy_and_tokenize' intent is not allowed for 0 order amounts.
	 *
	 * @param array $request The Klarna request.
	 * @return array
	 */
	public function set_tokenize_intent( $request ) {
		$body = json_decode( $request['body'], true );

		$body['intent'] = 'buy';
		if ( self::cart_has_subscription() ) {
			$body['intent'] = 'buy_and_tokenize';

			// Only allow free orders if the cart contains a subscription (not limited to trial subscription as a subscription can become free if a 100% discount coupon is applied).
			if ( 0.0 === floatval( $body['order_amount'] ) ) {
				$body['intent'] = 'tokenize';
			}
		}

		$request['body'] = wp_json_encode( $body );
		return $request;
	}

	/**
	 * Save the payment and recurring token to the order and its subscription(s).
	 *
	 * @param string $order_id The WooCommerce order id.
	 * @param string $recurring_token The recurring token ("customer token").
	 * @return void
	 */
	public static function save_recurring_token( $order_id, $recurring_token ) {
		$order = wc_get_order( $order_id );
		$order->update_meta_data( self::RECURRING_TOKEN, $recurring_token );

		foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'any' ) ) as $subscription ) {
			$subscription->update_meta_data( self::RECURRING_TOKEN, $recurring_token );
			$subscription->save();
		}

		$order->save();
	}

	/**
	 * Retrieve the necessary tokens required for subscriptions (unattended) payments.
	 *
	 * @param  int $order_id The WooCommerce order id.
	 * @return string The recurring token. If none is found, an empty string is returned.
	 */
	public static function get_recurring_tokens( $order_id ) {
		$order           = wc_get_order( $order_id );
		$recurring_token = $order->get_meta( self::RECURRING_TOKEN );

		if ( empty( $recurring_token ) ) {
			$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
			foreach ( $subscriptions as $subscription ) {
				$parent_order    = $subscription->get_parent();
				$recurring_token = $parent_order->get_meta( self::RECURRING_TOKEN );

				if ( ! empty( $recurring_token ) ) {
					break;
				}
			}
		}

		return $recurring_token;
	}

	/**
	 * Get a subscription's parent order.
	 *
	 * @param int $order_id The WooCommerce order id.
	 * @return WC_Order|false The parent order or false if none is found.
	 */
	public static function get_parent_order( $order_id ) {
		$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
		foreach ( $subscriptions as $subscription ) {
			$parent_order = $subscription->get_parent();
			return $parent_order;
		}

		return false;
	}

	/**
	 * Check if the current request is for changing the payment method.
	 *
	 * @return bool
	 */
	public static function is_change_payment_method() {
		return isset( $_GET['change_payment_method'] );
	}



	/**
	 * Check if an order contains a subscription.
	 *
	 * @param WC_Order $order The WooCommerce order or leave empty to use the cart (default).
	 * @return bool
	 */
	public static function order_has_subscription( $order ) {
		if ( empty( $order ) ) {
			return false;
		}

		return function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order, array( 'parent', 'resubscribe', 'switch', 'renewal' ) );
	}

	/**
	 * Check if a cart contains a subscription.
	 *
	 * @return bool
	 */
	public static function cart_has_subscription() {
		if ( ! is_checkout() ) {
			return false;
		}

		return ( class_exists( 'WC_Subscriptions_Cart' ) && WC_Subscriptions_Cart::cart_contains_subscription() ) || ( function_exists( 'wcs_cart_contains_failed_renewal_order_payment' ) && wcs_cart_contains_failed_renewal_order_payment() );
	}

	/**
	 * Add Klarna hosted payment page as allowed external url for wp_safe_redirect.
	 * We do this because WooCommerce Subscriptions use wp_safe_redirect when processing a payment method change request (from v5.1.0).
	 *
	 * @param array $hosts Domains that are allowed when wp_safe_redirect is used.
	 * @return array
	 */
	public function extend_allowed_domains_list( $hosts ) {
		$hosts[] = 'pay.playground.klarna.com';
		$hosts[] = 'pay.klarna.com';
		return $hosts;
	}

	/**
	 * Shows the recurring token for the order.
	 *
	 * @param WC_Order $order The WooCommerce order.
	 * @return void
	 */
	public function show_recurring_token( $order ) {
		if ( 'shop_subscription' === $order->get_type() ) {
			$recurring_token = $order->get_meta( self::RECURRING_TOKEN );
			?>
			<div class="order_data_column" style="clear:both; float:none; width:100%;">
				<div class="address">
					<p>
						<strong><?php echo esc_html( 'Klarna recurring token' ); ?>:</strong><?php echo esc_html( $recurring_token ); ?>
					</p>
				</div>
				<div class="edit_address">
				<?php
					woocommerce_wp_text_input(
						array(
							'id'            => self::RECURRING_TOKEN,
							'label'         => __( 'Klarna recurring token', 'klarna-payments-for-woocommerce' ),
							'wrapper_class' => '_billing_company_field',
							'value'         => $recurring_token,
						)
					);
				?>
				</div>
			</div>
				<?php
		}
	}
}