# learnpress/4.3.7/inc/gateways/class-lp-gateway-abstract.php

LearnPress – WordPress LMS Plugin for Create and Sell Online Courses, version 4.3.7. 1,075 lines.

- Page: https://pluginprobe.com/plugins/learnpress/4.3.7/code/inc/gateways/class-lp-gateway-abstract.php
- Raw: https://pluginprobe.com/plugins/learnpress/4.3.7/raw/inc/gateways/class-lp-gateway-abstract.php
- Modified: 2026-05-23T18:20:54+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/learnpress/4.3.7/code/inc/gateways/class-lp-gateway-abstract.php#L10-L20`.

```php
<?php

/**
 * Class LP_Gateway_Abstract
 *
 * @author  ThimPress
 * @package LearnPress/Classes
 * @version 1.0
 */

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

class LP_Gateway_Abstract extends LP_Abstract_Settings {

	/**
	 * Shared subscription order meta keys.
	 */
	const META_SUBSCRIPTION_ID                   = '_lp_subscription_id';
	const META_SUBSCRIPTION_CUSTOMER_ID          = '_lp_subscription_customer_id';
	const META_SUBSCRIPTION_PLAN_ID              = '_lp_subscription_plan_id';
	const META_SUBSCRIPTION_QUANTITY             = '_lp_subscription_quantity';
	const META_SUBSCRIPTION_STATUS               = '_lp_subscription_status';
	const META_SUBSCRIPTION_STATUS_TMP           = '_lp_subscription_status_tmp';
	const META_SUBSCRIPTION_RENEWAL_KEY          = '_lp_subscription_renewal_key';
	const META_SUBSCRIPTION_LAST_EVENT_ID        = '_lp_subscription_last_event_id';
	const META_SUBSCRIPTION_EVENT_ID             = '_lp_subscription_event_id';
	const META_SUBSCRIPTION_MANAGE_URL           = '_lp_subscription_manage_url';
	const META_SUBSCRIPTION_DATA_RECEIVER        = '_lp_subscription_data_receiver';
	const META_SUBSCRIPTION_DATA_PAYMENT_SUCCESS = '_lp_subscription_data_payment_success';

	/**
	 * @var null|string
	 */
	public $id = null;
	/**
	 * @var LP_Settings
	 */
	protected $settings;
	/**
	 * Name of gateway will be displayed in admin settings.
	 *
	 * @var string
	 */
	protected $method_title = '';

	/**
	 * Description of gateway will be displayed in admin settings.
	 *
	 * @var string
	 */
	protected $method_description = '';

	/**
	 * @var string
	 */
	public $order_button_text = '';

	/**
	 * This payment is turn on or off?
	 *
	 * @var string
	 */
	public $enabled = 'no';

	/**
	 * @var null
	 */
	public $title = null;

	/**
	 * @var null
	 */
	public $description = null;

	/**
	 * @var string
	 */
	protected $icon = '';
	/**
	 * @var bool set default select when checkout
	 */
	public $is_selected = false;

	/**
	 * Constructor
	 */
	public function __construct() {
		/*
		if ( ! $this->admin_name ) {
			$this->admin_name = preg_replace( '!LP_Gateway_!', '', get_class( $this ) );
		}*/

		if ( ! $this->id ) {
			$this->id = sanitize_title( $this->title );
		}

		$this->settings = LP_Settings::instance()->get_group( $this->id );
		$this->enabled  = $this->settings->get( 'enable', 'no' );

		add_filter( 'learn-press/admin/get-settings/admin-options-' . $this->id, array( $this, 'get_settings' ) );
	}

	/**
	 * Return unique Id of payment
	 *
	 * @return null|string
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Return method title.
	 *
	 * @return string
	 */
	public function get_method_title() {
		return $this->method_title;
	}

	/**
	 * Return method description.
	 *
	 * @return string
	 */
	public function get_method_description() {
		return $this->method_description;
	}

	/**
	 * Return method title displays in front end.
	 *
	 * @return string
	 */
	public function get_title() {
		return apply_filters( 'learn_press_gateway_title', $this->title, $this->id );
	}

	/**
	 * Return method description displays in front end.
	 *
	 * @return string
	 */
	public function get_description() {
		return apply_filters( 'learn_press_gateway_description', $this->description, $this->id );
	}

	/**
	 * Payment is turn on or off?
	 *
	 * @return bool
	 */
	public function is_enabled() {
		return $this->enabled === 'yes';
	}

	public function enable( $status ) {
		if ( is_bool( $status ) ) {
			$this->enabled = $status;

			$options = get_option( 'learn_press_' . $this->get_id() );

			if ( ! $options ) {
				$options = array();
			}

			$options['enable'] = $status ? 'yes' : 'no';
			update_option( 'learn_press_' . $this->get_id(), $options );
		}

		return $this->enabled == 'yes';
	}

	/**
	 * Process the payment.
	 *
	 * @param $order_id
	 *
	 * @return array
	 */
	public function process_payment( $order_id ) {
		return array();
	}

	/**
	 * Check if order should use subscription flow.
	 *
	 * Integrations decide via filter `learn-press/gateway/subscription-order`.
	 *
	 * @param LP_Order $order
	 *
	 * @return bool
	 */
	public function is_subscription_order( LP_Order $order ): bool {

		$order_id       = $order->get_id();
		$saved_price_id = sanitize_text_field( (string) get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ) );
		if ( ! empty( $saved_price_id ) ) {
			return true;
		}
		$is_subscription = (bool) apply_filters(
			'learn-press/gateway/subscription-order',
			false,
			$order,
			$this
		);

		return $is_subscription;
	}

	/**
	 * Get subscription context for provider APIs.
	 *
	 * Returns a gateway-agnostic payload that child gateways can pass to
	 * `pay_subscription()` after optional gateway-specific normalization.
	 *
	 * @param LP_Order $order
	 *
	 * @return array
	 */
	public function get_subscription_context( LP_Order $order ): array {
		$order_id = $order->get_id();

		$context = array(
			'price_id'    => get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ),
			'quantity'    => (int) get_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, true ),
			'success_url' => $this->get_return_url( $order ),
			'cancel_url'  => learn_press_get_page_link( 'checkout' ),
			'metadata'    => array(
				'lp_order_id'   => (string) $order_id,
				'lp_order_key'  => (string) $order->get_order_key(),
				'lp_gateway'    => $this->get_id(),
				'lp_user_id'    => (string) $order->get_user_id(),
				'lp_order_type' => 'subscription',
			),
		);

		if ( empty( $context['quantity'] ) ) {
			$context['quantity'] = 1;
		}

		return (array) apply_filters( 'learn-press/gateway/subscription-context', $context, $order, $this );
	}

	/**
	 * Persist subscription identifiers to order before payment execution.
	 *
	 * This allows payment methods to operate only on normalized parameters and
	 * avoid re-detecting custom integration attributes inside gateway code.
	 *
	 * @param LP_Order $order
	 * @param array    $data
	 *
	 * @return void
	 */
	protected function persist_subscription_payment_identifiers( LP_Order $order, array $data ) {

		$order_id = $order->get_id();

		update_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, sanitize_text_field( (string) ( $data['price_id'] ?? '' ) ) );
		update_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, max( 1, absint( $data['quantity'] ?? 1 ) ) );
	}
	/**
	 * Resolve normalized subscription payment params from order context.
	 *
	 * Behavior:
	 * - If order contains persisted subscription identifiers, treat it as
	 *   subscription payment.
	 * - If order is marked subscription by integration filter but has no
	 *   `price_id`, return a validation error early.
	 * - Otherwise return empty array (one-time payment flow).
	 *
	 * @param LP_Order $order
	 *
	 * @return array
	 * @throws Exception
	 */
	public function resolve_subscription_payment_data( LP_Order $order ): array {
		$context = $this->get_subscription_context( $order );
		$context = wp_parse_args(
			$context,
			array(
				'price_id'    => '',
				'quantity'    => 1,
				'success_url' => '',
				'cancel_url'  => '',
				'metadata'    => array(),
			)
		);

		$context['price_id'] = sanitize_text_field( (string) $context['price_id'] );
		$context['quantity'] = max( 1, absint( $context['quantity'] ) );
		$context['metadata'] = is_array( $context['metadata'] ) ? $context['metadata'] : array();

		if ( ! empty( $context['price_id'] ) ) {
			$this->persist_subscription_payment_identifiers( $order, $context );
			return $context;
		}

		if ( $this->is_subscription_order( $order ) ) {
			throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
		}

		return array();
	}

	/**
	 * Normalize and validate the shared subscription checkout payload.
	 *
	 * This method is intentionally gateway-agnostic and is used by child gateways
	 * (e.g. Stripe/PayPal) before they build provider-specific API requests.
	 *
	 * Payload contract:
	 * - `price_id` (string, required): provider-side configured recurring price/plan id.
	 * - `quantity` (int): defaults to 1 when missing/invalid/zero.
	 * - `success_url` / `cancel_url` (string, required): absolute callback URLs.
	 * - `metadata` (array): optional identifiers (order/user/etc.) for reconciliation.
	 *
	 * @param array $data
	 *
	 * @return array Normalized payload array.
	 * @throws Exception
	 */
	protected function validate_subscription_payload( array $data ): array {
		// Apply safe defaults to guarantee a stable input shape.
		$data = wp_parse_args(
			$data,
			array(
				'price_id'    => '',
				'quantity'    => 1,
				'success_url' => '',
				'cancel_url'  => '',
				'metadata'    => array(),
			)
		);

		// Scalar sanitation/coercion for fields commonly coming from request context.
		$data['price_id'] = sanitize_text_field( wp_unslash( (string) $data['price_id'] ) );
		$data['quantity'] = absint( $data['quantity'] );
		if ( empty( $data['quantity'] ) ) {
			$data['quantity'] = 1;
		}

		// URLs are stored in raw form for outbound provider API requests.
		$data['success_url'] = esc_url_raw( (string) $data['success_url'] );
		$data['cancel_url']  = esc_url_raw( (string) $data['cancel_url'] );

		// Defensive normalization for optional structured fields.
		$data['metadata'] = is_array( $data['metadata'] ) ? $data['metadata'] : array();

		// price_id is the minimum provider binding required for subscription checkout.
		if ( empty( $data['price_id'] ) ) {
			throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
		}

		// Redirect URLs are mandatory for provider-hosted checkout flows.
		if ( empty( $data['success_url'] ) || empty( $data['cancel_url'] ) ) {
			throw new Exception( __( 'Missing subscription return URLs.', 'learnpress' ) );
		}

		return $data;
	}

	/**
	 * Generic subscription checkout flow.
	 *
	 * Child gateways should override this method and return a payload with at
	 * least `status` and `redirect_url` on success.
	 *
	 * @param array $data
	 *
	 * @return array
	 * @throws Exception
	 */
	public function pay_subscription( array $data ): array {
		throw new Exception( sprintf( __( 'Gateway %s does not support subscription payment.', 'learnpress' ), $this->get_id() ) );
	}

	/**
	 * Normalize and validate shared plan-creation payload.
	 *
	 * Common payload contract:
	 * - `name` (string, required when `product_id` is empty)
	 * - `amount` (float, required, > 0)
	 * - `currency` (string, required)
	 * - `interval` (day|week|month|year)
	 * - `interval_count` (int, default 1)
	 * - `setup_fee` (float, optional, >= 0)
	 * - `product_id` (string, optional)
	 * - `metadata` (array, optional)
	 *
	 * @param array $data
	 *
	 * @return array
	 * @throws Exception
	 */
	protected function validate_data_plan_payload( array $data ): array {
		$data                   = wp_parse_args(
			$data,
			array(
				'name'           => '',
				'amount'         => 0,
				'currency'       => learn_press_get_currency(),
				'interval'       => 'month',
				'interval_count' => 1,
				'setup_fee'      => 0,
				'product_id'     => '',
				'metadata'       => array(),
			)
		);
		$data['name']           = sanitize_text_field( wp_unslash( (string) $data['name'] ) );
		$data['amount']         = (float) $data['amount'];
		$data['currency']       = strtoupper( sanitize_text_field( wp_unslash( (string) $data['currency'] ) ) );
		$data['interval']       = strtolower( sanitize_key( (string) $data['interval'] ) );
		$data['interval_count'] = max( 1, absint( $data['interval_count'] ) );
		$data['setup_fee']      = (float) $data['setup_fee'];
		$data['product_id']     = sanitize_text_field( wp_unslash( (string) $data['product_id'] ) );
		$data['metadata']       = is_array( $data['metadata'] ) ? $data['metadata'] : array();
		if ( empty( $data['product_id'] ) && empty( $data['name'] ) ) {
			throw new Exception( __( 'Missing subscription plan name.', 'learnpress' ) );
		}

		if ( $data['amount'] <= 0 ) {
			throw new Exception( __( 'Invalid subscription amount.', 'learnpress' ) );
		}

		if ( $data['setup_fee'] < 0 ) {
			throw new Exception( __( 'Invalid subscription setup fee.', 'learnpress' ) );
		}

		if ( empty( $data['currency'] ) ) {
			throw new Exception( __( 'Missing subscription currency.', 'learnpress' ) );
		}
		$allowed_intervals = array( 'day', 'week', 'month', 'year' );
		if ( ! in_array( $data['interval'], $allowed_intervals, true ) ) {
			throw new Exception( __( 'Invalid subscription interval.', 'learnpress' ) );
		}

		return $data;
	}

	/**
	 * Generic provider plan/price creation flow.
	 *
	 * Child gateways should override and return created provider identifiers,
	 * typically a `price_id`/`plan_id` to be used later by `pay_subscription()`.
	 *
	 * @param array $data
	 *
	 * @return array
	 * @throws Exception
	 */
	public function create_plan( array $data ): array {

		throw new Exception( sprintf( __( 'Gateway %s does not support subscription plan creation.', 'learnpress' ), $this->get_id() ) );
	}

	/**
	 * List provider plans/prices with optional filtering/pagination args.
	 *
	 * @param array $args
	 *
	 * @return array
	 * @throws Exception
	 */
	public function list_plans( array $args = array() ): array {

		throw new Exception( sprintf( __( 'Gateway %s does not support listing subscription plans.', 'learnpress' ), $this->get_id() ) );
	}

	/**
	 * Fetch provider plan/price details by plan id.
	 *
	 * Child gateways should override and return at least:
	 * - `plan`: raw provider response
	 * - `summary`: normalized fields used by integrations to compare updates
	 *   (amount/currency/interval/interval_count/setup_fee/status)
	 *
	 * @param string $plan_id
	 *
	 * @return array
	 * @throws Exception
	 */
	public function get_plan( string $plan_id ): array {

		throw new Exception( sprintf( __( 'Gateway %s does not support fetching subscription plan.', 'learnpress' ), $this->get_id() ) );
	}

	/**
	 * Update provider plan details by plan id.
	 *
	 * @param string $plan_id
	 * @param array  $data
	 *
	 * @return array
	 * @throws Exception
	 */
	public function update_plan( string $plan_id, array $data ): array {

		throw new Exception( sprintf( __( 'Gateway %s does not support updating subscription plan.', 'learnpress' ), $this->get_id() ) );
	}

	/**
	 * Delete/deactivate provider plan by plan id.
	 *
	 * @param string $plan_id
	 *
	 * @return array
	 * @throws Exception
	 */
	public function delete_plan( string $plan_id ): array {

		throw new Exception( sprintf( __( 'Gateway %s does not support deleting subscription plan.', 'learnpress' ), $this->get_id() ) );
	}
	/**
	 * Generic subscription webhook listener.
	 *
	 * Child gateways should override and orchestrate:
	 * verify -> normalize -> manager dispatch.
	 *
	 * @param WP_REST_Request $request
	 *
	 * @return array
	 * @throws Exception
	 */
	public function listen_webhook_subscription( WP_REST_Request $request ): array {
		throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook.', 'learnpress' ), $this->get_id() ) );
	}

	/**
	 * Receive subscription webhook from provider.
	 *
	 * @throws Exception
	 *
	 * @since 4.3.7
	 * @version 1.0.0
	 */
	public function capture_subscription_webhook( WP_REST_Request $request ) {
		throw new Exception(
			sprintf(
				__( 'Gateway %s does not support subscription webhook.', 'learnpress' ),
				$this->get_id()
			)
		);
	}

	/**
	 * Verify subscription webhook payload/signature with provider.
	 *
	 * Child gateways should return verified provider event payload/object.
	 *
	 * @param array $webhook_data Generic webhook data extracted from transport layer.
	 *
	 * @return array|object
	 * @throws Exception
	 */
	public function verify_subscription_webhook( array $webhook_data ) {

		throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook verification.', 'learnpress' ), $this->get_id() ) );
	}

	/**
	 * Build normalized webhook data array from transport-specific REST request.
	 *
	 * Contract:
	 * - raw_body: raw payload string (for signature verification like Stripe).
	 * - body: decoded JSON array when $decode_body is true, otherwise null.
	 * - headers: map of required header keys (lowercase) to raw header values.
	 *
	 * @param WP_REST_Request $request
	 * @param array           $required_headers
	 * @param bool            $decode_body
	 *
	 * @return array
	 */
	protected function build_webhook_data_from_request( WP_REST_Request $request, array $required_headers = array(), bool $decode_body = true ): array {

		$raw_body = (string) $request->get_body();
		$headers  = array();

		foreach ( $required_headers as $required_header ) {
			$required_header             = strtolower( sanitize_key( (string) $required_header ) );
			$headers[ $required_header ] = (string) $request->get_header( $required_header );
		}

		$body = null;
		if ( $decode_body ) {
			$body = LP_Helper::json_decode( $raw_body, true );
		}

		return array(
			'raw_body' => $raw_body,
			'body'     => is_array( $body ) ? $body : null,
			'headers'  => $headers,
		);
	}

	/**
	 * Validate normalized webhook payload contract before provider verification.
	 *
	 * This centralizes fail-fast checks so each gateway does not re-implement
	 * required key/header validation and accidentally diverge key names.
	 *
	 * @param array $webhook_data
	 * @param array $required_top_level_keys Allowed: raw_body, body, headers.
	 * @param array $required_headers
	 *
	 * @return void
	 * @throws Exception
	 */
	protected function validate_webhook_data_contract( array $webhook_data, array $required_top_level_keys = array(), array $required_headers = array() ) {

		$missing = array();

		foreach ( $required_top_level_keys as $required_key ) {
			$required_key = sanitize_key( (string) $required_key );

			switch ( $required_key ) {
				case 'raw_body':
					if ( empty( $webhook_data['raw_body'] ) || ! is_string( $webhook_data['raw_body'] ) ) {
						$missing[] = 'raw_body';
					}
					break;
				case 'body':
					if ( empty( $webhook_data['body'] ) || ! is_array( $webhook_data['body'] ) ) {
						$missing[] = 'body';
					}
					break;
				case 'headers':
					if ( ! isset( $webhook_data['headers'] ) || ! is_array( $webhook_data['headers'] ) ) {
						$missing[] = 'headers';
					}
					break;
			}
		}

		$headers_map = is_array( $webhook_data['headers'] ?? null ) ? $webhook_data['headers'] : array();
		foreach ( $required_headers as $required_header ) {
			$required_header = strtolower( sanitize_key( (string) $required_header ) );
			$header_value    = sanitize_text_field( (string) ( $headers_map[ $required_header ] ?? '' ) );
			if ( '' === $header_value ) {
				$missing[] = 'headers.' . $required_header;
			}
		}

		if ( ! empty( $missing ) ) {
				throw new Exception(
					sprintf(
					/* translators: %s: comma separated required webhook fields. */
						__( 'Invalid webhook request data: missing %s.', 'learnpress' ),
						implode( ', ', array_unique( $missing ) )
					),
					400
				);
		}
	}

	/**
	 * Normalize provider webhook event to LP event payload.
	 *
	 * Child gateways should map provider-specific event types/fields into this
	 * canonical schema so Subscription Manager can process consistently.
	 *
	 * @param array|object $provider_event
	 *
	 * @return array
	 */
	public function normalize_subscription_event( $provider_event ): array {
		$event = array(
			'event_id'        => '',
			'event_type'      => '',
			'subscription_id' => '',
			'customer_id'     => '',
			'price_id'        => '',
			'parent_order_id' => 0,
			'transaction_id'  => '',
			'amount'          => 0,
			'currency'        => '',
			'status'          => '',
			'metadata'        => array(),
			'raw'             => $provider_event,
		);

		return (array) apply_filters( 'learn-press/gateway/subscription/event', $event, $provider_event, $this );
	}

	/**
	 * Normalize provider webhook event to LP event payload.
	 *
	 * Merge data from provider webhook event to LP event payload.
	 *
	 * @param array $webhook_data [lp_order_id, plan_id, subscription_id, subscription_status]
	 *
	 * @return void
	 */
	public function normalize_subscription_data( array &$webhook_data = [] ) {
		$webhook_data = array_merge(
			array(
				'lp_order_id'            => 0,
				'lp_plan_id'             => '',
				'lp_subscription_id'     => '',
				'lp_subscription_status' => '',
			),
			$webhook_data
		);
	}

	/**
	 * Process subscription by status.
	 *
	 * Status flow:
	 * - trial/activated: triggers once on the parent LP Order (first payment).
	 * - renewed: triggers on each renewal, creating a child LP Order.
	 * - created/expired/canceled/suspended: logged, do_action only, no order state change.
	 *
	 * @param LP_Order $lp_order
	 * @param string $lp_subscription_status_set_to_handle Status you want set to handle by case
	 * @param array $webhook_data
	 *
	 * @return void
	 * @throws Exception
	 * @since 4.3.7
	 * @version 1.0.0
	 */
	public function process_subscription_by_status(
		$lp_order,
		string $lp_subscription_status_set_to_handle,
		array $webhook_data = []
	) {
		LP_Debug::log_to_comment( 'Progress wit data: ' . json_encode( $webhook_data, JSON_UNESCAPED_UNICODE ) );

		switch ( $lp_subscription_status_set_to_handle ) {
			case LP_Subscription_Manager::STATUS_TRIAL:
				// For trial, update LP order to complete, set subscription status to trial
				$lp_order->add_note(
					sprintf(
						'LP Order: %s %s: %s. %s. %s, %s',
						sprintf(
							'<a href="%s">%s</a>',
							$lp_order->get_edit_link(),
							$lp_order->get_order_number()
						),
						__( 'Started trialing created at', 'learnpress' ),
						$webhook_data['create_time'] ?? '',
						sprintf( '%s %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
						sprintf(
							__( 'Subscription ID: %s', 'learnpress' ),
							$webhook_data['lp_subscription_id']
						),
						sprintf(
							__( 'Plan ID: %s', 'learnpress' ),
							$webhook_data['lp_plan_id']
						)
					)
				);
				$this->process_subscription_when_payment_first( $lp_order, LP_Subscription_Manager::STATUS_TRIAL, $webhook_data );
				do_action( 'learn-press/subscription/trial', $this, $lp_order, $webhook_data );
				break;
			case LP_Subscription_Manager::STATUS_ACTIVATED:
				// For payment plan first success, update LP order to complete, set subscription status to activated
				$this->process_subscription_when_payment_first( $lp_order, $lp_subscription_status_set_to_handle, $webhook_data );
				$lp_order->add_note(
					sprintf(
						'LP Order: %s %s: %s. %s. %s, %s',
						sprintf(
							'<a href="%s">%s</a>',
							$lp_order->get_edit_link(),
							$lp_order->get_order_number()
						),
						__( 'Activated created at', 'learnpress' ),
						$webhook_data['create_time'] ?? '',
						sprintf( '%s: %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
						sprintf(
							__( 'Subscription ID: %s', 'learnpress' ),
							$webhook_data['lp_subscription_id']
						),
						sprintf(
							__( 'Plan ID: %s', 'learnpress' ),
							$webhook_data['lp_plan_id']
						)
					)
				);
				do_action( 'learn-press/subscription/active', $this, $lp_order, $webhook_data );
				break;
			case LP_Subscription_Manager::STATUS_RENEWED:
				// For payment plan renew success, parent order is completed and payment renew success
				$this->process_subscription_when_payment_renew_success( $lp_order, $webhook_data );
				$lp_order->add_note(
					sprintf(
						'LP Order: %s %s: %s. %s. %s, %s',
						sprintf(
							'<a href="%s">%s</a>',
							$lp_order->get_edit_link(),
							$lp_order->get_order_number()
						),
						__( 'Renew created at', 'learnpress' ),
						$webhook_data['create_time'] ?? '',
						sprintf( '%s: %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
						sprintf(
							__( 'Subscription ID: %s', 'learnpress' ),
							$webhook_data['lp_subscription_id']
						),
						sprintf(
							__( 'Plan ID: %s', 'learnpress' ),
							$webhook_data['lp_plan_id']
						)
					)
				);
				do_action( 'learn-press/subscription/renew', $this, $lp_order, $webhook_data );
				break;
			case LP_Subscription_Manager::STATUS_EXPIRED:
				// For payment plan expired, not impact orders
				$lp_order->add_note(
					sprintf(
						'LP Order: %s %s: %s. %s, %s',
						sprintf(
							'<a href="%s">%s</a>',
							$lp_order->get_edit_link(),
							$lp_order->get_order_number()
						),
						__( 'Expired created at', 'learnpress' ),
						$webhook_data['create_time'] ?? '',
						sprintf(
							__( 'Subscription ID: %s', 'learnpress' ),
							$webhook_data['lp_subscription_id']
						),
						sprintf(
							__( 'Plan ID: %s', 'learnpress' ),
							$webhook_data['lp_plan_id']
						)
					)
				);
				do_action( 'learn-press/subscription/expired', $this, $lp_order, $webhook_data );
				break;
			case LP_Subscription_Manager::STATUS_SUSPENDED:
				// For payment plan suspended, not impact orders
				$lp_order->add_note(
					sprintf(
						'LP Order: %s %s: %s. %s, %s',
						sprintf(
							'<a href="%s">%s</a>',
							$lp_order->get_edit_link(),
							$lp_order->get_order_number()
						),
						__( 'Suspended at', 'learnpress' ),
						$webhook_data['create_time'] ?? '',
						sprintf(
							__( 'Subscription ID: %s', 'learnpress' ),
							$webhook_data['lp_subscription_id']
						),
						sprintf(
							__( 'Plan ID: %s', 'learnpress' ),
							$webhook_data['lp_plan_id']
						)
					)
				);
				do_action( 'learn-press/subscription/suspended', $this, $lp_order, $webhook_data );
				break;
			case LP_Subscription_Manager::STATUS_CANCELLED:
				// For payment plan canceled, not impact orders
				$lp_order->add_note(
					sprintf(
						'LP Order: %s %s: %s. %s, %s',
						sprintf(
							'<a href="%s">%s</a>',
							$lp_order->get_edit_link(),
							$lp_order->get_order_number()
						),
						__( 'Canceled at', 'learnpress' ),
						$webhook_data['create_time'] ?? '',
						sprintf(
							__( 'Subscription ID: %s', 'learnpress' ),
							$webhook_data['lp_subscription_id']
						),
						sprintf(
							__( 'Plan ID: %s', 'learnpress' ),
							$webhook_data['lp_plan_id']
						)
					)
				);
				do_action( 'learn-press/subscription/cancelled', $this, $lp_order, $webhook_data );
				break;
		}

		do_action( 'learn-press/subscription/process', $this, $lp_order, $webhook_data );
	}

	/**
	 * Process order when subscription payment first.
	 *
	 * @since 4.3.7
	 * @version 1.0.0
	 */
	public function process_subscription_when_payment_first(
		LP_Order $order,
		string $lp_subscription_status_set_to_handle,
		$webhook_data
	) {
		$lp_subscription_amount   = $webhook_data['lp_subscription_amount'] ?? 0;
		$lp_subscription_currency = $webhook_data['lp_subscription_currency'] ?? '';
		$order->update_status( LP_ORDER_COMPLETED );
		update_post_meta(
			$order->get_id(),
			self::META_SUBSCRIPTION_STATUS,
			$lp_subscription_status_set_to_handle
		);
		update_post_meta(
			$order->get_id(),
			'lp_subscription_amount',
			$lp_subscription_amount
		);
		update_post_meta(
			$order->get_id(),
			'lp_subscription_currency',
			$lp_subscription_currency
		);
	}

	/**
	 * Process order when payment recurring success.
	 *
	 * @throws Exception
	 * @since 4.3.7
	 * @version 1.0.0
	 */
	public function process_subscription_when_payment_renew_success( LP_Order $lp_order_parent, $webhook_data ) {
		error_log( 'renew pay' );

		$lp_subscription_amount   = $webhook_data['lp_subscription_amount'] ?? 0;
		$lp_subscription_currency = $webhook_data['lp_subscription_currency'] ?? '';

		// Create new Order child
		$order_renew = new LP_Order();
		$order_renew->set_parent_id( $lp_order_parent->get_id() );
		$order_renew->set_user_id( $lp_order_parent->get_user_id() );
		$order_renew->set_checkout_email( $lp_order_parent->get_checkout_email() );
		$order_renew->set_status( LP_ORDER_COMPLETED );
		$order_renew->set_created_via( 'subscription' );
		$order_renew->set_currency( $lp_subscription_currency ?? $lp_order_parent->get_currency() );
		$order_renew->set_total( $lp_subscription_amount );
		$order_renew->set_subtotal( $lp_subscription_amount );
		$order_renew->set_data( 'payment_method', $lp_order_parent->get_data( 'payment_method' ) );
		$order_renew->set_data( 'payment_method_title', $lp_order_parent->get_payment_method_title() );
		$order_renew->save();

		error_log( 'renew ' . json_encode( $webhook_data, JSON_UNESCAPED_UNICODE ) );

		update_post_meta(
			$order_renew->get_id(),
			self::META_SUBSCRIPTION_DATA_RECEIVER,
			wp_json_encode( $webhook_data, JSON_UNESCAPED_UNICODE )
		);
		update_post_meta(
			$order_renew->get_id(),
			'lp_subscription_amount',
			$lp_subscription_amount
		);
		update_post_meta(
			$order_renew->get_id(),
			'lp_subscription_currency',
			$lp_subscription_currency
		);
	}

	/**
	 * Get provider manage subscription URL for order.
	 *
	 * Child gateways can override when provider offers customer portal pages.
	 *
	 * @param LP_Order $order
	 *
	 * @return string
	 */
	public function get_manage_subscription_url( LP_Order $order ): string {
		$url = get_post_meta( $order->get_id(), self::META_SUBSCRIPTION_MANAGE_URL, true );
		if ( ! is_string( $url ) ) {
			$url = '';
		}

		return (string) apply_filters( 'learn-press/gateway/subscription/manage-url', $url, $order, $this );
	}

	/**
	 * Get the icon of payment displays in front end.
	 *
	 * @return mixed
	 */
	public function get_icon() {
		$size = apply_filters( 'learn-press/default-payment-gateway-icon-sizes', null ); // array( 52, 32 ) is low quatity.

		if ( $size ) {
			$icon_size = sprintf( 'width: %dpx; height: %dpx', $size[0], $size[1] );
		} else {
			$icon_size = '';
		}

		$icon = $this->icon ? '<img class="gateway-icon" src="' . $this->icon . '" alt="' . esc_attr( $this->get_title() ) . '" style="' . $icon_size . '" />' : '';

		return apply_filters( 'learn_press_gateway_icon', $icon, $this->id );
	}

	/**
	 * Return the form where user can input payment details or anything else.
	 *
	 * @return string
	 */
	public function get_payment_form() {
		return '';
	}

	/**
	 * Validate required field before submitting fields.
	 *
	 * @return bool
	 */
	public function validate_fields() {
		// TODO: validate fields if needed
		return true;
	}

	/**
	 * @param LP_Order $order
	 *
	 * @return mixed
	 */
	public function get_return_url( $order = null ) {
		if ( $order ) {
			$return_url = $order->get_checkout_order_received_url();
		} else {
			$return_url = learn_press_get_endpoint_url( 'lp-order-received', '', learn_press_get_page_link( 'checkout' ) );
		}

		return apply_filters( 'learn_press_get_return_url', $return_url, $order );
	}

	public function __get( $prop ) {
		switch ( $prop ) {
			case 'method_title':
			case 'method_description':
			case 'id':
				_deprecated_argument( $prop, '3.0.0', sprintf( __( '%s has been deprecated. Please use % instead of.', 'learnpress' ), $prop, "get_{$prop}" ) );

				return call_user_func( array( $this, "get_{$prop}" ) );
			default:
				return property_exists( $this, $prop ) ? $this->{$prop} : false;
		}
	}

	/**
	 * @since 3.0.0
	 *
	 * return bool
	 * @deprecated 4.2.3.5
	 */
	public function is_display() {
		_deprecated_function( __METHOD__, '4.2.3.5' );
		$display = apply_filters( 'learn-press/payment-method/display', true, $this->id );
		$display = apply_filters( 'learn-press/payment-method-' . $this->id . '/display', $display );

		// @deprecated
		$display = apply_filters( 'learn_press_display_payment_method', $display, $this->id );

		return $display;
	}

	/**
	 * @return string
	 */
	public function __toString() {
		return $this->method_title;
	}
}

```
